System V IPC – 信号量
信号量不同于其它IPC,它本身是不携带消息数据的,它的功能主要实现进程的同步的。上一篇讲解共享内存的时候说过,多个进程会出现同时操作共享内存的情况,多个进程访问临界资源可能会造成错误,所以需要互斥的访问,那么信号量恰好可以实现对共享内存的互斥访问!(信号量和共享内存是一对好兄弟)
信号量是由内核维护的一个整数,一般该值 ≥ 0;信号量正是由内核维护的一个整数,所以对信号量的操作是原子操作,是由内核保证的!
信号量最简单的理解:可以理解成是资源的剩余数量。如体育馆上有3个篮球场、2个乒乓球场。此时为了表示两种资源需要创建两个信号量sem1、sem2。早上体育馆开馆时,球场都是空闲的,所以两个信号量为了表示球场的数量分别初始化成:sem1 = 3, sem2 = 2;当有人来打乒乓球时,需要占用一个乒乓球场,那么代表乒乓球场的sem2需要 -1。此时还剩下一个乒乓球场,倘若此时又来了人来打乒乓球,所以需要再占用一个乒乓球场,那么sem2需要 -1,此时sem2的值为0,表示已经没有乒乓球场这个资源了,所以再来人打乒乓球,肯定是打不了了。假如有一伙打球的人打累了,退场了,那么sem2的值需要 +1,此时sem2的值为1。
通过上面的例子,动态的说明了信号量值的作用,信号量代表某个资源,信号量的值代表该资源的剩余数量。
某些靓仔、靓女看到这里可能会有一个疑问,为啥不直接使用一个变量来代表资源的数量?对一个变量做简单的加减操作时,虽然代码只有简单的一行,但实际上是需要很多条汇编指令来完成的,并不是一个原子操作(不可以被分割,执行过程中不可以被打断)。
示例:
//add.c
#include <stdio.h>
int main(int argc, char **argv)
{
int a = 0;
a++;
return 0;
}
[wy@wy ~/textCode/text/FIFO]$ gcc add.c -S //生成.s文件
//add.s
.file "add.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $0, -4(%rbp)
addl $1, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
可以看到简单一个+1操作有好几条汇编指令(当然,你不需要浪费时间去读懂这些汇编指令的含义,知道对一个变量简单的+1不是原子操作就够了)
-
信号量集的打开或创建
int semget(key_t key, int nsems, int semflg); //参数2:创建的信号量的个数,需要>0
需要注意的是,此时只是创建了信号量,此时信号量还没有值,所以还不能直接使用
可以通过ipcs -s查看是否创建成功,也可以使用ipcrm -S/s 删除信号量集
[wy@wy ~/textCode/text/ipc/sem]$ ipcs -s --------- 信号量数组 ----------- 键 semid 拥有者 权限 nsems 0x00000080 32768 wy 660 1
-
信号量的控制
int semctl(int semid, int semnum, int cmd, ...); //参数2:需要操作的信号量(哪一个信号量) //参数3:操作,可以通过SETVAL(对一个信号量设置)、SETALL(对所有信号量设置,此时参数semnum需要填成0) 对信号量的值进行设置 //可变长参数:根据不同的cmd,有不同的填法,如果需要填第四个参数,它被定义成一个union,如下 union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */ }; //semun中的结构体定义不在详细说明,详情可以查看man帮助
对信号量设置初始值的时候,如果初始值大于1,又称为计数信号量;如果初始值等于1,称为二元信号量,该信号量的作用显然适用于互斥操作(特殊的同步)!
-
信号量的操作
int semop(int semid, struct sembuf *sops, size_t nsops); //sops:指向结构体数组的指针,结构体中包含了需要执行操作 struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; //nsops:指定数组的大小(至少为1) //sem_flg标记,除了可以设置为IPC_NOWAIT(不阻塞)外,还可以设置SEM_UNDO(回退操作), //如果当前两个进程访问临界资源采用二进制信号量,其中一个进程执行完信号量值减一变为0后, //突然意外终止了,造成信号量的值一直为0,那么另一个进程需要访问临界资源的时候,需要判断 //信号量的值,发现值一直为零,所以使没办法访问临界资源的,所以如果使用SEM_UNDO //标志位后,当某个进程意外退出时,可以回退信号量在被该进程使用之前的值!
信号量的操作在大学教材中常用的说法是P、V;这两个字母可不是某两个英文单词的首字母,而是荷兰词汇的首字母。因为提出信号量的概念的大佬是荷兰计算机科学家Dijkstra提出的。
-
代码示例:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/wait.h> #include <sys/types.h> /* * 本程序实现两个进程操作共享内存的一个整型值,使用信号量实现两个进程 * 互斥的访问共享内存 */ #define N 10000000 /* 小目标:1000w */ int main(int argc, char **argv) { int shmid = shmget(100, 8, IPC_CREAT|0660); int *p = (int*)shmat(shmid, NULL, 0); memset(p, 0, 8); int semid = semget(128, 1, IPC_CREAT|0660); //对一号信号量(数组下标从0开始,所以我写的是0)的值设置为1 semctl(semid, 0, SETVAL, 1); struct sembuf P, V; memset(&P, 0, sizeof(P)); memset(&V, 0, sizeof(V)); //定义P,V结构体的操作 P.sem_num = 0; P.sem_op = -1; P.sem_flg = 0; V.sem_num = 0; V.sem_op = 1; V.sem_flg = 0; if(0 == fork()){ for(int i = 0; i < N; ++i){ //执行P操作,使信号量值-1,变成0 semop(semid, &P, 1); (*p)++; //执行V操作,使信号的值+1 semop(semid, &V, 1); } exit(0); } else{ for(int i = 0; i < N; ++i){ //执行P操作,使信号量值-1,变成0 semop(semid, &P, 1); (*p)++; //执行V操作,使信号的值+1 semop(semid, &V, 1); } wait(NULL); printf("*p = %d\n", *p); } shmdt(p); return 0; } 运行结果: *p = 20000000
可以得到正确的结果!
-
最后不推荐使用system V的信号量实现同步,可以使用锁或是使用posix的信号量替代
三种system V的高级IPC就全部讲解完毕了,总结如下,哈哈!
消息队列 | 共享内存 | 信号量 | |
---|---|---|---|
创建 | msgget() | shmget() | semget() |
控制 | msgctl() | shmctl() | semctl() |
其它 | 发消息:msgsnd() 收消息:msgrcv() | 附加到本进程:shmat() 解除附加:shmdt() | 控制:semop() |
查看 | ipcs -q | ipcs -m | ipcs -m |
查看限制 | ipcs -lq | ipcs -lm | ipcs -ls |
命令删除 小写后面+id 大写后面+key | ipcrm -q/Q | ipcrm -m/M | ipcrm -s/S |
本人能力有限,如有错误望各位大佬不吝指正,原创不易,转载请注明出处