一、信号量简介
信号量: 用于管理对资源的访问。
荷兰计算机科学家Edsger Dijkstra提出的信号量概念
是在并发编程领域迈出的重要一步。
信号量是一个特殊的变量,
它只取正数值,
并且程序对其访问都是原子操作。
二、信号量的定义
它是一个特殊变量,
只允许对它进行等待(wait)和发送信号(signal)这两种操作,
P(信号量变量): 用于等待。
V(信号量变量): 用于发送信号。
这两个字母分别源于荷兰语单词
passeren, 传递, 就好像进入临界区域之前的检查点,(或开信号标志, up);
vrijgeven,给予或释放,就好像放弃对临界区域的控制权, (或关信号标志,down);
P(sv) 如果 sv的值 > 零, 就给它减去1;
如果 sv的值 == 零, 就挂起该进程的执行.
V(sv) 如果 有其它进程因等待sv而被挂起, 就让它恢复运行;
如果 没有进程因等待sv而被挂起, 就给它加1.
信号量分成:
二进制信号量, 取值只能是0和1;
通用信号量, 可以取多个正整数值;
三、工作原理
伪码如下:
- semaphore sv = 1;
- loop forever {
- noncritical code section;
- P(sv);
- critical code section;
- V(sv);
- noncritical code section;
- }
图示如下:
四、Linux的信号量机制
所有的Linux信号量函数都是针对成组的通用信号量进行操作,
而不是针对单个的二进制信号量。
信号量的函数定义如下所示:
- #include <sys/sem.h>
- int semget(key_t key, int num_sems, int sem_flags);
- int semctl(int sem_id, int sem_num, int command, ...);
- int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
1. semget函数
- int semget(key_t key, int num_sems, int sem_flags);
参数:
key : 信号量键值,
一个唯一的非零整数。
不相关的进程可以通过它访问同一个信号量。
num_sems: 信号量数目,
它几乎总是取值为1.
sem_flags:一组标志。
它低端的9个比特是该信号量的权限,其作用类似于文件的访问权限。
此外,它还可以和值IPC_CREAT做按位或操作,来创建新的信号量。
可以联合使用标志IPC_CREAT和IPC_EXCL
来确保创建出的是一个新的、唯一的信号量。
如果该信号量已存在,它将返回一个错误。
返回值:
成功时, 返回一个正数(非零)值,
它就是其他信号量函数将用到的信号量标识符.
失败进, 返回-1.
NOTE:
它很类似于文件名, 代表程序可能要使用的某个资源,
如果多个程序使用相同的key值, 它将负责协调工作。
类似于文件的使用情况,
不同的进程可以用不同的信号量标识符(信号量变量名)来指向同一个信号量。
程序对所有的信号量访问都是间接的,
它先提供一个键,
再由系统生成一个相应的信号量标识符。
只有semget函数才直接使用信号量键,
所有其他的信号量函数都是使用由semget函数返回的信号量标识符。
特殊的信号量键值: IPC_PRIVATE,
它的作用是创建一个只有创建者进程才能才可以访问的信号量。
2. semop函数
用于改变信号量的值。
它的一切动作都是一次性完成的,
这是为了避免出现因使用多个信号量而可能发生的竞争现象。
- int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
参数:
sem_id : 由semget返回的信号量标识符。
sem_ops: 指向一个结构数组的指针,
每个数组元素至少包含以下成员:
struct sembuf {
short sem_num;
short sem_op;
short sem_flag;
}
sem_num, 信号量编号,
除非是使用一组信号量,
否则它的取值一般为0, 表示第一个信号。
sem_op , 信号量在一次操作中需要改变是数值,
可以用一个非1的数值来改变信号量的值,
> 0, 这个值为加至semval。
= 0, semop会等到semval降为0,
除非sem_flag参数有设为IPC_NOWAIT。
< 0, 如果 semval >= sem_op的绝对值,
则semval的值会减去sem_op的绝对值。
如果 semval < sem_op的绝对值 且 sem_flag等于IPC_NOWAIT,
则semop()会立即返回错误。
通常只会用到两个值,
-1, 也就是P操作, 它等待信号量变为可用。
+1, 也就是V操作, 它发送信号表示信号量现在可用。
sem_flag,通常被设置成SEM_UNDO。
它将使操作系统跟踪当前进程对这个信号量的修改情况,
如果 这个进程在没有释放信号量的情况下终止,
OS将自动释放该进程持有的信号量。
num_sem_ops: 表示结构数组sem_ops的个数。
若成功,返回0;
否则, 返回-1, 错误原因保存在errno中。
3. semctl函数
用来直接控制信号量信息。
- int semctl(int sem_id, int sem_num, int command, ...);
参数:
sem_id : 由semget返回的信号理标识符。
sem_num: 信号量编号,
当需要用到成组的信号量时,就要用到这个参数,
它一般取值为0,表示这是第一个也是唯一的一个信号量。
command: 将要采取的动作。
最常用的有:
SETVAL: 用来把信号量初始化为一个已知的值。
这个值通过union semun中的val成员设置。
其作用是在信号量第一次使用之前对它进行设置。
IPC_RMID: 用于删除一个已经无需继续使用的信号量标识符。
第四个参数: 是一个union semun结构,
至少包含以下成员:
- union semun
- {
- int val;
- struct semid_ds *buf;
- unsigned short *array;
- }
返回值:
将根据command参数的不同而返回不同的值。
对于SETVAL和IPC_RMID,
成功时,返回0;
失败进,返回-1;
五、示例
用两个不同字符的输出来表示进入和离开临界区域。
如果程序启动时带有一个参数,
它将在进入和退出临界区域时打印字符X;
而程序的其它运行实例将在进入和退出临界区域时打印字符O;
因为在任一给定时刻,只能有一个进程可以进入临界区域,
所以字符X和O应该是成对出现的.
(1) 调用semget来创建一个信号量
该函数将返回一个信号量标识符;
如果程序是第一个被调用的(也就是说调用时带有一个参数,argc>1),
就调用set_semvalue初始化信号量
并将op_char设置为X:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/sem.h>
- #include "semun.h"
- static int set_semvalue(void);
- static void del_semvalue(void);
- static int semaphore_p(void);
- static int semaphore_v(void);
- static int sem_id; // 信号量标识符
- int main(int argc, char *argv[])
- {
- int i;
- int pause_time;
- char op_char = ‘O’;
-
- srand((unsigned int)getpid());
-
- /* 创建信号量 */
- sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
- if (argc > 1)
- {
- if (!set_semvalue())
- {
- fprintf(stderr, “Failed to initialize semaphore\n”);
- exit(EXIT_FAILURE);
- }
- op_char = ‘X’;
- sleep(2);
- }
(2) 接下来是一个循环,它进入和离开临界区10次。
在每次循环的开始时,
首先调用semaphore_p函数,
它在程序进入临界区域时设置信号量以等待进入:
- for(i = 0; i < 10; i++)
- {
- if (!semaphore_p()) // P操作, 等待
- exit(EXIT_FAILURE);
-
- /* 临界区域代码 */
- printf(“%c”, op_char);fflush(stdout);
- pause_time = rand() % 3;
- sleep(pause_time);
- printf(“%c”, op_char);fflush(stdout);
- /* 临界区域结束 */
(3) 在临界区域之后,
调用semaphore_v来将信号量设置为可用,
然后等待一段随机时间,
再进入下一次循环。
在整个循环语句执行完毕后,
调用del_semvalue函数来清理信号量
- if (!semaphore_v()) // V操作,发送信号
- exit(EXIT_FAILURE);
- pause_time = rand() % 2;
- sleep(pause_time);
- } // end of for
- printf(“\n%d - finished\n”, getpid());
- if (argc > 1)
- {
- sleep(10);
- del_semvalue(); // 清理信号量
- }
- exit(EXIT_SUCCESS);
- } // end of main
(4)函数set_semvalue通过调用semctl的command参数SETVAL,
来初始化信号量。
在使用信号量之前必须这样做。
- static int set_semvalue(void)
- {
- union semun sem_union;
- sem_union.val = 1;
- if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
- return(0);
- return(1);
- }
(5)函数del_smvalue通过调用semctl的command参数IPC_RMID,
来删除信号量ID。
实际编程时一次要在执行结束前进行信号量删除,
以防导致下次程序引用时出错。
- static void del_semvalue(void)
- {
- union semun sem_union;
- if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
- fprintf(stderr, “Failed to delete semaphore\n”);
- }
(6)函数semaphore_p对信号做减1操作(等待):
- static int semaphore_p(void)
- {
- struct sembuf sem_b;
- sem_b.sem_num = 0;
- sem_b.sem_op = -1; /* P() */
- sem_b.sem_flg = SEM_UNDO;
- if (semop(sem_id, &sem_b, 1) == -1) {
- fprintf(stderr, “semaphore_p failed\n”);
- return(0);
- }
- return(1);
- }
(7)函数semaphore_v将sembuf结构中的sem_op设置为1(释放):
- static int semaphore_v(void)
- {
- struct sembuf sem_b;
- sem_b.sem_num = 0;
- sem_b.sem_op = 1; /* V() */
- sem_b.sem_flg = SEM_UNDO;
- if (semop(sem_id, &sem_b, 1) == -1) {
- fprintf(stderr, “semaphore_v failed\n”);
- return(0);
- }
- return(1);
- }
下面是两个程序调用实例时的一些样本输出:
- $ cc sem1.c -o sem1
- $ ./sem1 1 &
- [1] 1082
- $ ./sem1
- OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
- 1083 - finished
- 1082 - finished
- $
NOTE:
如果程序在系统上执行不正常,
可能需要在程序执行之前执行命令
$stty -tostop
以确保产生tty输出后台程序不会引发系统生成一个信号。