Linux下多任务间通信和同步-System V信号量
2019-07-12 21:08发布
生成海报
Linux下多任务间通信和同步-System V信号量嵌入式开发交流群280352802,欢迎加入!一.简介
信号量与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制.相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志.出了用于访问控制外,还可用于进程同步.信号量有以下两种类型:
- 二值信号量:最简单的信号量形式,信号量的值只能取0或1;
- 计算信号量:信号量的值可以取任意非负值(当然受内核本身的约束).
操作信号量的方法与消息队列类似,主要包括三种类型:
- 打开或创建信号量;
- 增加或减少信号量的值;
- 获得或设置信号量属性.
信号量同步的原理实际上就是操作系统中所用到的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);
}
这个简单的程序只允许每个进程只有一个二进制信号量,如果需要用到更多的信号量,可以通过传递信号量变量的方法进行扩展. 通过多次启动这个程序来进行测试.第一次启动时要加上一个参数,表示应用程序由它来负责创建和删除信号量的工作,而后面的程序就不需要程序.下面是一个执行的结果.
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮