知识点:

  • Linux 信号量通信原理介绍
  • Linux 信号量通信相关 API 介绍

0x01 Linux 信号量概述

在 Linux 中,信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

正如前面实验三章节内容的说明一样,在 Linux 系统下解决进程间临界资源竞争的问题,最有效的方式就是使用信号量。所以,信号量就是用来解决进程间的同步和互斥问题的一种进程间通信机制。

_信号量与信号的区别_:在 Linux 系统下,信号量名字是 semaphore ,而信号的名字为 signal。它们虽然都可以用来解决进程同步和互斥问题,但是所采用的的机制不一样。信号使用系统的信号处理机制,而信号量则最终是使用原子操作的 PV 来实现的。

0x02 Linux 信号量工作原理

在多任务操作系统环境下,多个进程会同时运行,并且一些进程间可能会存在一定的关联。多个进程可能为了完成同一个任务相互协作,这就形成了进程间的同步关系。而且在不同进程间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程间的互斥关系。

进程间的互斥关系与同步关系存在的根源在于临界资源。临界资源是在同一时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器及其它外围设备等)和软件资源(共享代码段、共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会称为临界资源。

信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P/V 操作)。其中,信号量对应于某一种资源,取一个非负的整形值。信号量值(常用 sem_id 表示)指的是当前可用的该资源的数量,若等于 0 则意味着目前没有可用的资源。

PV 原子操作的具体定义如下:

  • P 操作:如果有可用的资源(信号量值 > 0),则此操作所在的进程占用一个资源(此时信号量值减 1,进入临界区代码);如果没有可用的资源(信号量值 = 0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
  • V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(即信号量值加 1)。

_二元信号量_:二元信号量(Binary Semaphore)是最简单的一种锁(互斥锁),它只用两种状态:占用与非占用。所以它的引用计数为 1。

0x03 Linux 信号量相关 API 函数

在 Linux 系统中,使用信号量通常分为以下 4 个步骤:

  • 创建信号量或获得在系统中已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
  • 初始化信号量,此时使用 semctl() 函数的 SETVAL 操作。当使用二维信号量时,通常将信号量初始化为 1。
  • 进行信号量的 PV 操作,此时,调用 semop() 函数。这一步是实现进程间的同步和互斥的核心工作部分。
  • 如果不需要信号量,则从系统中删除它,此时使用 semctl() 函数的 IPC_RMID 操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
/*
参数:
key:信号量的键值,多个进程可以通过它访问一个信号量,其中有个特殊值 IPC_PRIVATE ,用来创建当前进程私有信号量
nsems:需要创建的信号量数目
semflg:同 open() 函数的权限位相同,其中 IPC_CREAT 创建新的信号量,IPC_EXCL 创建新的唯一的信号量
返回值:
成功:信号量标识符,在信号量其他函数中会用到 失败:返回 -1
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, union semun arg);
/*
参数:
semid:semget() 函数反悔的信号量标识符,要操作的信号量
semnum:信号量编号,当使用信号量集会用到,单个信号量操作取参数 0
cmd:指定对信号量的各种操作,当使用单个信号量时,常用的操作如下:
IPC_STAT:获得该信号量的 semid_ds 结构,并存放到第四个参数指定的位置中
IPC_SETVAL:将信号量值设置为 arg 的 val 值
IPC_GETVAL:返回信号量的当前值
IPC_RMID:从系统中删除信号量(或者信号量集)
arg:是 union semun 结构,有时需要执行定义
返回值:
成功:根据 cmd 的值返回不同的值
失败:-1
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
/*
参数:
semid:semget() 函数反悔的信号量标识符,要操作的信号量
sops:指向信号量操作数组,
nsops:操作数组 sops 中的操作个数,通常取 1 (一个操作)
返回值:
成功:信号量标识符 失败:-1
*/

0x04 信号量基本用法

在 VS Code 平台下创建新的代码文件 fork_demo.c ,如下图所示:

然后添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define DELAY_TIME 3


union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};


int init_sem(int sem_id, int init_val)
{
union semun sem_union;
sem_union.val = init_val;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("init semaphore error.\n");
return -1;
}
return 0;
}

int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
// perror("Delete semaphore error.\n");
return -1;
}
return 0;
}

int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}

int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}



int main(int argc, char *argv[])
{
pid_t pid;
int sem_id;
//创建信号量
sem_id = semget(ftok(".",'a'), 1, 0666|IPC_CREAT);
//初始化信号量
init_sem(sem_id, 0);

pid = fork();
if(pid < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
//子进程
printf("Child progress [%d] will wait for som seconds...\n", getpid());
sleep(DELAY_TIME);
printf("Child progress [%d] \n", getpid());
sem_v(sem_id);
exit(1);
}
else
{
//父进程
sem_p(sem_id);
printf("Parent progress [%d] will wait for som seconds...\n", getpid());
sem_v(sem_id);
del_sem(sem_id);
}

waitpid(pid, NULL, 0);
return 0;
}

在 VS Code 平台下的终端窗口使用 gcc 编译代码并执行,如下图所示:

0x05 信号量进程同步

接下来利用信号量同步机制创建两个进程交替进行输出计数,一个进程输出奇数,另一个输出偶数,交替输出组成联系数字排列。为了使得输出结果能够显示在同一个终端窗口,使其中的一个进程通过 fork() 创建新的进程并调用 execl() 函数去执行另一个可执行程序,最终达到我们想要的效果。具体如下:

在 VS Code 平台下创建新的代码文件偶数输出程序代码 even_demo.c ,如下图所示:

然后添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* 该程序将仅输出奇数值,配合 even_demo 程序完成 0 - 20 的计数输出*/

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>


#define DELAY_TIME 3

union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};

//信号量初始化
int init_sem(int sem_id, int init_value);

//从系统中删除信号量
int del_sem(int sem_id);

//P操作
int sem_p(int sem_id);

//V操作
int sem_v(int sem_id);

//定义ftok参数
#define SEM_ODD_KEY "./odd_dem_key"
#define SEM_EVEN_KEY "./even_dem_key"

int main()
{
pid_t pid;
int sem_odd, sem_even;

//创建一个信号量
sem_even = semget(ftok(SEM_EVEN_KEY, 'a'), 1, 0666|IPC_CREAT);
sem_odd = semget(ftok(SEM_ODD_KEY, 'a'), 1, 0666);
init_sem(sem_even, 0);

pid = fork();
if(pid < 0)
{
perror("fork error.");
exit(0);
}
if(pid == 0)
{
execl("./odd_demo", "test", NULL);
exit(0);
}

for(int i = 0; i < 20; i = i + 2)
{
sem_p(sem_odd); //获取奇数程序信号
printf("[%d]====> (%d)\n", getpid(), i);
sem_v(sem_even);
}

sem_p(sem_even);
// sleep(DELAY_TIME);
waitpid(pid, NULL, 0);

del_sem(sem_even);
printf("prograss even[%d] quit..\n", getpid());
exit(0);
}

int init_sem(int sem_id, int init_val)
{
union semun sem_union;
sem_union.val = init_val;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("init semaphore error.\n");
return -1;
}
return 0;
}

int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
// perror("Delete semaphore error.\n");
return -1;
}
return 0;
}

int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}

int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}

然后继续新建代码文件 odd_demo.c ,这是用于输出奇数消息的程序,如下图所示:

然后添加如下代码内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/* 该程序将仅输出奇数值,配合 even_demo 程序完成 0 - 20 的计数输出*/

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define DELAY_TIME 3

union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};

//信号量初始化
int init_sem(int sem_id, int init_value);

//从系统中删除信号量
int del_sem(int sem_id);

//P操作
int sem_p(int sem_id);

//V操作
int sem_v(int sem_id);

//定义ftok参数
#define SEM_ODD_KEY "./odd_dem_key"
#define SEM_EVEN_KEY "./even_dem_key"

int main()
{
pid_t pid;
int sem_odd, sem_even;

//创建一个信号量
sem_odd = semget(ftok(SEM_ODD_KEY, 'a'), 1, 0666|IPC_CREAT);
sem_even = semget(ftok(SEM_EVEN_KEY, 'a'), 1, 0666);
init_sem(sem_odd, 0);

sem_v(sem_odd);
for(int i = 1; i < 20; i = i + 2)
{
sem_p(sem_even); //获取偶数信号量
printf("[%d]====> (%d)\n", getpid(), i);
sem_v(sem_odd);
}

sleep(DELAY_TIME);
del_sem(sem_odd);
printf("prograss odd[%d] quit..\n", getpid());
return 0;
}

int init_sem(int sem_id, int init_val)
{
union semun sem_union;
sem_union.val = init_val;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("init semaphore error.\n");
return -1;
}
return 0;
}

int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
perror("Delete semaphore error.\n");
return -1;
}
return 0;
}

int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}

int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;

if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P error.\n");
return -1;
}
return 0;
}

在 VS Code 平台下的终端窗口使用 gcc 编译代码并执行,如下图所示:

1
2
3
gcc odd_demo.c -o odd_demo
gcc even_demo.c -o even_demo
./even_demo # 由于 odd_demo 程序在 eve_demo 中采用 execl 方式执行,所以不需要手动执行 odd_demo ,仅执行 even_demo 即可(从偶数 0 开始计数)

从上图中可以看到,两个进程之间交替进行,顺序很有规律,表示我们使用信号量的控制是有效的,在后面的共享内存通信中也会用到信号量作为进程之间的同步机制,所以这一章希望大家好好学习理解和掌握。