在Linux中支持System V进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。今天就承接消息队列的余热来阐述一下信号量的进程间通信方式及工作原理。
IPC的一点补充
Linux中的内存空间分为系统空间和用户空间。
在系统空间中:由于各个线程的地址空间是共享的,即一个线程可以随意访问 kernel中的任意地址,所以无需进程通信机制的保护。
而在用户空间中:每个进程都有自己的地址空间,一个进程要和另外一个进程通信,必须陷入到有足够权限访问其他进程的kernel中,从而与其他进程通信。
信号量的引入原因?
为了防止多个进程同时访问一块各项资源而引发的一系列问题,我们需要一种可以通过它生成并使用令牌来授权,在任意时刻只能有一个执行线程访问代码的临界区。而信号量则提供了这样的机制让一个临界区同一时间只有一个线程在访问,即信号量是用来协调进程对资源的访问的。
当然在我们开始讲信号量之前,我们需要对几个重要的概念有一个初步的认识:
临界资源:一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
临界区:临界区内的数据一次只能同时被一个进程使用,当一个进程使用临界区内的数据时,其他需要使用临界区数据的进程进入等待状态。
互斥:指某一资源同时只允许一个访问者对其进行访问。
原子性:一个事务包含多个操作,这些操作要么全部执行,要么都不执行。
同步: 基本上都是以互斥为条件,让不同的进程访问临界资源,以某种特定的顺序去访问。
信号量的本质
信号量在本质上是一种数据操作锁(计数器,记录统计临界资源的数目)。它本身不具备数据交换的功能,而是通过保护其他的通信(文件、外部设备)等临界资源来实现进程间通信。信号量在此过程中负责数据操作的互斥、同步等功能。
当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用:
①信号量 > 0:表示资源可用请求。
②信号量 = 0:无资源可用,进程会进入睡眠状态直至资源可用。
当进程不再使用一个信号量控制的共享资源时,信号量+1,对信号量的值进行增加操作均为原子操作(原因在于:信号量的主要作用是维护资源的互斥或多进程的同步访问)。而在信号量的创建及初始化上,不能保证操作均为原子性。
[生命周期]:信号量的生命周期与消息队列一样,不随进程的结束而结束,而是随内核的。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),sv为信号量,它们的行为如下:
P(sv):如果sv的值大于零,就对其减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有进程因等待sv而挂起,就给它加1。
[举例说明]:两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当他试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区并执行V(sv)是释放信号量,这时第二个进程可以恢复执行。
[二元信号量]:是最简单的一种锁,它只用两种状态(占有与非占用)。它适合只能被唯一一个线程访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放。
Linux的信号量机制
Linux中提供了一组精心设计的信号量接口来对信号量进行操作,它们不只是针对二进制信号量,但是这些函数都是用来对成组的信号量进行操作的,它们的声明存在于sys/sem.h中。
1 2 3 4 5 6 7 |
The semid_ds struct shall contain the following members: struct ipc_perm sem_perm Operation permission structure. unsigned short sem_nsems Number of semaphores in set. time_t sem_otime Last semop. ()time. time_t set_ctime Last time changed by semctl. (). |
在sem_structure中也有关于struct ipc_perm的结构体成员,这说明信号量同样是IPC的进程通信方式之一。虽然信号量本质上并不能数据交换,但是其负责数据操作的互斥及同步的功能。
① 在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。
② 创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。
③ 即使没有进程在使用IPC资源,它们仍然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。
信号量相关接口函数
1、ftok函数:把一个已经存在的路径名和一个整数标识得转换成一个key_t值,称为IPC键:
1 2 3 4 5 6 |
NAME ftok - convert a pathname and a project identifier to a System V IPC key SYNOPSIS #include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); |
参数 [pathname]:通常是跟本应用用关的目录。
参数 [proj_id]:指的是本应用所用到的IPC的一个序列号,成功返回IPC键,失败返回-1。
[返回值]:成功返回键值,失败返回-1。
注:两进程如在pathname和proj_id上达成一致(或约定好),双方就都能够通过调用ftok函数得到同一个IPC键。
pathname的实现是组合了三个键,分别是:
①pathname所在文件系统的信息(stat结构的st_dev成员)。
②pathname在本文件系统内的索引节点号(stat结构的st_ino成员)。
③id的低序8位(不能为0)。
ftok调用返回的整数IPC键由 proj_id的低序8位,st_dev成员的低序8位,st_info的低序16位组合而成。
不能保证两个不同的路径名与同一个proj_id的组合产生不同的键,因为上面所列的三个条目(文件系统标识符、索引节点、proj_id)中的信息位数可能大于一个整数的信息位数。
2、semget()函数:创建一个信号量或访问一个已经存在的信号量集。
1 |
int semget(key_t key, int nsems, int semflg); |
参数1[key]:类似于端口号,也可以由ftok函数生成。
参数2[nsems]:在sysytem v中,申请信号量是以信号量集nsems去申请,而不是一个一个去申请,底层是一个数组。
参数3[semflg]:IPC_CREAT或IPC_EXCL。
[返回值]:是一个称为信号量标识符的整数,semop和semctl函数将使用它。
3、 semop()函数:操作一个或一组信号,也可称为PV操作。
1 |
int semop(int semid, struct sembuf *sops, unsigned nsops); |
参数1[semid]:信号集的id,通过semget获取。
参数2[sops]:是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
1 2 3 4 5 6 |
struct sembuf{ short sem_num; // 在信号集中的编码0,1,2... short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数, // 一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作 short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时, 操作系统释放信号量 }; |
参数nsops规定sops数组元素的个数:sem_op的取值如下:
参数3[nsops]:信号操作结构的数量,恒大于等于1。
[返回值]:成功返回0,失败返回-1。
4、semctl()函数:初始化或移除信号量集。
1 |
int semctl(int semid, int semnum, int cmd, ...);//可变参数列表 |
参数1[semid]:信号量集IPC标识符。
参数2[semnum]:表示信号集中的哪个信号,第一个信号为0。
参数3[cmd]:在semid指定的信号集上指向此命令,cmd的选择如下:
1 2 3 4 5 6 7 8 9 10 11 |
//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 设置信号量集中的一个单独的信号量的值。 |
参数4[可变参数列表]:是可选的,取决于第三个参数cmd。
[返回值]:成功返回正数,失败返回-1 。
[查看信号量]
1 |
ipcs -s |
[删除信号量]
1 |
ipcrm -s [semid] |
模拟父子进程的互斥
我们通过使用信号量,当打印完子进程的AA之后再打印父进程的BB,即保证了资源在某一时刻只有一个访问者。
comm.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#ifndef _COMM_H #define _COMM_H #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> #include <sys/stat.h> #define PATHNAME "." #define PROJ_ID 0X6666 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 } int creatSemSet(int nums); int getSemSet(); int destorySems(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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#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 creatSemSet(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 destorySems(int semid) { if(semctl(semid,0,IPC_RMID,NULL) < 0 ) { perror("semctl"); return -1; } return 0; } |
sem.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include "comm.h" int main() { int semid = creatSemSet(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; } |