Linux 系统下的进程间通信 IPC 入门 「中」

发布于:2024-04-19 ⋅ 阅读:(24) ⋅ 点赞:(0)

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「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;
}