在Linux中,信号量是一种用于进程间同步和互斥的机制。它可以用于控制对共享资源的访问,以避免多个进程同时访问造成的竞争条件。
Linux提供了两种类型的信号量:System V信号量和POSIX信号量。
System V信号量是通过系统调用(如semget、semop和semctl)来操作的。它们是用于进程间同步和互斥的基本机制,并且可以在不同进程之间共享。
POSIX信号量是通过库函数(如sem_init、sem_wait和sem_post)来操作的。它们是Linux提供的一种更现代、更灵活的信号量机制,也可以在不同进程之间共享。
无论是System V信号量还是POSIX信号量,它们都可以用于实现各种同步和互斥机制,如互斥锁、条件变量和读写锁等。
System V信号量
semget
:创建或获取信号量集。每个信号量集包含多个信号量,每个信号量都有一个唯一的标识符。semop
:用于进行信号量操作,包括P操作(等待信号量)和V操作(释放信号量)。P操作将信号量的值减一,如果它的值小于零,进程将被阻塞。V操作将信号量的值加一,唤醒等待的进程。semctl
:用于进行信号量的控制操作,如获取、设置信号量的值,删除信号量集等。
semget
int senget(key_t key, int nsems, int semfalg);
- key:是一个键值,用来标识信号量集。多个进程可以使用相同的key来获取同一个信号量集。
- nsems:表示信号量集中包含的信号量数量。
- semflag:指定创建或获取信号量集的标志。可以使用IPC_CREAT来创建新的信号量集,与IPC_EXCL 一起使用来确保创建唯一的信号量集,也可以与其他标志用于权限设置(0666)。*
返回值
- 成功返回一个信号量集的标识符。
- 失败返回-1,并设置errno。
示例:
################### 创建一个唯一的信号量 #######################
key_t key = ftok("/usr/bin", 255);
//创建一个信号量集,如果信号量已经存在 返回错误。
m_semId = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
################### 打开一个已经存在的信号量 #######################
m_semId = semget(key, 0, 0);
参数 nsems, semfalg 都将被忽略。
semctl
int semctl(int semid, int semnum, int cmd, union semnu arg);
union semnu
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
参数:
-
semid:信号量集的标识符,由 semget 函数创建或获取。
-
semnum:表示要操作的具体信号量集中的索引,通常为0表示第一个信号量。
-
cmd:表示要执行的操作,可以是以下之一:
- GETVAL:获取信号量的当前值。
- SETVAL:设置信号量的值。
- GETPID:获取上一次执行semop操作的进程ID
- GETNCNT:获取等地啊操作的进程数量。
- GETZCNT:获取等待清零操作的进程数量。
- SETALL:用于设置一组信号量的值,通常与arg参数中提供的数组一起使用。
- IPC_STAT:用于获取信号量的信息结果。
- IPC_SET:用于修改信号量的信息结构,通常与arg参数中提供的sembuf结构一起使用
- IPC_RMID:从系统中删除信号量集。
- 等等。
-
arg:一个联合体(union semun),用于传递操作所需的参数,具体内容取决于cmd的值。
返回值:
- 失败时返回-1,并设置errno。成功时 cmd标黄的 返回对应的值(非负值),其余返回0。
semop
它允许进程对信号量就行P(等待)、V(释放)操作,以实现进程之间的同步和互斥。
int semop(int semid, struct sembug* sops, size_t nsops);
// sembug 的定义
struct sembuf {
short sem_num; // 信号量在信号量集中的索引,从0开始
short sem_op; // 操作,通常为正数表示V操作,负数表示P操作
short sem_flg; // 操作标志,通常为0,取值有 SEM_UNDO | IPC_NOWAIT
};
如果sem_op > 0,信号量的值会加上sem_op的值,表示进程释放控制的资源。
如果sem_op = 0,如果没有设置IPC_NOWAIT,则调用进程进入睡眠状态,直到信号量的值为0;否则进程不会睡眠,直接返回EAGAIN。
如果sem_op < 0,信号量的值会加上sem_op的值。如果没有设置IPC_NOWAIT,则调用进程会阻塞,直到资源可用;否则进程直接返回EAGAIN。
参数:
- semid:信号量集的标识符,由semget函数创建或获取。
- sops:指向sembuf结构数组的指针,该结构包含要执行的操作和信号量索引。
- SEM_UNDO:用于启用进程终止时的自动撤销操作。如果设置了此标志,并且进程在执行 semop 操作之后非正常退出,内核将自动执行撤销操作,将信号量的值还原为操作之前的状态,以避免资源泄漏。这通常用于防止由于进程异常退出而导致的资源泄漏。
- IPC_NOWAIT:用于非阻塞操作。如果设置了此标志,并且 semop 操作不能立即执行(例如,由于信号量值不允许),则该操作不会等待,而是立即返回错误。
- nsops:表示sops数组中操作的数量。
示例:
################### 执行P操作(等待信号量) ######################
struct sembuf operation;
operation.sem_num = 0; // 操作的信号量索引
operation.sem_op = -1; // 执行P操作
operation.sem_flg = SEM_UNDO;
semop(semid, &operation, 1);
################### 执行V操作(释放信号量) ######################
struct sembuf operation;
operation.sem_num = 0; // 操作的信号量索引
operation.sem_op = 1; // 执行V操作
operation.sem_flg = SEM_UNDO; //程序异常退出时会对该操作进行撤销,该程序中所有设置了该标志的操作都将撤销。
semop(semid, &operation, 1);
sops参数中包含的操作集 按数组顺序自动执行,也就是说,这些操作要么作为一个完整的单元执行,要么根本不执行。如果不能立即执行所有操作,则系统调用的行为取决于各个sem_flg字段中是否存在IPC IPC_NOWAIT标志。
# 两个信号量的初始化均设置为1
struct sembuf sops[2] = {0};
sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT;
sops[0].sem_num = 0;
sops[0].sem_op = 0;
sops[1].sem_flg = SEM_UNDO;
sops[1].sem_num = 1;
sops[1].sem_op = -1;
int rv = semop(m_semId, &sops[0], 2);
if (0 != rv)
{
printf("Lock error, errno = %d, errmsg = %s\n", errno, strerror(errno));
return rv;
}
上面这个示例中同时操作信号量集中的两个信号量, 但是第一个信号量不能立即执行但是因为又设置了 IPC_NOWAIT属性, 所以此时semop失败,errno设置为EAGAIN(并且没有执行sops中的任何操作)。
注意事项
- 创建好的信号量集在程序结束时需要手动进行释放,如果没有释放即使程序在退出时也不会被系统回收。(可以使用ipcrm 命令删除信号量)。
- 多个进程使用同一个信号量集时,如果有一个进程对该信号量集进行了释放,那么其他的进程在对该信号量集操作时将会失败。