进程间通信 IPC 主题二之 信号量

时间:2022-10-05 15:14:33

IPC主题二:信号量

信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识,信号量在此过程中负责数据操作的互斥,同步等功能。


一:为什么要使用信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。


二:信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复
执行。


三:linux的信号机制

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

注:信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。

函数:

信号量函数由semget、semop、semctl三个函数组成。

semget : 作用是创建一个新信号量或取得一个已有信号量。
函数原型:int semget(key_t key, int num_sems, int sem_flags);
参数
key:信号量集标识符(成功返回非零唯一标识码,失败返回 -1)。
num_sems : 指定需要的信号量数目, 一般情况下为1。
sem_flags
IPC_CREAT : 当想要当信号量不存在时创建一个新的信号量。
IPC_CREAT | IPC_EXCL : 创建一个新的唯一的信号量,如果信号量已存在,返回一个错误。

semop : 作用是改变信号量的值。
函数原型:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
参数
sem_id : semget成功返回的信号标识码。
sembuf结构:

01struct sembuf{  
02. short sem_num;//除非使用一组信号量,否则它为0
03. short sem_op;
04. //在一次操作中需要改变的数据,通常是两个数,P(等待 -1), V(发送+1).
05. short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
06. //并在进程没有释放该信号量而终止时,操作系统释放信号量
07.};

num_sem_ops : 进行操作信号量的个数,即sembuf结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作。

semctl : 该函数用来直接控制信号量信息。
函数原型:
int semctl(int sem_id, int sem_num, int command, ...);
参数
sem_id : 信号量集标识符。
sem_num : 指定需要的信号量数目, 一般情况下为1。
commond:有以下两种可能

(1)SETVAL:用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

(2)IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

下面我们来用代码测试一下使用信号量进行进程间通信。

首先是未加信号量的父子进程:

  1 #include "comm.h"
2 int main()
3 {
4 pid_t id = fork();//创建子进程
5 if(id < 0)
6 {
7 perror("fork");
8 return -1;
9 }
10 else if(0 == id)
11 {
12 //child
13 while(1)
14 {
15 printf("A");
16 fflush(stdout);
17 usleep(500000);
18 printf("A");
19 fflush(stdout);
20 usleep(500000);
21 }
22 }
23 else
24 {
25 //father
26 while(1)
27 {
28 usleep(200000);
29 printf("B");
30 fflush(stdout);
31 usleep(500000);
32 printf("B");
33 fflush(stdout);
34 usleep(500000);
35 }
36 pid_t ret = waitpid(id, NULL, 0);
37 if(ret > 0)
38 {
39 printf("proc is done\n");
40 }
41 }
42 return 0;
43 }

运行结果:
进程间通信 IPC 主题二之 信号量

可以看出父子进程随机向屏幕上输出字符,这些字符毫无顺序,所以证明这一操作并不是原子的。

接下来是加了信号量的代码:

  1 #ifndef _COMM_H_
2 #define _CONN_H_
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 #include <sys/ipc.h>
8 #include <sys/sem.h>
9
10 #define PATHNAME "."
11 #define PROJ_ID 0
12
13 union semun
14 {
15 int val;
16 struct semid_ds* buf;
17 unsigned short* array;
18 struct seminfo* _buf;
19 };
20
21 int creatSems(int nums);
22 int getSem();//获取信号量
23 int destorySems(int semid);
24 int initSems(int semid, int which, int value);
25 int P(int semid, int which);
26 int V(int semid, int which);
27
28 #endif

comm.c

  1 #include "comm.h"
2 static int commSems(int nsems, int semflag)
3 {
4 key_t _key = ftok(PATHNAME, PROJ_ID);
5 if(_key < 0)
6 {
7 perror("ftok");
8 return -1;
9 }
10 int semid = semget(_key, nsems, semflag);
11 if(semid < 0)
12 {
13 perror("semget");
14 return -2;
15 }
16 return semid;
17 }
18
19 int creatSems(int nums)
20 {
21 return commSems(nums, IPC_CREAT | IPC_EXCL | 0666);
22 }
23
24 //将semid对应的信号量集中下标为which的信号量初始化value
25 int initSems(int semid, int which, int value)
26 {
27 union semun _un;
28 _un.val = value;
29 int ret = semctl(semid, which, SETVAL, _un);
30 if(ret < 0)
31 {
32 perror("initSem");
33 return -1;
34 }
35 return 0;
36 }
37
38 static int commSemOp(int semid, int which, int op)
39 {
40 struct sembuf _buf;
41 _buf.sem_num = which;
42 _buf.sem_op = op;
43 _buf.sem_flg = 0;
44 return semop(semid, &_buf, 1);
45 }
46
47 int P(int semid, int which)
48 {
49 return commSemOp(semid, which, -1);
50 }
51
52 int V(int semid, int which)
53 {
54 return commSemOp(semid, which, +1);
55 }
56
57 int getSems()
58 {
59 return commSems(0, IPC_CREAT);
60 }
61
62 int destorySems(int semid)
63 {
64 if(semctl(semid, 0, IPC_RMID, NULL) < 0)
65 {
66 perror("destory");
67 return -1;
68 }
69 return 0;
70 }

test_sem.c

  1 #include "comm.h"
2 int main()
3 {
4 int semid = creatSems(1);
5 //初始化信号量集
6 initSems(semid, 0, 1);
7 pid_t id = fork();
8 if(id == 0)
9 {
10 //child
11 int semid = getSems();//获取信号量
12 while(1)
13 {
14 P(semid, 0);
15 printf("A");
16 fflush(stdout);
17 usleep(500000);
18 printf("A");
19 fflush(stdout);
20 usleep(500000);
21 V(semid, 0);
22 }
23 }
24 else
25 {
26 //father
27 while(1)
28 {
29 P(semid, 0);
30 usleep(500000);
31 printf("B");
32 fflush(stdout);
33 usleep(500000);
34 printf("B");
35 fflush(stdout);
36 usleep(50000);
37 V(semid, 0);
38 }
39
40 pid_t ret = waitpid(id, NULL, 0);
41 if(ret > 0)
42 {
43 printf("proc is done");
44 }
45 }
46 return 0;
47 }

运行结果:
进程间通信 IPC 主题二之 信号量

通过运行结果可以清晰的看到父子进程每次输出的两个字符都是同时输出的,并没有像之前那样杂乱无章,这也正说明了信号量使得两个进程之间的通信保持原子性。