Linux 进程通信 IPC 对象之信号量:信号量与其他 IPC 对象不同,它是一个计数器,用于多个进程对共享数据对象的访问,它的本质是一种数据操作锁。
它不像消息队列和管道那样具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信。
如何通过信号量来控制进程间通信
为了获得共享资源,进程需要执行下列操作:
(1)、测试控制该资源的信号量;
(2)、若此信号量为正,则进程可以使用该资源,在这种情况下进程会将信号量值减一,表示它使用了一个资源单位;
(3)、若此信号量的值为 0,则进程进入休眠状态,直至信号量值大于 0,进程才被唤醒,此时又返回到步骤(1)。
注:信号量的测试和减一操作应当是原子操作,为此信号量通常是在内核中实现的。
为什么要使用信号量
使用信号量是为了防止因多个程序同时访问一个共享资源而引发的一系列问题,而信号量就是这样的一种方法,它可以保证任一时刻只能有一个执行线程访问代码的临界区域(临界区域是执行数据更新的代码需要独占式的执行),也就是说信号量是用来协调进程对共享资源的访问的。
操作系统是如何对信号量进行管理的
首先,内核为每个信号量集合维护着一个 semid_ds 结构
这个结构中包含了每个 IPC 对象都会包含的成员那就是 ipc_perm(这个结构主要规定了 IPC 对象的权限和所有者),另外还含有信号量集合中元素数量 sem_nsems 成员,以及信号量处理的相关时间。
每个信号量其实是由一个无名结构体所表示的,它包含下列成员
- struct {
- unsigned short semval;
- pid_t sempid;
- unsigned short semncnt;
- unsigned short semzcnt;
- }
另外,操作系统还提供了相关的信号量系统调用接口,来使用户对信号量进行管理。
信号量接口函数信号量的创建与获得
- int semset(key_t key, int nsems, int flag); //若成功,返回信号量ID,若失败,返回-1
当创建一个新的信号量集合时,要对 semid_ds 结构下列成员赋初值
sem_otime 设置为 0; //sem_otime 是最后一次 semop() 的时间 sem_ctime 设置为当前时间; // 是最后一次调用 semctl() 的时间
sem_nsems 设置为 nsems; // 信号量集合中的信号量数,如果是创建则必须要指定 nsems,如果引用现有的那么将 nsems 设置为 0 即可。
semctl 包含了多种信号量操作
- int semctl(int semid, int semnum, int cmd, ...
- /*union semun arg*/
- );
该函数包含了对信号量的多种操作其中第四个参数是可选的,是否使用取决于 cmd
如果使用了第四个参数,那么它的类型是 union semun;
cmd:cmd 参数通常指定下列十种参数的一种
IPC_STAT: 对此集合取 semid_ds 结构,并存储在 arg.buf 中;
IPC_SET: 按 arg.buf 指向的结构中的值,设置此集合相关的结构中的 sem_perm.uid,sem_perm.gid 和 sem_perm.mode 字段。
IPC_RMID: 从系统中删除该信号量的集合,删除立即生效。
GETVAL: 返回成员 semnum 的 semval 值, 由 arg.va 指定(这条在对信号量的初始化中用到);
SETVAL: 设置成员 semnum 的 setval 值;
GETPID: 返回成员 semnum 的 sempid 值;
GETALL: 取信号量集合中所有的信号量值,这些值存储在 arg.array 中;
SETALL: 将该集合中的所有信号量值设置成指向 arg.array 指向的数组中的值;
semop 函数
- int semop(int semid, struct sembuf semoparry[], size_t nops); //若成功,返回0,若出错,返回-1
nops:nops, 规定了该数组(semoparry[])中操作信号量的数量;
:是一个类型为 struct sembuf 的数组,sembuf 的成员如下
sembuf 是一个表示信号量操作的数组;
关于对信号量的操作主要与 sembuf 中的 sem_op 成员有关:
(1): 若 sem_op 为正值,此时对应于进程释放的或占用的资源数 (所以信号量的 V 操作的 sem_op 一般为正值,),sem_op 值会加到信号量的值上;如果指定了 undo 标志(对应于 sem_flg 成员的 SEM_UNDO 位),则也从该进程的此信号量调整值减去 sem_op.
(2): 若 sem_op 为负值,则表示调用进程要获取由该信号量控制的资源(所以我们的 P 操作的 sem_op 一般为负值,并且这个值的绝对值要小于等于 semval).
(3): 若 sem_op 为 0:表示调用进程希望等待到该信号量的值变为 0:
SEM_UNDO
SEM_UNDO 是 semun 中的 sem_flg 成员的标志位,设定该标志位主要是用来异常退出进程时进行调整,操作系统所设置的调整如下:
由于信号量的生命周期是 "随系统", 即如果我们创建了信号量集合,但是没有对其进行删除,那么信号量便会一直存在在操作系统中,直到操作系统关闭或者用户使用命令删除为止,而如果我们为信号量设定了 SEM_UNDO 标志,如果 sem_op 的值小于 0(即此时调用进程占有临界区域),当该进程终止时,内核都会检验进程是否还有尚未处理的信号量调整值,如果有则进行相应的调整。
当操作信号量 (semop) 时,sem_flg 可以设置 SEM_UNDO 标识;SEM_UNDO 用于将修改的信号量值在进程正常退出(调用 exit 退出或 main 执行完)或异常退出(如段异常、除 0 异常、收到 KILL 信号等)时归还给信号量。
测试用例
我们在父进程中用 fork 创建子进程,然后父进程向屏幕上打印 AA, 子进程向屏幕上打印 BB, 观察在设置信号量和没有设置信号量时所出现的不同的情况,案例源代码如下:
信号量头文件
- /************************************************************************* > File Name: MySem.h > Author: LZH > Mail: www.597995302@qq.com > Created Time: 2017年02月16日 星期四 02时47分13秒 ************************************************************************/
- #ifndef __SEM_H__#define __SEM_H__#include#include#include#include#include#define PATHNAME "."#define PROJID 0x6666union 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(Linux-specific) */
- };
- int CreatSem(int nsems);
- int InitSem(int semid);
- int GetSemID();
- int P(int semid, int which);
- int V(int semid, int which);
- int DestorySem(int semid);#endif //__SEM_H__
信号量源文件
- /************************************************************************* > File Name: MySem.c > Author: LZH > Mail: www.597995302@qq.com > Created Time: 2017年02月16日 星期四 02时49分08秒 ************************************************************************/
- #include "MySem.h"int InitSem(int semid) {
- union semun un;
- un.val = 1;
- int ret = semctl(semid, 0, SETVAL, un);
- if (ret < 0) {
- perror("semctl ...\n");
- return - 1;
- }
- return 0;
- }
- static int CommSem(int nsems, int flags) {
- key_t _k = ftok(PATHNAME, PROJID);
- if (_k < 0) {
- perror("ftok error..\n");
- return - 1;
- }
- int semid = semget(_k, nsems, flags);
- if (semid < 0) {
- perror("semget error..\n");
- return - 2;
- }
- return semid;
- }
- int CreatSem(int nsems) {
- return CommSem(nsems, IPC_CREAT | IPC_EXCL | 0666);
- }
- int GetSemID() {
- return CommSem(0, 0);
- }
- int SemOp(int semid, int op, int num) {
- struct sembuf buf;
- buf.sem_op = op;
- buf.sem_num = num;
- buf.sem_flg = 0;
- int ret = semop(semid, &buf, 1);
- if (ret < 0) {
- perror("Semop..\n");
- return - 1;
- }
- return 0;
- }
- int P(int semid, int which) {
- return SemOp(semid, -1, which);
- }
- int V(int semid, int which) {
- return SemOp(semid, 1, which);
- }
- int DestorySem(int semid) {
- int ret = semctl(semid, 0, IPC_RMID);
- if (ret < 0) {
- perror("semctl..\n");
- }
- return 0;
- }
- #include "MySem.h"int main() {
- int semid = CreatSem(10);
- printf("Semid:%d\n", semid);
- InitSem(semid);
- pid_t id = fork();
- if (id == 0) {
- while (1) {
- P(semid, 0);
- usleep(5300);
- printf("A ");
- fflush(stdout);
- usleep(5000);
- printf("A ");
- usleep(10000);
- fflush(stdout);
- V(semid, 0);
- }
- } else {
- while (1) {
- P(semid, 0);
- usleep(10300);
- printf("B ");
- fflush(stdout);
- usleep(10000);
- printf("B ");
- usleep(10000);
- fflush(stdout);
- V(semid, 0);
- }
- }
- DestorySem(semid);
- return 0;
- }
如果我们没有对父子进程的临界区域(打印字符的代码)使用信号量的相关操作,测试结果如下:
我们会发现不像我们想象的那样出现成对的 AA 和 BB,这时因为父子进程对输出屏幕这个共享竞争的结果,如果我们使用了信号量,所得到的测试结果如下:
这时输出屏幕上出现了成对的 AA 和 BB, 这样就体现了信号量原子性操作的价值,原子性操作即要么不做,要做就一次做完。
总结
本文我们主要提到了 IPC 对象之一——信号量的实现机制和相关操作,我们要能明白是信号量不是进程之间传递消息的共享资源,而是管理进程之间交换数据的资源的计数器,此外内核还提供了相关的接口函数来使用户能够用信号量管理临界区域,避免出现多个程序同时访问一个共享资源所引发的一系列问题,在这些接口函数中,我们特别要注意 semop 函数, 它保证了信号量进行的是原子操作。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: http://www.92to.com/bangong/2017/02-17/17253292.html