目录
信号量跟前面的 MSG 和 SHM 有极大的不同,SEM 不是用来传输数据的, 而是作为“旗语”,用来协调各进程或者线程工作的
信号量类别
分为三种:ststem-V信号量,POSIX有名信号量和POSIX无名信号量
作用
用来表征一种资源的数量,当多 个进程或者线程争夺这些稀缺资源的时候,信号量用来保证他们合理 地、秩序地使用这些资源,而不会陷入逻辑谬误之中
基本概念
- 多个进程或线程有可能同时访问的资源(变量、链表、文件等等)称为共享资源,也叫临界资源
- 访问这些资源的代码称为临界代码,这些代码区域称为临界区
- 程序进入临界区之前必须要对资源进行申请,这个动作被称为 P 操作,这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样,P 操作就是申请资源,如果申请成功,资源数将会减少。如果申请失败,要不在门口等,要不走人
- 程序离开临界区之后必须要释放相应的资源,这个动作被称为 V 操作,这就像你把车开出停车场之后,要将停车卡归还给保安一样,V 操作就是释放资源,释放资源就是让资源数增加
api semget
注意 在/proc/sys/kernel/sem 中可查看
- SEMMNI:系统中信号量的总数最大值
- SEMMSL:每个信号量中信号量元素的个数最大值
- SEMMNS:系统中所有信号量中的信号量元素的总数最大值
api semop
注意
- 信号量操作结构体
struct sembuf { unsigned short sem_num; /* 信号量元素序号(数组下标) */ short sem_op; /* 操作参数 */ short sem_flg; /* 操作选项 */ };
信号量元素的序号从 0 开始,实际上就是数组下标
- 根据 sem_op 的数值,信号量操作分成 3 种情况
- 当 sem_op 大于 0 时:进行 V 操作,即信号量元素的值(semval)将会被加上 sem_op 的值。如果 SEM_UNDO 被设置了,那么该 V 操作将会被系统记录。V 操作永远不会导致进程阻塞
- 当sem_op等于0时:进行等零操作,如果此时semval恰好为0,则semop( )立即成功返回,否则如果 IPC_NOWAIT 被设置,则立即出错返回并将 errno 设置为EAGAIN,否则将使得进程进入睡眠,直到以下情况发生
- semval 变为 0
- 信号量被删除。(将导致 semop( )出错退出错误码为 EIDRM)
- 收到信号。(将导致 semop( )出错退出,错误码为 EINTR)
- 当 sem_op 小于 0 时:进行 P 操作,即信号量元素的值(semval)将会被减 去 sem_op 的绝对值。如果 semval 大于或等于 sem_op 的绝对值,则 semop( )立 即成功返回,semval 的值将减去 sem_op 的绝对值,并且如果 SEM_UNDO 被设置 了,那么该 P 操作将会被系统记录。如果 semval 小于 sem_op 的绝对值并且设置了 IPC_NOWAIT,那么 semop( )将会出错返回且将错误码置为 EAGAIN,否则将使得 进程进入睡眠,直到以下情况发生
- semval 的值变得大于或者等于 sem_op 的绝对值。
- 信号量被删除。(将导致semop( )出错退出,错误码为 EIDRM)
- 收到信号。(将导致semop( )出错退出,错误码为 EINTR)
api semctl
注意
- 这是一个变参函数,根据 cmd 的不同,可能需要第四个参数,第四个参数是一个 如下所示的联合体,用户必须自己定义
union semun { int val; /* 当 cmd 为 SETVAL 时使用 */ struct semid_ds *buf; /* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */ unsigned short *array; /* 当 cmd 为 GETALL 或 SETALL 时使用 */ struct seminfo *__buf;/* 当 cmd 为 IPC_INFO 时使用 */ };
- 使用 IPC_STAT 和 IPC_SET 需要用到以下属性信息结构体
struct semid_ds { struct ipc_perm sem_perm; /* 权限相关信息 */ time_t sem_otime; /* 最后一次 semop( )的时间 */ time_t sem_ctime; /* 最后一次状态改变时间 */ unsigned short sem_nsems; /* 信号量元素个数 */ };
权限结构体
struct ipc_perm { key_t __key; /* 该信号量的键值 key */ uid_t uid; /* 所有者有效 UID */ gid_t gid; /* 所有者有效 GID */ uid_t cuid; /* 创建者有效 UID */ gid_t cgid; /* 创建者有效 GID */ unsigned short mode; /* 读写权限 */ unsigned short __seq; /* 序列号 */ };
- 使用 IPC_INFO 时,需要提供以下结构体:
struct seminfo { int semmap; /* 当前系统信号量总数 */ int semmni; /* 系统信号量个数最大值 */ int semmns; /* 系统所有信号量元素总数最大值 */ int semmnu; /* 信号量操作撤销结构体个数最大值 */ int semmsl; /* 单个信号量中的信号量元素个数最大值 */ int semopm; /* 调用 semop( )时操作的信号量元素个数最大值 */ int semume; /* 单个进程对信号量执行连续撤销操作次数的最大值 */ int semusz; /* 撤销操作的结构体的尺寸 */ int semvmx; /* 信号量元素的值的最大值 */ int semaem; /* 撤销操作记录个数最大值 */ };
- 使用 SEM_INFO 时,跟 IPC_INFO 一样都是得到一个 seminfo 结构体,但其中 几个成员的含义发生了变化
- semusz 此时代表系统当前存在的信号量的个数
- semaem 此时代表系统当前存在的信号量中信号量元素的总数
例子
Jack 需要写数据,Rose 需要读数据,所以对于 Jack 而言内 存空间是资源——有这个资源才能写数据,同理对于 Rose 而言内存中的数据是资源——有 了这个资源才能读数据,而且 Jack 一旦写了数据,内存空间资源就减少了,数据资源就增 加了,当 Rose 读取数据的时候,情况恰好相反。 为了协调他们,使用两个信号量元素,来分别表示“内存空间”和“数据”这两种资源, 在刚开始的时候,“内存空间”的可用数目是 1(假设将整块 SHM 当做一个资源),而“数 据”的可用数据是 0(刚开始啥也没有),根据访问临界资源的一般原则
照上图所示,两个信号量元素就好像两盏红绿灯,有效地协调了双方的读写操作—— Jack 不会重复写入而把尚未读出的数据覆盖,Rose 也不会将一个数据重复读取多次,每 当他们申请相应的资源而不可获得时,信号量会自动使得他们睡眠
head4sem.h
1 #ifndef _HEAD4SEM_H_
2 #define _HEAD4SEM_H_
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <unistd.h>
7 #include <errno.h>
8 #include <string.h>
9 #include <strings.h>
10
11 #include <sys/stat.h>
12 #include <sys/ipc.h>
13 #include <sys/shm.h>
14 #include <sys/sem.h>
15
16 #define SHMSZ 128
17
18 #define PROJ_PATH "." // 用以产生键值 key
19 #define ID4SHM 1
20 #define ID4SEM 2
21
22 union semun // 自定义的信号量操作联合体
23 {
24 int val;
25 struct semid_ds *buf;
26 unsigned short *array;
27 struct seminfo *__buf;
28 };
29
30 static void sem_p(int semid, int semnum) // P 操作
31 {
32 struct sembuf op[1];
33 op[0].sem_num = semnum;
34 op[0].sem_op = -1;
35 op[0].sem_flg = 0;
36
37 semop(semid, op, 1);// PV操作
38 }
39
40 static void sem_v(int semid, int semnum) // V 操作
41 {
42 struct sembuf op[1];
43 op[0].sem_num = semnum;
44 op[0].sem_op = 1;
45 op[0].sem_flg = 0;
46
47 semop(semid, op, 1);// PV操作
48 }
49
50 static void seminit(int semid, int semnum, int value) // 初始化
51 {
52 union semun a;
53 a.val = value;
54 semctl(semid, semnum, SETVAL, a); //设置设置该信号量元素的值
55 }
56
57 #endif
Jack.c
1 #include "head4sem.h"
2
3 int main(int argc, char **argv)
4 {
5 key_t key1 = ftok(PROJ_PATH, ID4SHM); // 获取 SHM 对应的键共享内存
6 key_t key2 = ftok(PROJ_PATH, ID4SEM); // 获取 SEM 对应的键信号量
7
8 // 获取 SHM 的 ID,并将之映射到本进程虚拟内存空间中
9 int shmid = shmget(key1, SHMSZ, IPC_CREAT|0644);
10 char *shmaddr = shmat(shmid, NULL, 0);
11
12 // 获取 SEM 的 ID,若新建则初始化之,否则直接获取其 ID
13 int semid = semget(key2, 2, IPC_CREAT|IPC_EXCL|0644);
14 if(semid == -1 && errno == EEXIST)
15 {
16 semid = semget(key2, 2, 0644); // 直接获取 SEM 的 ID
17 }
18 else
19 {
20 seminit(semid, 0, 0); // 将第 0 个元素初始化为 0,代表数据
21 seminit(semid, 1, 1); // 将第 1 个元素初始化为 1,代表空间
22 }
23
24 while(1)
25 {
26 sem_p(semid, 1); // 向第 1 个信号量元素申请内存空间资源
27 fgets(shmaddr, SHMSZ, stdin); // 从标准输入读取一行数据,并存储到共享内存中
28 sem_v(semid, 0); // 增加代表数据资源的第0个信号量元素的值
29 }
30
31 return 0;
32 }
Rose.c
1 #include "head4sem.h"
2
3 int main(int argc, char **argv)
4 {
5 key_t key1 = ftok(PROJ_PATH, ID4SHM);
6 key_t key2 = ftok(PROJ_PATH, ID4SEM);
7
8 int shmid = shmget(key1, SHMSZ, IPC_CREAT|0644);
9 char *shmaddr = shmat(shmid, NULL, 0);
10
11 int semid = semget(key2, 2, IPC_CREAT|IPC_EXCL|0644);
12 if(semid == -1 && errno == EEXIST)
13 {
14 semid = semget(key2, 2, 0644);
15 }
16 else
17 {
18 seminit(semid, 0, 0);
19 seminit(semid, 1, 1);
20 }
21
22 while(1)
23 {
24 sem_p(semid, 0); // 向第 0 个信号量元素申请数据资源
25 printf("from Jack: %s", shmaddr);
26 sem_v(semid, 1); // 增加代表空间资源的第1个信号量元素的值
27 }
28
29 return 0;
30 }