Linux下多任务间通信和同步-System V信号量

2019-07-12 21:08发布

Linux下多任务间通信和同步-System V信号量嵌入式开发交流群280352802,欢迎加入!

一.简介

信号量与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制.相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志.出了用于访问控制外,还可用于进程同步.信号量有以下两种类型:
  1. 二值信号量:最简单的信号量形式,信号量的值只能取0或1;
  2. 计算信号量:信号量的值可以取任意非负值(当然受内核本身的约束).
操作信号量的方法与消息队列类似,主要包括三种类型:
  • 打开或创建信号量;
  • 增加或减少信号量的值;
  • 获得或设置信号量属性.
信号量同步的原理实际上就是操作系统中所用到的PV原语.一次P操作使信号量sem减1,而一次V操作使信号量sem加1.进程(或线程)根据信号量的值来判断是否对共享资源具有访问权限.当sem的值大于等于0时,该进程(或线程)具有公共资源的访问权限;相反,当sem的值小于0时,该进程(或线程)将阻塞直到sem的值大于等于0为止.
信号量有两组系统调用函数.一种叫做System V信号量常用于进程的同步;另一种来源于POSIX,常用于线程同步.两者非常相似,但它们使用的函数调用却各不相同.本文介绍System V信号量的系统调用,后续的博文中介绍POOIX信号量.这组信号量系统调用的名字都以"sem"开头,基本的系统调用有三个:semget,semopt和semctl.

二.semget系统调用

该函数调用返回与键值key相对应的信号量描述子。其原型如下:
#include #include #include int semget(key_t key, int nsmes, intsemflg);参数key:是标志一个信号量集的标示符,用法与msgget()中的key相同;
参数nsems:指定打开或者新创建的信号量集中将包含信号量的数目;
参数semflg:是一些标志位.如果key所表示的信号量存在,则semget指定了IPC_CREAT|IPC_EXCL标志,那么计数参数nsems与原来信号量的数目不等,返回的也是EEXIST错误;如果semget只指定了IPC_CREAT标志,那么参数nsems必须与原来的值一致.

三.semopt系统调用

该系统调用执行信号量集合上的操作数组,这是个原子操作.其原型:
#include #include #include int semop(int semid, struct sembuf *sops,unsigned nsops);Sembuf 结构如下:
struct sembuf{ unsigned short sem_num; //对应信号集中的信号量,包括0 short sem_op; short sem_flg; //可取IPC_NOWIAT以及SEM_UNDO };         sem_num对应信号集中的信号量,0对应第一个信号量.sgm_flag可取IPC_NOWAIT和SEM_UNDO.如果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位.如果设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放.如果为一个信号量设置了该标志,内核都要分配一个sem_undo结构来记录它,为的是确保以后资源能够安全释放.
         事实上,如果进程退出了,那么它所占用就释放了,但信号量值却没有改变,此时,信号量值反应的已经不是资源占有的实际情况,在这种个情况下,问题的解决就靠内核来完成.这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍能然有它的记录,此时,就需要父进程调用waitpid来解决问题了.
         sem_op的值大于0,等于0以及小于0确定了对sem_num指定的信号量进行的三种操作.具体参考相应的man手册.
         这里需要强调的时semop同时操作多个信号量,在实际应用中,对应多种资源的申请或释放.semop保证操作的原子性,这一点尤为重要.尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占用任何资源情况下继续等待,这样,一方面避免了资源的浪费,另一方面,避免了进程之间由于申请共享资源造成死锁.
         也许从实际含义上更好理解这些操作:信号量的当前值记录相应资源目前可用数目;sem_op>0对应相应进程要释放sem_op数目的共享资源;sem_op=0可以用于对共享资源是否已用完的测试;sem_op<0相当于进程要申请-sem_op个共享资源.再联想操作的原子性,更不难理解该系统调用合适正常返回,何时睡眠等待.
四.semctl系统调用
该系统调用实现对信号量的各种控制操作.其原型:
#include #include #include int semctl(int semid, int semnum,int cmd,union semun arg);semun联合的结构定义为:
union semun{ int val; struct semid_ds *buf; unsigned short *array; };该系统调用详细信息请参见其手册,这里只给出参数cmd所能那个指定的操作:
  • IPC_STAT:获取信号量信息,信息由arg.buf返回;
  • IPC_SET:设置信号量信息,待设置信息保存在arg.buf中
  • GETALL:返回所有信号量的值,结果保存在arg.array中,参数sennnum被忽略;
  • GETNCNT:返回等待semnum所代表信号量的值增加的进程数,相当于目前有多少进程在等待semnum代表的信号量所代表的共享资源;
  • GETPID:返回最后一个对semnum所代表信号量执行semop操作的进程ID;成功时返回值为sempid;
  • GETVAL:返回semnum所代表信号量的值;成功返回值为semval;
  • GETZCNT:返回等待semnum所代表信号量的值变成0的进程数;成功时返回值为semzcnt;
  • SETALL:通过arg.array更新所有信号量的值;同时,更新与本信号集相关的semid_ds结构的sem_ctime成员;
  • SETVAL:设置semnum所代表信号量的值为arg.val.

五.System V信号量的应用实例

/**************************************************************************************/ /*简介:二进制信号量演示程序 */ /*************************************************************************************/ #include #include #include #include #include #include static int set_semvalue(void); static void del_semvalue(void); static int semaphore_p(void); static int semaphore_v(void); static int sem_id; union semun { int val; struct semid_ds *buf; unsigned short *array; }; int main(int argc, char *argv[]) { int i; int pause_time; char op_char = 'O'; srand((unsigned int)getpid()); sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); if (argc > 1) { if (!set_semvalue()) { fprintf(stderr, "Failed to initialize semaphore "); return 1; } op_char = 'X'; sleep(2); } /* 这是一个总共进出关键代码十次的循环语句。在每次循环的开始我们都要先做一次 semaphore_p调用,程序将从此开始进人关键代码。 */ for(i = 0; i < 10; i++) { if (!semaphore_p()) return 1; printf("%c", op_char); fflush(stdout); pause_time = rand() % 3; sleep(pause_time); printf("%c", op_char); fflush(stdout); /*经过一个随机等待时间之后,在进人下一次循环之前要先调用semaphore_v 把信号量设置为可用状态。整个循环语句执行完毕后,我们发出del_semaphore调 用对代码进行清理。*/ if (!semaphore_v()) return 1; pause_time = rand() % 2; sleep(pause_time); } printf(" %d - finished ", getpid()); if (argc > 1) { sleep(10); del_semvalue(); } return 0; } /* set_semvalue函数通过一个带SETVAL命令的semcti调用初始化信号量。在使用信号量之 前必须这样调用该函数*/ static int set_semvalue(void) { union semun sem_union; sem_union.val = 1; if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0); return(1); } /*del_semvalue函数通过调用一个带IPC_RMID命令的semctl系统调用 来删除那个信号量的标识码。*/ static void del_semvalue(void) { union semun sem_union; if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) fprintf(stderr, "Failed to delete semaphore "); } /* semaphore对信号量做”-1“操作(等待): */ static int semaphore_p(void) { struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = -1; /* P() */ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_p failed "); return(0); } return(1); } /* semaphore_v把sembuf结构中的sem_op部分设置为"l",从而使信号量变得可用。*/ static int semaphore_v(void) { struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1; /* V() */ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphore_v failed "); return(0); } return (1); } 这个简单的程序只允许每个进程只有一个二进制信号量,如果需要用到更多的信号量,可以通过传递信号量变量的方法进行扩展. 通过多次启动这个程序来进行测试.第一次启动时要加上一个参数,表示应用程序由它来负责创建和删除信号量的工作,而后面的程序就不需要程序.下面是一个执行的结果.