以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/39XQUQtGC3Ow-0s0JKWnog
信号量
信号量一般用于配合共享内存的数据传输,共享内存被多个进程之间共享访问,各个进程对共享内存的访问必须被同步才安全和有效。
申请信号量资源时,返回的是一组信号量集合,包含多个信号量,信号量 id 对应的是信号量集合,而不是单个信号量,每个信号量可以分别控制各种同步。信号量通过序号指定,序号从 0 开始。
系统范围内,可以申请的最多信号量集合数为 32000 个,每个信号量集合最多包含的信号量为 32000 个,每个信号量调用的最大操作数是 500,信号量值最大可达 32767。如果需要查看限制值,可以:
$ cat /proc/sys/kernel/sem
通过 semget() 申请信号量集合,参数 key 指定 IPC key,参数 nsems 指定包含的信号量数量。参数 semflg = IPC_CREAT | IPC_EXCL
指定尝试创建信号量,如果已经存在则返回失败,失败后可重新尝试获取。
int semid = semget(key, nsems, IPC_CREAT | IPC_EXCL | 0666);
if (-1 == semid) { // failure
semid = semget(key, nsems, 0666);
if (-1 == semid) {
printf("semget %s", strerror(errno));
}
}
信号量通过请求(抢占)和释放来控制进程间同步,本质上就是满足一定条件下对信号量值的加减。每个信号量有一些关联的变量,比如信号量值 semval。
请求信号量
/**
* @brief 请求信号量
* @param id 信号量集合 id
* @param semindex 信号量序号,从 0 开始
* @param val 信号量值偏移绝对值,一般为 1
*/
void sem_set_wait(int id, int semindex, short val)
{
int ret;
struct sembuf buf;
buf.sem_num = semindex;
// P(sv) 减1 获取信号量
buf.sem_op = (-1) * val;
buf.sem_flg = 0;
while ((ret = semop(id, &buf, 1))
&& (errno == EINTR));
if (ret == -1) {
perror("semop");
}
}
请求信号量,也就是常说的 P 操作,实际是对信号量值做减法操作,如果当前信号量值比减数的绝对值小,那么一般情况下会阻塞当前线程。是否阻塞可以通过设置标志 sembuf.sem_flg 来决定。
释放信号量
/**
* @brief 释放信号量
* @param id 信号量集合 id
* @param semindex 信号量序号,从 0 开始
* @param val 信号量值偏移绝对值,一般为 1
*/
void sem_set_signal(int id, int semindex, short val)
{
struct sembuf buf;
buf.sem_num = semindex;
// V(sv) 加1 释放信号量
buf.sem_op = val;
buf.sem_flg = 0;
if (semop(id, &buf, 1) == -1) {
perror("semop");
}
}
释放信号量,也就是常说的 V 操作,实际是对信号量值做加法操作,不会阻塞当前线程。
最佳实践
如果有两个数据生产消费端,一个生产端和一个消费端,为了实现先生产再消费,消费完再生产,以及以此类推的同步流程,那么可以从信号量集合中使用其中的两个信号量,比如信号量 0 和信号量 1。
生产端:
// 请求信号量 0
sem_set_wait(semid, 0, 1);
// 生产 ...
// 释放信号量 1
sem_set_signal(semid, 1, 1);
消费端:
// 请求信号量 1
sem_set_wait(semid, 1, 1);
// 消费 ...
// 释放信号量 0
sem_set_signal(semid, 0, 1);
请求信号量 0,那么信号量 0 应该先被释放,信号量 1 同理。生产端生产前需要请求信号量 0,消费端消费前需要请求信号量 1,生产完成后释放信号量 1,消费完成后释放信号量 0。意味着生产前必须先消费,消费前必须先生产。
这样子相互依赖的逻辑决定了必须另一端执行完毕,当前端才可以开始执行。
既然是相互依赖,就必须有切入点,那么实际运行开始时,需要先允许生产端请求到信号量,可以设置信号量 0 的值为 1,这样生产端就可以顺利请求信号量 0 并开始生产,生产完成后再释放信号量 1,接着消费端才能够顺利请求到信号量 1。
设置信号量的值需要用到 semctl():
#include <sys/sem.h>
union semun {
/* Value for SETVAL */
int val;
/* Buffer for IPC_STAT, IPC_SET */
struct semid_ds *buf;
/* Array for GETALL, SETALL */
unsigned short *array;
/* Buffer for IPC_INFO (Linux-specific) */
struct seminfo *__buf;
};
int semctl(int semid, int semnum, int cmd, ...);
semctl() 有 4 个参数,semid 指定信号量集合 id,semnum 指定信号量集合中的信号量序号,cmd 指定对信号量的操作命令,最后一个参数可选。
semctl() 可用于信号量的各种设置,当 cmd = SETVAL 时,设置共用体 semun 类型的 val 成员为信号量的目标值,然后值传递 semun 类型变量给 semctl() 最后一个参数,代码如下
/**
* @brief 初始化信号量值
* @param id 信号量集合 id
* @param semindex 信号量序号,从 0 开始
* @param val 信号量初始值
* @return int 0:成功,-1:失败
*/
int sem_init(int id, int semindex, int val)
{
union semun su;
su.val = val;
if (semctl(id, semindex, SETVAL, su) == -1) {
perror("semctl");
return -1;
}
return 0;
}