在Linux中支持System V进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。下面我们阐述一下信号量的进程间通信方式以及工作原理。
IPC的一点补充
Linux中的内存空间分为系统空间和用户空间。
在系统空间中:由于各个线程的地址空间是共享的,即一个线程可以随意访问 kernel 中任意地址,所以无需进程通信机制的保护。
在用户空间中:每个进程都有自己的地址空间,一个进程要和另外一个进程通信,必须有足够的权限能够访问其他进程的 kernel ,从而与其他进程通信。
信号量的引入原因?
为了防止多个进程同时访问一块资源而引发的一系列问题,我们需要一种可以通过它生成并使用令牌来授权,在任意时刻只能有一个执行线程访问代码的临界区。而信号量则提供了这样的机制让一个临界区同一时间只有一个线程在访问,即信号量是用来协调进程对资源的访问的。
当然在我们开始讲信号量之前,我们需要对几个重要的概念有一个初步的认识:
临界资源:一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
临界区:临界区内的数据一次只能同时被一个进程使用,当一个进程使用临界区内的数据时,其他需要使用临界区数据的进程进入等待状态。
互斥:指某一资源同时只允许一个访问者对其进行访问。
原子性:一个事务包含多个操作,这些操作要么全部执行,要么都不执行。
同步:基本都是以互斥为条件,让不同的进程访问临界资源,以某种特定的顺序去访问。
信号量的本质
信号量在本质上是一种数据操作锁(计数器,记录统计临界资源的数目)。它本身不具备数据交换的功能,而是通过保护其他的通信(文件、外部设备)等临界资源来实现进程间通信。信号量在此过程中负责数据操作的互斥、同步等功能。
当请求一个使用信号量来表示的临界资源时,进程需要先读取信号量的值来判断资源是否可用:
(1)信号量 > 0:表示有资源可用。
(2)信号量 = 0:表示无资源可用,进程会进入睡眠状态直至资源可用。
当进程不再使用一个信号量控制的共享资源时,信号量+1,对信号量的值进行增加操作均为原子操作(原因在于:信号量的主要作用是维护资源的互斥或多个进程的同步访问)。而在信号量的创建及初始化上,不能保证操作均为原子的。
生命周期:信号量的生命周期并不随进程的结束而结束,而是随内核的。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和 V(sv),sv为信号量,它们的行为如下:
P(sv):如果sv的值大于0,就对其减1;如果它的值为0,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有进程因等待sv而被挂起,就给它加1。
举例说明:两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1;而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv这时候为0,它就会被挂起以等待第一个进程离开临界区并执行V(sv)释放信号量后,这时候第二个进程才可以恢复执行。
二元信号量:是最简单的一种锁,它只有两种状态(占用和非占用)。它适合只能被唯一一个线程访问的资源。当个人员信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得锁,并将二元信号量置为占用状态,这时其他的所有试图获取该二元信号量的线程将会被等待,直至该锁被释放。
Linux信号量机制
Linux中提供了一组精心设计的信号量接口来对信号量进行操作,它们不只是针对二进制信号量,但是这些函数都是用来对成组的信号量进程操作的,它们的声明在 sys/sem.h 中。
在sem_structure中也有关于struct ipc_perm的结构体成员,这说明信号量同样是IPC的进程通信方式之一。虽然信号量本质上并不能进行数据交换,但是其负责数据操作的互斥及同步的功能。
(1)在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建信号量时,要指定该集合中信号量值的数量。
(2)创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量赋初值。
(3)即使没有进程在使用IPC资源,它们任然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以用关键字(SEM_UNDO)在退出时恢复信号量值为初始值。
信号量相关接口函数
1.ftok函数:把一个已经存在的路径名和一个整数标识得转换成一个key_t值,称为IPC键:
key_t ftok(const char *pathname, int proj_id);
参数[pathname]:通常是跟本应用有关的目录。
参数[proj_id]:指的是本应用所用到的IPC的一个序列号,成功返回IPC键,失败返回-1。
[返回值]:成功返回键值,失败返回-1。
注:两进程如在pathname和proj_id上达成一致(或约定好),双方就都能够通过调用ftok函数得到同一个IPC键。
pathname的实现是组合了三个键,分别是:
(1)pathname所在文件系统的信息(stat结构的st_dev成员)。
(2)pathname在文件系统内的索引节点号(stat结构的st_ino成员)。
(3)id的低序8位(不能为0)。
ftok调用返回的整数IPC键由proj_id的低序8位,st_dev成员的低序8位,st_info的低序16位组合而成。
不能保证两个不同的路径名与同一个proj_id的组合产生不同的键,因为上面所列的三个条目(文件系统、标识符、索引节点、proj_id)中的信息位数可能大于一个整数的信息位数。
2.semget函数:创建一个信号量或访问一个已经存在的信号量集。
int semget(key_t key, int nsems, int semflg);
参数[key]:类似于端口号,也可以由ftok函数生成。
参数[nsems]:在System V中,申请信号量是以信号量集nsems去申请,而不是一个一个去申请,底层是一个数组。
参数[semflg]:IPC_CREAT或IPC_EXCL
[返回值]:是一个称为信号量标识符的整数,semop 和 semctl 函数将使用它。
3.semop函数:用来创建和访问一个信号量集。
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数[semid]:是该信号量的标识码,也就是semget函数的返回值。
参数[sops]:是个指向一个结构数值的指针。
参数[nsops]:信号量操作结构的个数,恒大于等于1。
[返回值]:成功返回0,失败返回-1。
信号量操作由sembuf结构表示:
struct sembuf{ short sem_num; // 在信号集中的编码0,1,2... short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数, // 一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作 short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时, 操作系统释放信号量 };
参数nsops规定sops数组元素的个数:sem_op的取值如下:
(1)若sem_op为正(V操作),这对应于进程释放占用的资源数。sem_op值加到信号量的值上去。
(2)若sem_op为负(P操作),这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。
(3)若sem_op为0,这表示调用进程希望等待到信号量值变为0。
4.semctl函数:初始化或移除信号量集。
int semctl(int semid, int semnum, int cmd, ...);//可变参数列表
参数[semid]:信号量集IPC标识符。
参数[semnum]:表示信号量集中的哪个信号量,第一个为0。
参数[cmd]:在semid指定的信号量集上指向此命令,cmd的选择如下:
//10 cmd IPC_STAT 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。 IPC_SET 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。 IPC_RMID 将信号量集从内存中删除。 GETALL 用于读取信号量集中的所有信号量的值。 GETNCNT 返回正在等待资源的进程数目。 GETPID 返回最后一个执行semop操作的进程的PID。 GETVAL 返回信号量集中的一个单个的信号量的值。 GETZCNT 返回这在等待完全空闲的资源的进程数目。 SETALL 设置信号量集中的所有的信号量的值。 SETVAL 设置信号量集中的一个单独的信号量的值。
参数[可变参数列表]:是可选的,取决于第三个参数cmd。
[返回值]:成功返回正数,失败返回-1。
5.查看信号量:
ipcs -s
6.删除信号量:
ipcrm -s [semid]
通过代码模拟父子进程的互斥
Makefile
//Makefile sem:comm.c sem.c gcc -o $@ $^ .PHONY:clean clean: rm sem
comm.h
//comm.h #include<sys/wait.h> #include<sys/ipc.h> #include<sys/stat.h> #include<sys/sem.h> #define PROJ_ID 0x6666 #define PATHNAME "." union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *_buf; }; int createSemSet(int nums); int getSemSet(); int destroySems(int semid); int initSems(int semid,int who,int value); int P(int semid,int who); int V(int semid,int who); #endif
comm.c
//comm.c #include"comm.h" static int commSemSet(int nums,int flags) { key_t key = ftok(PATHNAME,PROJ_ID); if(key < 0) { perror("ftok"); return -1; } int semid = semget(key,nums,flags); if(semid < 0) { perror("semget"); return -2; } return semid; } int createSemSet(int nums) { return commSemSet(nums,IPC_CREAT | IPC_EXCL | 0666); } int initSems(int semid,int who,int initval) { union semun _un; _un.val = initval; if(semctl(semid,who,SETVAL,_un) < 0) { perror("semctl"); return -1; } return 0; } static int commPV(int semid,int who,int op) { struct sembuf _sf; _sf.sem_num = who; _sf.sem_op = op; _sf.sem_flg = 0; return semop(semid,&_sf,1); } int P(int semid,int who) { return commPV(semid,who,-1); } int V(int semid,int who) { return commPV(semid,who,1); } int getSemSet() { return commSemSet(0,IPC_CREAT); } int destroySems(int semid) { if(semctl(semid,0,IPC_RMID,NULL) < 0) { perror("semctl"); return -1; } return 0; }
sem.c
//sem.c #include"comm.h" int main() { int semid = createSemSet(1); initSems(semid,0,1); pid_t id = fork(); if(id == 0)//child { int semid = getSemSet(); while(1) { P(semid,0); printf("A"); fflush(stdout); usleep(300000); printf("A"); fflush(stdout); usleep(300000); V(semid,0); } } else//father { while(1) { P(semid,0); printf("B"); fflush(stdout); usleep(400000); printf("B"); fflush(stdout); usleep(400000); V(semid,0); } wait(NULL); } destroySemSet(semid); return 0; }