以下内容为本人的著作,如需要转载,请声明原文链接 微信公众号「englyf」https://mp.weixin.qq.com/s/lrERW0P7Q6zvklv4JfUO2A
信号量的无序竞争和有序竞争
在 linux 的多进程(或者多线程,这里以进程为例)开发里经常有进程间的通信部分,常见的技术手段有信号量、消息队列、共享内存等,而共享内存和信号量就像衬衫和外套一样搭配才算完整。
信号量的使用可以使得对资源的访问具有排它性,单一时刻只允许同一个进程访问,而其它的进程统统排队等候或者取消行程打道回府。
对资源的访问权既然要有排它性,那么访问权的获得就必然有竞争关系。竞争关系,又会使得结果是有顺序的,包括有序和无序。无序就是,竞争是公平的,对资源的访问权获取是随机的。而有序则是,对竞争的结果有刻意的安排,出现固定的顺序,比如数据生产消费模型里,数据一般是安排先在生产端输出,然后才轮到消费端访问。
好了,扯得太长太阳都快出来了。
信号量的使用库有 System V 库和 POXIS 库两种,这里仅简单介绍 System V 库和相关 API,太详细会让人睡着的。
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
...
复制代码
好了,介绍到这里,期待你的一键三连(⊙o⊙)
评论