我们在进程间通信专题已经详细的介绍过了管道和共享内存的进程间通信机制。但是其实还有一种非常独特的进程间通信机制,也就是本篇博客将为大家介绍的System V机制的信号量。
为什么说他独特,因为之前我们介绍的通信机制最终都会有一个数据交互的场所,而信号量其实是一个计数器。有的同学会提出质疑,计数器怎么能作为进程通信的机制呢?别忘了,进程控制也是进程通信的目的。接下来,笔者就为大家详细介绍今天的主角。
上面说了,信号量其实是一个非负值计数器,这个计数器表示现在系统中有多少资源能够被申请使用。当进程申请信号量时这个计数器就会减少,反之归还资源时这个计数器就会加加。每当信号量为0时表示没有资源可以被申请,此时再来申请资源的进程就会被暂时挂起,直到信号量不为零时才唤醒。
由此可见信号量一定维持了一个属于自己的等待队列。其实信号量很好理解,我们拿生活中一个形象的栗子说明
类比进程,每个卫生间被占用就好比向信号量申请资源,相反归还资源。厕所满时就要像进程一样等待。而在System V中信号量申请和归还对应了下面的俩个操作。
P(sv): 如果sv的值大于零,就给它减一; 如果它的值为零,否则就挂起该进程(对应申请资源)
V(sv): 如果有其他进程因等待sv而被挂起,就让他恢复运行,如果没有进程因等待sv而挂起,就给它+1.(对应归还资源)
对于操作系统来说,他需要管理信号量。一般需要对信号量完成下里的几种操作:
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作.。为此,信号量通常是在内核中实现的,这个不难理解。常用的信号量形式被称为二元信号量.,它控制单个资源,其初始值为1.。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。
现在相信你对信号量应该已经有了初步的理解,但是其实真正System V信号量并没有像我们想的那样单纯是个计数器,而是每次申请时,它可以帮我们申请一组信号量。
struct semid_ds
{
struct ipc_perm sem_perm;//信号量的操作权限信息
unsigned short sem_nsems;//信号集中有多少个信号量
time_t sem_otime;//最后一次调用semop的时间
time_t sem_ctime;//最后一次调用semctl的时间
};
而信号量集中一个一个信号量通常使用一个匿名结构体来描述:
struct ...
{
unsigned short semval;//信号量值
pid_t sempid;//最后一次执行semop操作的进程数量
unsigned semncnt;//等待信号量增加的进程数
unsigned semzcnt;//等待信号量变为零的进程数
}
上面结构体中提到了几个还没有说的函数不用关心,下面讲解后同学们可以倒回来看看。现在我们就结合函数,一起来看看如何使用信号量。
semget函数:用来创建一个新信号量集,或者用来获取已经有的信号量集
int semget(key_t key, int num_sems, int sem_flags);
参数介绍:
函数调用成功后就会在系统中为我们创建一个semid_ds信号集合,并且初始化其中的某些部分。函数返回值返回一个此信号集的标识符,失败时返回-1。
semop函数:用来改变信号量的值
int semop(int sem_id, struct sembuf *sem_opa,size_t num_sem_ops);
参数介绍:
单独说第二个参数说明他肯定不一般,说实话信号量这个接口设计的真的是非常不友好,恶心程度快赶上了select了emmmm。。。。
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0,代表要操作信号量的编号
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
sem_flg:顺序原因我们先介绍这个标识,这个标识一般为SEM_UNDO和SEM_NOWAIT
sem_num:要操作信号量的编号,拿我们最开始那个田字格图来说,操作信号量1就填1,不过之前我们也说了,一般我们信号量集合中只有一个信号量,所以大多数情况下都是0
sem_op:此变量是一个整数,会分为三种情况,然而再谈三种情况前,我们又要提到信号量调整数的概念
现在来谈sem_op设置为不同数时的三种情况:
sem_op为正数:最易于处理的情况是sem_op为正值,这对英语进程释放占用的资源数。sem_op值会加到信号量的值上,如果指定了undo标志,则也要从该进程的此信号量调整值semadj中减去sem_op的值
若se_op为负数:则表示要获取由该信号量控制的资源,如果该信号量的值大于或等于sem_op的绝对值,则从信号量值中减去sem_op的绝对值。 这能保证信号量的结果值大于等于0,如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。但是如果信号量的值小于sem_op的绝对值,情况就不一样了。
若指定了IPC_NOWAIT,则semop出错返回EAGAIN。若未指定IPC_NOWAIT,则该信号量的semncnt+1(因为调用进程将进入休眠模式),然后调用进程被挂起直至下列事情之一发生
i.此信号量值变成大于等于sem_op的绝对值(既某个进程已经释放了某些资源). 此信号量的semncnt的值减一,并且从信号量值中减去sem_op的绝对值.如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值当中
ii.从系统中删除了此信号量,自这种情况下,函数出错返回EIDRM
iii. 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt的值减去1,并且函数出错返回EINTR
若sem_op为0:这表示调用进程希望等待到该信号量的值变为0
若指定了IPC_NOWAIT,则semop出错返回EAGAIN。若未指定IPC_NOWAIT,则该信号量的semnznt+1(因为调用进程将进入休眠模式),然后调用进程被挂起直至下列事情之一发生
i.此信号量的值变成0,此信号量的semzcnt减去一(因为调用进程已结束等待)
ii.从系统中删除此信号量,在这种情况下,函数出错返回EIDRM
iii.进程捕捉到一个信号,并且从信号处理程序返回. 在这种情况下,此信号量的semzcnt值减去1,并且函数出错返回EIINTR
semctl函数:该函数用来直接控制信号量信息,也就是直接删除信号量或者初始化信号量
看到这里的老铁没有吐吧,反正笔者先吐为敬。别急,还有更恶心的东西,先来看semctl函数原型:
int semctl(int sem_id, int sem_num, int command, ...);
参数介绍:
sem_id:填充semget函数的返回值
sem_num: 操作信号在信号集中的编号,同sembuf中的sem_num
command :命令,标识要进行的操作
参数4:一个不简单的省略号
命令 | 含义 |
---|---|
IPC_STAT | 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中 |
IPC_SET | 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数 |
IPC_RMID | 将信号量集从内存中删除 |
GETALL | 用于读取信号量集中的所有信号量的值 |
GETNCNT | 返回正在等待资源的进程数目 |
GETPID | 返回最后一个执行semop操作的进程的PID |
GETVAL | 返回信号量集中的一个单个的信号量的值 |
GETZCNT | 返回这在等待完全空闲的资源的进程数目 |
SETALL | 设置信号量集中的所有的信号量的值 |
SETVAL | 设置信号量集中的一个单独的信号量的值 |
第四个参数在使用有些选项时需要用到,比如SETVAL。仔细的读者发现我们即使讲到现在还是没有将信号量的值初始化,所以我们使用SETVAL时就需要使用第四个参数
union semun
{
int val; //用于SETVAL
struct semid_ds *buf; //用于IPC_SET和IPC_STAT
unsigned short *arry; //SETALL和GETALL使用
};
此时就实现了进程间的同步,信号量接口真的设计的非常不友好
#include
#include
#include
#include
#include
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
struct seminfo* __buf;
};
void pv(int sem_id, int op)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
}
int main()
{
int sem_id = semget(IPC_PRIVATE, 1, IPC_CREAT | IPC_EXCL | 0644);
union semun sem_un;
sem_un.val = 1;//初始化信号量为1
semctl(sem_id, 0, SETVAL, sem_un);
pid_t id = fork();
if (id < 0)
{
return 1;
}
else if (id == 0)
{
printf("child try to get binary sem\n");
pv(sem_id, -1);
printf("chlid get the sem and release it after 5 seconds\n");
sleep(5);
pv(sem_id, 1);
exit(0);
}
else
{
printf("parent try to get binary sem\n");
pv(sem_id, -1);
printf("parent get the sem and release it after 5 seconds\n");
sleep(5);
pv(sem_id, 1);
}
waitpid(id, NULL, 0);
semctl(sem_id, 0, IPC_RMID, sem_un);
return 0;
}
看了信号量的系统调用接口,我感觉我上辈子就是个蜀道啊。不过多看几遍,应该也不是太难理解。