以下内容为本人的著作,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/lrERW0P7Q6zvklv4JfUO2A
在linux的多进程(或者多线程,这里以进程为例)开发里经常有进程间的通信部分,常见的技术手段有信号量、消息队列、共享内存等,而共享内存和信号量就像衬衫和外套一样搭配才算完整。
信号量的使用可以使得对资源的访问具有排它性,单一时刻只允许同一个进程访问,而其它的进程统统排队等候或者取消行程打道回府。
对资源的访问权既然要有排它性,那么访问权的获得就必然有竞争关系。竞争关系,又会使得结果是有顺序的,包括有序和无序。无序就是,竞争是公平的,对资源的访问权获取是随机的。而有序则是,对竞争的结果有刻意的安排,出现固定的顺序,比如数据生产消费模型里,数据一般是安排先在生产端输出,然后才轮到消费端访问。
好了,扯得太长太阳都快出来了。
信号量的使用库有System V库和POXIS库两种,这里仅简单介绍System V库和相关API,太详细会让人睡着的。
函数原型 | 备注 |
---|---|
int semget(key_t key, int nsems, int semflg) | 获取或者创建一个信号量集的标识符,一个信号量集可以包含有多个信号量,nsems代表信号量数量,key可以通过ftok获取(也可以直接使用IPC_PRIVATE,但是仅能用于父子进程间通信),semflg代表信号量集的属性 |
int semctl(int semid, int semnum, int cmd, union semun arg) | 设置或者读取信号量集的某个信号量的信息,semid代表semget返回值,semnum代表信号量的序号,类型union semun在某些系统中不一定存在(如有需要可以自定义) |
int semop(int semid, struct sembuf *sops, unsigned nsops) | 执行PV操作,P是对资源的占用,V是对资源的释放,类型struct sembuf包含了操作的具体内容,nsops代表操作信号量的个数(一般仅用1) |
struct sembuf {
short sem_num; //指定信号量,信号量在信号量集中的序号,从0开始
short sem_op; //小于0,就是执行P操作,对信号量减去sem_op的绝对值;大于0,就是执行V操作,对信号量加上sem_op的绝对值;等于0,等待信号量值归0
short sem_flg; //0,IPC_NOWAIT,SEM_UNDO(方便于调用进程崩溃时对信号量值的自动恢复,防止对资源的无用挤占)
}
下面介绍一下信号量的两种使用方式。
信号量最简单的使用方式就是无序的竞争方式。比如在获取资源时,只使用一个信号量,各个进程公平竞争上岗。预设其中一个特定进程启动后,初始化信号量的值为1(调用semctl实现)。然后当所有进程其中的一个需要抢占资源时,P操作对信号量值减1,信号量值归0,调用进程抢占资源成功,资源使用完成后,V操作对信号量值加1,信号量值变为1,释放资源。
当信号量值归0后,其它进程如果需要抢占资源,对信号量执行P操作会导致调用进程挂起并等待,这是调用进程堵塞了。如果执行P操作时,semop的sem_flg用了IPC_NOWAIT,则直接返回-1,通过errno可以获取到错误代码EAGAIN。
PV操作就是通过semop函数对信号量的值检查再加减操作。
老是觉得话太多还不如几行代码来得直接明了。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>
void P(int sid)
{
struct sembuf sem_p;
sem_p.sem_num = 0;
sem_p.sem_op = -1;
sem_p.sem_flg = 0;
if (semop(sid, &sem_p, 1) == -1) {
perror("p fail");
exit(-1);
}
}
void V(int sid)
{
struct sembuf sem_v;
sem_v.sem_num = 0;
sem_v.sem_op = 1;
sem_v.sem_flg = 0;
if (semop(sid, &sem_v, 1) == -1) {
perror("v fail");
exit(-1);
}
}
int main(int argc, char *argv[])
{
int fd = open("semtest", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(-1);
}
key_t key = ftok("semtest", 'a');
if (key == -1) {
perror("ftok");
exit(-1);
}
int sid = semget(key, 1, IPC_CREAT | 0666);
if (sid == -1) {
perror("semget");
exit(-1);
}
if (semctl(sid, 0, SETVAL, 1) == -1) {
perror("semctl");
exit(-1);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(-1);
} else if (pid == 0) {
// child process
while (1) {
P(sid);
printf("child get\n");
sleep(1);
printf("child release\n");
V(sid);
}
} else {
// parent process
printf("parent pid %d child pid %d\n", getpid(), pid);
while (1) {
P(sid);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid);
}
}
return 0;
}
然后看看结果输出,第一次可能是这样子的
parent pid 13156 child pid 13157
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...
第二次可能就是这样子了
parent pid 12873 child pid 12874
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...
很明显这就是信号量的无序竞争结果,就像永远猜不到下一个出现的会是如花姐姐还是白雪公主。
其实,进程间对资源的使用方式常常是有刻意顺序的,比如数据的生产消费模型使用场景。我们去茶楼喝茶,都是要先下好单等厨房的师傅们弄好端出来,我们才下筷吃起来,这里边就有既定的顺序啦。
那么怎么实现信号量的有序操作呢?如果仅仅使用一个信号量,对于各个进程来说,同一个信号量的值,你知我知大家知,大伙处在同一起跑线上,明显一个信号量是不够了。那么可以尝试使用多个信号量,毕竟人多力量大,大力出奇迹?(玩笑,给个评价____)
假设有两个进程(A和B)竞争使用同一个资源,使用资源的顺序要求先是A,然后B,如此循环。每个进程各分配一个代表的信号量(semA/semB)。由于信号量的值默认是0的,那么可以在最优先的进程(A)中对信号量(semA)的值初始化为1,其它信号量(semB)初始化为0,而在其它进程中不需要再对信号量的值作初始化了。
当进程(A)需要抢占资源时,P操作信号量(semA),信号量(semA)的值归0,抢占资源成功。进程(A)使用完需要释放资源时,V操作信号量(semB),信号量(semB)的值变为1,释放完成。在进程(A)中,资源释放后,这时如果再次尝试抢占资源,则P操作信号量(semA),检查信号量(semA)的值,发现已为0,抢占资源失败,进程(A)挂起等待资源。
在进程(A)释放资源后,如果进程(B)尝试抢占资源,P操作信号量(semB),信号量(semB)的值归0,抢占资源成功。进程(B)使用完需要释放资源时,V操作信号量(semA),信号量(semA)的值变为1,释放完成。如果进程(A)未曾抢占资源并且释放,这时进程(B)尝试抢占资源,P操作信号量(semB),检查信号量(semB)的值,发现已为0,抢占资源失败,进程(B)挂起等待资源。
这样就实现了资源总是先给到进程(A),待进程(A)释放资源后,进程(B)才有资格获取到。
下面是代码,look一look
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sem.h>
#include <string.h>
#include <errno.h>
void P(int sid, int index)
{
struct sembuf sem_p;
sem_p.sem_num = index;
sem_p.sem_op = -1;
sem_p.sem_flg = 0;
if (semop(sid, &sem_p, 1) == -1) {
printf("%d p fail: %s", index, strerror(errno));
exit(-1);
}
}
void V(int sid, int index)
{
struct sembuf sem_v;
sem_v.sem_num = index;
sem_v.sem_op = 1;
sem_v.sem_flg = 0;
if (semop(sid, &sem_v, 1) == -1) {
printf("%d v fail: %s", index, strerror(errno));
exit(-1);
}
}
int main(int argc, char *argv[])
{
int fd = open("semtest", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(-1);
}
key_t key = ftok("semtest", 'a');
if (key == -1) {
perror("ftok");
exit(-1);
}
int sid = semget(key, 2, IPC_CREAT | 0666);
if (sid == -1) {
perror("semget 2");
exit(-1);
}
if (semctl(sid, 0, SETVAL, 1) == -1) {
perror("semctl 0");
exit(-1);
}
if (semctl(sid, 1, SETVAL, 0) == -1) {
perror("semctl 1");
exit(-1);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(-1);
} else if (pid == 0) {
// child
while (1) {
P(sid, 1);
printf("child get\n");
sleep(1);
printf("child release\n");
V(sid, 0);
}
} else {
// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
while (1) {
P(sid, 0);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid, 1);
}
}
return 0;
}
编译执行,看看输出。
parent pid 271 child pid 272
parent get
parent release
child get
child release
parent get
parent release
child get
child release
parent get
parent release
...
无论执行多少遍这程序,发现parent永远是最先抢占资源的。不信的话,还可以在parent的while循环之前加个延时,再看看输出结果(治好你的小鸡咕噜。。。)。你会发现parent这只小兔子无论故意睡多久的懒觉,还是会第一个冲出屏幕(不是终点线)。
// parent
printf("parent pid %d child pid %d\n", getpid(), pid);
sleep(10);
while (1) {
P(sid, 0);
printf("parent get\n");
sleep(1);
printf("parent release\n");
V(sid, 1);
}
如果把上面信号量初始化的代码改一改(会不会单车变摩托?想多了。。。)
改成:子进程的代表信号量值初始化为1,父进程的代表信号量初始化为0。
if (semctl(sid, 0, SETVAL, 0) == -1) {
perror("semctl 0");
exit(-1);
}
if (semctl(sid, 1, SETVAL, 1) == -1) {
perror("semctl 1");
exit(-1);
}
编译后再执行程序看看输出,发现最先抢占资源的变成永远是child了
parent pid 298 child pid 299
child get
child release
parent get
parent release
child get
child release
parent get
parent release
child get
child release
...