原文链接:https://kidwjb.top/archives/206
线程和进程信号量的区别
笔者之前一直使用的都是线程信号量,所以第一次接触到用于进程间同步的进程信号量会去想它和线程信号量有什么区别。
根据结合AI的解释,线程使用的信号量仅同一进程内的线程可见,而进程信号量可跨多个独立进程共享。一般取决于sem_init()的第二参数pshared是0还是1
| 维度 | 线程信号量 | 进程信号量 |
|---|---|---|
| 共享范围 | 仅同一进程内的线程可见 | 可跨多个独立进程共享(IPC) |
| 内存位置 | 普通进程内存(栈/堆/全局区) | 必须位于共享内存或内核命名空间 |
| POSIX 初始化 | sem_init(&sem, 0, value) pshared = 0 |
sem_init(&sem, 1, value) pshared ≠ 0(需配合共享内存) 或使用命名信号量 sem_open() |
| 生命周期 | 随进程结束自动释放;可 sem_destroy() 清理 |
命名信号量具有内核持久性,需 sem_unlink() 显式删除;未命名信号量依赖共享内存生命周期 |
| 性能开销 | 仅需保证进程内线程间内存可见性,开销较小 | 涉及跨进程内存屏障、共享内存同步、权限检查,通常开销略大 |
信号量API
信号量的API也和共享内存一样有两种API,一种是system V一种是POSIX
System V
semget函数
semget函数用来获取或创建信号量
int semget(key_t key, int nsems, int semflg);
-
参数
key是信号量的键值,和共享内存一样,typedef unsigned int key_t,是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证。key用十六进制表示比较好,因为后续可以调试时使用ipcs -s查看 -
参数
nsems是创建信号量集中信号量的个数,该参数只在创建信号量集时有效,这里固定填1。 -
参数
sem_flags是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。如果没有设置IPC_CREAT标志并且信号量不存在,就会返错误(errno的值为2,No such file or directory)。
对于semget的有一点需要注意,每次获取信号量都需要先去判断它是否存在,不存在再去创建,否则可能会重复创建出现错误或者无法上锁
semctl函数
该函数用来控制信号量(常用于设置信号量的初始值和销毁信号量)
int semctl(int semid, int sem_num, int command, ...);
-
参数
semid是由semget函数返回的信号量标识 -
参数
sem_num是信号量集数组上的下标,表示某一个信号量,填0,和数组下标引用一样 -
参数
cmd是对信号量操作的命令种类,常用的有以下两个-
IPC_RMID:销毁信号量,不需要第四个参数;
-
SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定
-
-
第四参数是一个自定义的共同体
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
销毁信号量
semctl(semid,0,IPC_RMID);
初始化信号量值
union semun sem_union;
sem_union.val = 1;
semctl(semid,0,SETVAL,sem_union);
semop函数
该函数时信号量的操作函数,可以进行信号量的等待或者释放操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
-
参数semid是由semget函数返回的信号量标识。
-
参数nsops是操作信号量的个数,即sops结构变量的个数,设置它的为1(只对一个信号量的操作)。
-
参数sops是一个结构体
struct sembuf
{
short sem_num; // 信号量集的个数,单个信号量设置为0。
short sem_op; // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
short sem_flg; // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
// 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
};
等待信号量
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
释放信号量
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
使用示例
#ifndef _MYSEM_H
#define _MYSEM_H
#include <sys/ipc.h>
class mySem
{
public:
int init(key_t key);
int destroy();
int wait();
int post();
private:
int semId;
union semun // 用于信号灯操作的共同体。
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
};
#endif // !_MYSEM_H
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include "mySem.h"
int mySem::init(key_t key)
{
//先获取
semId = semget(key,1,0640);
//如果不存在就创建
if(semId == -1)
{
if(errno == 2)
{
semId = semget(key,1,0640 | IPC_CREAT);
if(semId == -1)
{
perror("init 1 semget():");
return -1;
}
semun sem_union;
sem_union.val = 1;
if(semctl(semId,0,SETVAL,sem_union) < 0)//设置信号量的初始值
{
perror("init semctl():");
return -1;
}
}
else
{
perror("init 2 semget():");
return -1;
}
}
return 0;
}
int mySem::destroy()
{
//销毁信号量
if(semctl(semId,0,IPC_RMID) == -1)
{
perror("destroy semctl():");
return -1;
}
return 0;
}
int mySem::wait()
{
//先构造sembuf结构体填充内容
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; //获取
sem_b.sem_flg = SEM_UNDO;
if(semop(semId,&sem_b,1) == -1)
{
perror("wait semop():");
return -1;
}
return 0;
}
int mySem::post()
{
//先构造sembuf结构体填充内容
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; //释放
sem_b.sem_flg = SEM_UNDO;
if(semop(semId,&sem_b,1) == -1)
{
perror("wait semop():");
return -1;
}
return 0;
}
int main(int argc,char **argv)
{
int pid = getpid();
printf("PID : %d\r\n",pid);
mySem sem;
if(sem.init(0x1234) != 0)
{
printf("pid%d :sem init failed\r\n",getpid());
return -1;
}
printf("pid%d :sem init succ\r\n",getpid());
if(sem.wait() != 0)
{
printf("pid%d :sem wait failed\r\n",getpid());
return -1;
}
printf("pid%d :sem wait succ\r\n",getpid());
sleep(20);
if(sem.post() != 0)
{
printf("pid%d :sem post failed\r\n",getpid());
return -1;
}
printf("pid%d :sem post succ\r\n",getpid());
return 0;
}
这里我封装了一个mySem的类,方便进行信号量的初始化以及获取,释放锁操作,调试命令可以使用ipcs -s
ubuntu@ubuntu-2204:~/souceCode/semCode$ ./mySem &
[1] 4173315
ubuntu@ubuntu-2204:~/souceCode/semCode$ PID : 4173315
pid4173315 :sem init succ
pid4173315 :sem wait succ
ubuntu@ubuntu-2204:~/souceCode/semCode$ ./mySem &
[2] 4173317
ubuntu@ubuntu-2204:~/souceCode/semCode$ PID : 4173317
pid4173317 :sem init succ
ubuntu@ubuntu-2204:~/souceCode/semCode$ pid4173315 :sem post succ
pid4173317 :sem wait succ
[1]- Done ./mySem
ubuntu@ubuntu-2204:~/souceCode/semCode$ ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x00001234 32806 ubuntu 640 1
ubuntu@ubuntu-2204:~/souceCode/semCode$ pid4173317 :sem post succ
[2]+ Done ./mySem
ubuntu@ubuntu-2204:~/souceCode/semCode$
POSIX
POSIX信号量是一个sem_t类型的变量,但POSIX有两种信号量的实现机制 :无名信号量 和命名信号量。 无名信号量只可以在共享内存的情况下,比如实现进程中各个线程之间的互斥和同步,因此无名信号量也被称作基于内存的信号量;命名信号量通常用于不共享内存的情况下,比如进程间通信。
sem_wait
sem_wait是一个通用的接口,无论是无名信号量和命名信号量都可以使用
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
-
sem_wait的作用是,若sem小于 0 ,则线程阻塞于信号量sem,直到sem大于 0 ;否则信号量值减1。 -
sem_trywait作用与sem_wait相同,只是此函数不阻塞线程,如果sem小于 0,直接返回一个错误(错误设置为EAGAIN)。 -
sem_timedwait作用也与sem_wait相同,第二个参数表示阻塞时间,如果sem小于 0 ,则会阻塞,参数指定阻塞时间长度。abs_timeout指向一个结构体,这个结构体由从1970-01-01 00:00:00 +0000 (UTC)开始的秒数和纳秒数构成。
sem_post
sem_post是一个通用的接口,无论是无名信号量和命名信号量都可以使用
#include <semaphore.h>
int sem_post(sem_t *sem);
- 若成功,返回 0 ;若出错,返回-1
无名信号量
sem_init
该函数用于创建信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数
sem指向要初始化的信号量 - 参数
value为信号量的初始值 - 参数
pshared用于说明信 号量的共享范围,如果pshared为0,那么该信号量只能由初始化这个信号量的进程中的线程使用 ,如果pshared非零,任何可以访问到这个信号量的进程都可以使用这个信号量 - 如果成功,
sem_init返回0,如果不成功,sem_init返回-1并设置errno
sem_destroy
int sem_destroy(sem_t *sem);
- 参数
sem为指向要销毁的信号量的指针 - 如果成功,
sem_destroy返回 0,如果不成功,sem_destroy返回-1 并设置errno
有名信号量
sem_open
该函数用于创建或打开一个命名信号量,和共享内存的shm_open类似
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
-
name是一个标识信号量的字符串。 -
oflag:用来确定是创建信号量还是连接已有的信号量。oflag的参数可以为0,O_CREAT或O_EXCL:如果为0,表示打开一个已存在的信号量;如果为
O_CREAT,表示如果信号量不存在就创建一个信号量,如果存在则打开被返回,此时mode和value都需要指定;如果为
O_CREAT|O_EXCL,表示如果信号量存在则返回错误。 -
mode用于创建信号量时指定信号量的权限位 ,和open函数一样,包括:S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。 -
value 表示创建信号量时,信号量的初始值。
sem_close
该函数用于关闭命名信号量
int sem_close(sem_t *sem);
单个程序可以用sem_close函数关闭命名信号量,但是这样做并不能将信号量从系统中删除,因为命名信号量在单个程序执行之外是具有持久性的
sem_unlink
sem_unlink函数用于在所有进程关闭了命名信号量之后,将信号量从系统中删除
int sem_unlink(const char *name);
无名信号量使用示例
这里给出一个无名信号量的使用示例
#include <semaphore.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <unistd.h>
#define SHM_NAME "/wjbtest"
int main(int argc,char **argv)
{
int fd = 0;
int ret = 0;
printf("PID : %d\r\n",getpid());
fd = shm_open(SHM_NAME,O_CREAT | O_RDWR,0640);
if(ftruncate(fd,sizeof(sem_t)) != 0)
{
perror("ftruncate failed:");
//解除共享内存空间
shm_unlink(SHM_NAME);
return -1;
}
//获取共享内存文件相关属性信息,这里获取的是文件大小,查看是否是设置的大小
struct stat filestat;
fstat(fd, &filestat);
printf("shm st_size :%ld\n",filestat.st_size);
sem_t *sem_ptr = (sem_t *)mmap(NULL,sizeof(sem_t),PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(sem_ptr == NULL)
{
perror("mmap failed :");
return -1;
}
printf("sem_ptr : %x\r\n",sem_ptr);
close(fd);
//进行信号量的初始化,多个进程只进行一次初始化!!
ret = sem_init(sem_ptr,1,1);
if(ret != 0)
{
perror("sem_init failed :");
return -1;
}
printf("sem init succ\r\n");
sem_wait(sem_ptr);
printf("sem wait succ\r\n");
sleep(180);
sem_post(sem_ptr);
printf("sem post succ\r\n");
return 0;
}
现象如下:
root@test:/home/jhliu/OS_code/semCode/unnamedSem# ./mySem &
[1] 3935725
root@test:/home/jhliu/OS_code/semCode/unnamedSem# PID : 3935725
shm st_size :32
sem_ptr : b981e000sem init succ
sem wait succ
root@test:/home/jhliu/OS_code/semCode/unnamedSem# ./semwait
PID : 3935778
shm st_size :32
sem_ptr : 7a3e3000
sem post succ
sem wait succ
sem post succ
[1]+ Done ./mySem
这里很需要注意的一点是无名信号量的初始化只要一次就好,多进程共享的情况下不要重复初始化 !笔者这里就中招了发现第二个进程不能被锁住直接调用wait成功
对于sem_destroy()函数,它的实际行为是:销毁该 sem_t 对象内部的同步状态与资源,使其立即变为无效对象 。调用后,其他进程/线程若继续使用该信号量,将直接触发 未定义行为。
有名信号量使用示例
逻辑和上述一致
#include <semaphore.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <unistd.h>
int main(int argc,char **argv)
{
printf("PID : %d\r\n",getpid());
sem_t *sem_ptr =sem_open("/wjbTest",O_CREAT,0640,1);
if(sem_ptr == NULL)
{
perror("sem_open:");
return -1;
}
sem_wait(sem_ptr);
printf("PID%d :sem wait succ\r\n",getpid());
sleep(10);
sem_post(sem_ptr);
printf("PID%d :sem post succ\r\n",getpid());
return 0;
}
现象:
jhliu@test:~/OS_code/semCode/namedSem$ ./mySem &
[1] 3953225
jhliu@test:~/OS_code/semCode/namedSem$ PID : 3953225
PID3953225 :sem wait succ
jhliu@test:~/OS_code/semCode/namedSem$ ./mySem &
[2] 3953250
jhliu@test:~/OS_code/semCode/namedSem$ PID : 3953250
jhliu@test:~/OS_code/semCode/namedSem$ PID3953225 :sem post succ
PID3953250 :sem wait succ
PID3953250 :sem post succ
[1]- Done ./mySem
[2]+ Done ./mySem