这会是一系列文章,讲解的内容也很简单,文章的目的是让自己的知识固话和文档化,以备自己不时的复习,同时也希望能够给予初学者一些帮助。
前面的文章有介绍了如何利用管道和消息队列进行进程间的通信,但是能够进行 IPC 的方式有很多种,最高效的是共享内存,比较常见的还有 socket,但是在介绍共享内存之前,先介绍本篇文章的主题,那就是信号量。
信号量不是传统意义的信号(signal),它的英文单词是(sempheroe),主要作用不是为了传递数据,而是为了协作几个进程之间的通信机制,所以很多人认为应该把它翻译成信号灯更好。
既然是信号灯,那么最重要的就是指示作用了.
怎么理解呢?
很好理解,我们生活中常见。
大家看看上面的这张图,信号量其实描述的就是这样一种场景。
节假日高峰期的时候,知名的餐饮店外面都需要排队等座位,因为店铺内部的座位有限,所以基本是出来几个,外面的再进去几个。
座位就是宝贵的资源,而信号量在软件开发中,目的也是为了保护好系统中重要的资源。
这个资源依情况而定,可以是一个变量,一段内存,或者是某个硬件设备。
举个例子,大家都知道火车票售卖,假设有 10 个进程或者线程执行售票的任务,那么火车票的数量就称为了一个特别珍贵的资源,要好好保护起来,同一时刻只能允许一个线程更改它的数量,不然就乱套了。
再举个大家都明白的道理,我们的手机,同一个时刻只能由 1 个 APP 进行操作相机,Camera 在这个时候就是很关键的一个资源,要好好保护起来。
为了能够有序地协调各个进程或线程操作,信号量实现了一种 PV 操作。
信号量这个量是数量的意思,意思是信号量是有值的。
当一个进程要访问某个重要资源时,首先它的查询信号量的值。
具体而言,就是通过 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)
semget 返回的是 semid,如果异常的话返回 -1.
前面讲过信号量主要实现 PV 操作,它们可以通过同一个函数 semop 进行。
#include<sys/sem.h> int semop (int semid, struct sembuf *sops, size_t nsops)
sops 是一个 sembuf 类型结构体,详细定义如下:
struct sembuf { unsigned short int sem_num; /* semaphore number */ short int sem_op; /* semaphore operation */ short int sem_flg; /* operation flag */ };
好了代码示例,我们要进行一个 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 接受多个参数
cmd 有许多种操作
最常用的无疑是 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
可以看到,两个进程实现了交替使用资源。
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句