知识点:
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) ;#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl (int semid, int semnum, int cmd, union semun arg) ;#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop (int semid, struct sembuf *sops, size_t nsops) ;
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 ) { 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 #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) ;int sem_p (int sem_id) ;int sem_v (int sem_id) ;#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); 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 ) { 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 #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) ;int sem_p (int sem_id) ;int sem_v (int sem_id) ;#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
从上图中可以看到,两个进程之间交替进行,顺序很有规律,表示我们使用信号量的控制是有效的,在后面的共享内存通信中也会用到信号量作为进程之间的同步机制,所以这一章希望大家好好学习理解和掌握。