Linux 多进程通信开发(五): 信号量

这会是一系列文章,讲解的内容也很简单,文章的目的是让自己的知识固话和文档化,以备自己不时的复习,同时也希望能够给予初学者一些帮助。

前面的文章有介绍了如何利用管道和消息队列进行进程间的通信,但是能够进行 IPC 的方式有很多种,最高效的是共享内存,比较常见的还有 socket,但是在介绍共享内存之前,先介绍本篇文章的主题,那就是信号量。

什么是信号量?

信号量不是传统意义的信号(signal),它的英文单词是(sempheroe),主要作用不是为了传递数据,而是为了协作几个进程之间的通信机制,所以很多人认为应该把它翻译成信号灯更好。

既然是信号灯,那么最重要的就是指示作用了.

怎么理解呢?

很好理解,我们生活中常见。

大家看看上面的这张图,信号量其实描述的就是这样一种场景。

节假日高峰期的时候,知名的餐饮店外面都需要排队等座位,因为店铺内部的座位有限,所以基本是出来几个,外面的再进去几个。

座位就是宝贵的资源,而信号量在软件开发中,目的也是为了保护好系统中重要的资源。

这个资源依情况而定,可以是一个变量,一段内存,或者是某个硬件设备。

举个例子,大家都知道火车票售卖,假设有 10 个进程或者线程执行售票的任务,那么火车票的数量就称为了一个特别珍贵的资源,要好好保护起来,同一时刻只能允许一个线程更改它的数量,不然就乱套了。

再举个大家都明白的道理,我们的手机,同一个时刻只能由 1 个 APP 进行操作相机,Camera 在这个时候就是很关键的一个资源,要好好保护起来。

为了能够有序地协调各个进程或线程操作,信号量实现了一种 PV 操作。

PV 操作

信号量这个量是数量的意思,意思是信号量是有值的。

当一个进程要访问某个重要资源时,首先它的查询信号量的值。

  • 大于 0,比如 3 ,表明可以接受 3 个不同的进程去操作资源,你可以想象饭店里面还有 3 个空位。
  • 等于 0,表明现在这个进程访问不了资源,因为里面没有空位了,并且进程将会挂起,好比餐厅里面座位满了,你要继续吃饭的话,就拿个号码等待。
  • 小于 0,绝对值等于等候的进程数量,比如 -5,意思是有 5 个进程在等待队列中,好比餐厅外面现在有 5 个人在排队。

具体而言,就是通过 P 和 V 两种操作。

P 和 V 都是出自荷兰语的单词。

P 的具体涵义很很多种说法,我个人倾向于 “proberen,try to reduce",也就是试探的意识,假设信号量取值为 S, P 的意图就是要减少 S 的值,直白点就是它想拿到信号量的许可去访问临界资源。比如餐厅还有 3 个位置,某个人进去之后,可用的位置就变成了 2 个。

V 的作用相反,它的目的是尝试增加 S 的值。比如餐厅的顾客吃完饭后,可用的座位又多了一个。

需要注意的是,一般我们开发信号量的值取 1、 0、 -1 这 3 个,代表处理两个进程同步。

了解了这种机制后,下面我们就可以开始详细讲解 linux 下信号量的相关 API 了。

信号集的的获取

和消息队列类似,linux 中用信号集管理信号量,也就是一个信号集里面可以有许多个信号量。

#include <sys/sem.h>

int semget (key_t key, int nsems, int semflg)
  • key 是由 ftok() 创建的描述符,不熟悉的同学可以阅读前面两篇文章。
  • nsems 信号量的数量,刚刚有讲到一个信号集合可以存放很多的信号量。
  • semflg 标志位,取值 IPC_CREAT 和 IPC_EXEL
  • IPC_CREAT 无信号集则创建新的,有的话返回现成的信号集 id。
  • IPC_EXCL 和 IPC_CREAT 配合使用,如果有已经存在的信号集的话,则返回 -1,错误码 EEXIST。

semget 返回的是 semid,如果异常的话返回 -1.

信号量的操作

前面讲过信号量主要实现 PV 操作,它们可以通过同一个函数 semop 进行。

#include<sys/sem.h>

int semop (int semid, struct sembuf *sops, size_t nsops)
  • semid 由 semget 返回的信号集的 id
  • sops 是一个结构体,里面包含了信号量的 index,信号量的值,操作标志
  • nsops 要操作的信号量个数

sops 是一个 sembuf 类型结构体,详细定义如下:

struct sembuf
{
  unsigned short int sem_num;	/* semaphore number */
  short int sem_op;		/* semaphore operation */
  short int sem_flg;		/* operation flag */
};
  • sem_num 是信号量在信号集中的序号
  • sem_op 代表操作的值,大于 0 说明是 V 操作,相当于释放资源。
  • sem_op 小于 0,说明是 P 操作,前面讲过 P 总是抱着要减少信号量的值 S 去的。
  • sem_op 如果等于 0,则一般情况下进程堵塞,知道信号量的值变为 0.
  • sem_flg 代表操作模式,如果设置为 IPC_NOWAIT 的话,sem_op 取值为 0 或者负数的时候会直接返回 EAGAIN 的错误。

好了代码示例,我们要进行一个 P 操作,要怎么弄呢?

void P(int semid)
{
    struct sembuf buf = {0,-1,0};

    semop(semid,&buf,1);
}

进行 V 操作,可以这样。

void V(int semid)
{
    struct sembuf buf = {0,1,0};

    semop(semid,&buf,1);
}

信号集的控制

一个信号集可以存放很多信号量,所以对于信号集本身也有需要操作要进行,比如添加信号量、删除信号量、初始化信号量。

它们都通过 semctl 这个函数实现。

int semctl (int semid, int semnum, int cmd, ...)

semctl 接受多个参数

  • semid 信号集的 id 号
  • semnum 要操作的信号的序号
  • cmd 操作命令
  • … 配合 cmd 的参数

cmd 有许多种操作

  • IPC_SET
  • IPC_RMID
  • GETPID
  • GETVAL
  • GETALL
  • GETNCNT
  • GETZCNT
  • SETVAL
  • SETALL

最常用的无疑是 GETVAL 命令,用来获取某个信号量的值。

另外 SETVAL 经常用来重置信号量的值。

至于 cmd 后面的 … 它是一个 union 类型。

union semun {
	int val;			/* value for SETVAL */
	struct semid_ds *buf;	/* buffer for IPC_STAT & IPC_SET */
	unsigned short *array;	/* array for GETALL & SETALL */
	struct seminfo *__buf;	/* buffer for IPC_INFO */
	void *__pad;
};

如果 cmd 取值 SETVAL,那么后面的参数就应该设置为 int 类型。

如果 cmd 取值为 GETVAL,返回值就是要获取的结果。

代码示例

假设有一个洗手间一次只能容纳一个人,那么其他人就需要排队了。

现在有 2 个人都想上洗手间,每个人用一个进程示意。

#include <sys/sem.h>
#include <iostream>
#include <unistd.h>

using namespace std;

void P(int semid)
{
    struct sembuf buf = {0,-1,0};

    semop(semid,&buf,1);
}

void V(int semid)
{
    struct sembuf buf = {0,1,0};

    semop(semid,&buf,1);
}

void use_toilet(int pid,int semid)
{
    cout << "I'm process: " << pid << " read for use toilet" << endl;
    P(semid);
    cout << "I'm process: " << pid << " using toilet" << endl;
    sleep(1);
    cout << "I'm process: " << pid << " i'm done" << endl;
    V(semid);
}


int main(int argc,char** argv)
{
    key_t keyid = ftok("testforsem",101);

    if (keyid < 0)
    {
        cerr << " get key failed" << endl;
        return -1;
    }

    int semid = semget(keyid,1,IPC_CREAT|0660);
    if (semid < 0)
    {
        cerr << "get semphore failed" << endl;
        return -1;
    }

    //设置信号量最大资源数为 1,只允许一个进程访问
    if (semctl(semid,0,SETVAL,1) < 0)
    {
        cerr << "Inital sem val failed" << endl;
        return -1;
    }

    pid_t pid = fork();

    switch (pid)
    {
        case -1:
            cerr << "Fork failed" << endl;
            break;

        case 0:
            use_toilet(getpid(),semid);
            break;

        default:
            use_toilet(getpid(),semid);
            sleep(3);
            break;
    }

    return 0;
}

代码比较简单,首先在当前目录我创建了一个 testforsem 的文件,为的就是提供给 ftok 使用。

touch testforsem

然后 fork 了一个进程。

子进程和父进程都使用同样的代码。

最后编译然后执行。

g++ testsem.cpp
./a.out

结果如下:

I'm process: 4136 read for use toilet
I'm process: 4136 using toilet
I'm process: 4137 read for use toilet
I'm process: 4136 i'm done
I'm process: 4137 using toilet
I'm process: 4137 i'm done

可以看到,两个进程实现了交替使用资源。

总结

  • 信号量本身没有数据传输能力,它不像管道和消息队列那样。
  • 信号量是进程或者线程对于共享资源的有效协作机制。
  • 信号量需要各个进程遵守规则才能运行,如果绕开 PV 操作,直接操控信号量的值,会打乱原有的秩序。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券