14.1.4 使用信号量
下面将用完整的编程接口为二进制信号量创建一个简单得多的PV类型接口,然后用这个非常简单的接口来演示信号量是如何工作的。用 程序sem1.c来试验信号量,该程序可以被多次调用。通过一个可选的参数来指定程序是负责创建信号量还是负责删除信号量。
用两个不同字符的输出来表示进入和离开临界区域。如果程序启动时带有一个参数,它将在进入和退出临界区域时打印字符X;而程序的其他运行实例将在进入和退出临界区域时打印字符O。因为在任意时刻,只能有一个进程可以进入临界区域,所以字符X和O应该是成对出现的。
信号量
编写程序sem1.c
/*************************************************************************
> File Name: sem1.c
> Description: sem1.c程序被用来试验信号量,该程序可以被多次调用,通过一个可选的参数来指定程序是负责创建信号量还是负责删除信号量
> Author: Liubingbing
> Created Time: 2015年07月17日 星期五 21时41分23秒
> Other: sem1.c程序用两个不同字符的输出来表示进入和离开临界区域.
如果程序启动时带有一个参数,它将在进入和退出临界区域时打印字符X;而程序的其他示例将在进入和退出临界区域时打印O
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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());
/* semget函数的作用是创建一个新信号量或取得一个已有信号量的键
* 第一个参数key是整数值,不相关的进程可以通过它访问同一个信号量
* 第二个参数指定需要的信号量数目,几乎总是取1
* 第三个参数是一组标志,与open函数的标志非常类似,作用类似于文件的访问权限
* 如果成功则返回值信号量标识符(可以被其他信号量函数使用) */
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
/* 如果程序是第一个被调用的(也就是说它在被调用时带有一个参数,使得argc>1) */
if (argc > 1) {
/* set_semvalue函数初始化信号量 */
if (!set_semvalue()) {
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
/* 打印字符为'X' */
op_char = 'X';
sleep(2);
}
/* 下面这个循环,程序进入和离开临界区域10次,在每次循环的开始,调用semaphore_p函数在程序进入临界区域时设置信号量以等待进入 */
for (i = 0; i < 10; i++) {
/*semaphore_p函数对信号量做减1操作(等待),如果成功则返回1,如果失败则返回0 */
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);
fflush(stdout);
/* 在临界区域之后,调用semaphore_v函数来将信号量设置为可用 */
if (!semaphore_v())
exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (argc > 1) {
sleep(10);
/* del_semvalue函数清理代码 */
del_semvalue();
}
exit(EXIT_SUCCESS);
}
/* set_semvalue函数通过semctl调用的command参数设置SETVAL来初始化信号量
* semctl函数用来直接控制信号量信息 */
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
/* semctl函数用来直接控制信号量信息
* 第一个参数sem_id是由semget返回的信号量标识符
* 第二个参数是信号量编号,一般取值为0
* 第三个参数command是将要采取的动作,SETVAL标志用来把信号量初始化为一个已知的值,这个值通过union semun中的val成员设置
* 第四个参数是一个union semun结构
* 如果成功返回0,如果失败返回-1 */
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
/* del_semvalue函数通过将semctl调用的command参数设置为IPC_RMID来删除信号量ID */
static void del_semvalue(void)
{
union semun sem_union;
/* semctl函数用来直接控制信号量信息
* 第一个参数sem_id是由semget返回的信号量标识符
* 第三个参数command是将要采取的动作,IPC_RMID表示用于删除一个已知无需继续使用的信号量标识符*/
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
/* semaphore_p函数对信号量做减1操作(等待) */
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0; /* sem_num是信号量编号,一般取值为0 */
sem_b.sem_op = -1; /* sem_op是信号量在一次操作中需要改变的值,通常是-1和1,-1即P操作等待信号量变为可用,1即V操作表示信号量已可用*/
sem_b.sem_flg = SEM_UNDO; /* sem_flg一般设置为SEM_UNDO,它将使得操作系统跟踪当前进程对这个信号量的修改情况 */
/* semop函数用于改变信号量的值
* 第一个参数sem_id是由semget返回的信号量标识符
* 第二个参数是指向一个结构数组的指针 */
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
/* semaphore_v函数对信号量做加1操作(可用)*/
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; /* sem_op是信号量在一次操作中需要改变的值,1即V操作,表示信号量已可用 */
sem_b.sem_flg = SEM_UNDO;
/* semop函数用于改变信号量的值 */
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
编写程序semun.h
/*************************************************************************
> File Name: semun.h
> Description: semun.h程序定义了联合semun
> Author: Liubingbing
> Created Time: 2015年07月17日 星期五 21时48分04秒
> Other: semun.h
************************************************************************/
#ifndef _SEMUN_H
#define _SEMUN_H
#endif
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* union semun is defined by including <sys/sem.h> */
#else
/* 联合semun是semctl函数的第四个参数
* semctl函数用来直接控制信号量信息 */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
#endif
sem1.c在包含了必须的系统头文件之后,包含头文件semun.h,如果系统头文件sys/sem.h没有定义X/Open规范所需的联合semun,这个头文件包含了对它的定义。然后是函数原型的声明和全局变量的定义,调用函数semget来创建一个信号量,该函数将返回一个信号量标识符。如果程序是第一个被调用的(也就是说它在被调用时带有一个参数,使得argc>1),就调用set_semvalue初始化信号量并将op_char设置为X。
这个简单的程序值允许每个程序有一个二进制信号量,虽然可以通过传递信号量变量的方法来扩展它可以支持更多的信号量,但通常一个二进制信号量即已足够。
可以通过多次启动这个程序的方法来对它进行测试,第一次启动时加上一个参数,表示应该由它来负责创建和删除信号量,其他的调用实例不使用参数。
下面是两个程序调用实例时的一些输出:
字符"O"和"X"分别代表程序的第一个和第二个调用实例。 因为每个程序都在其进入和离开临界区域时打印一个字符,所以每个字符都应该成对出现。如上图所示,字符O和X都是成对出现的,这表明对临界区域的处理是正确的。如果这个程序在系统上不能正常工作,可能需要在启动程序之前执行命令stty -tostop,以确保不会产生tty输出的后台程序不会引发系统生成一个信号。
程序分析
在程序的开始,用semget函数通过一个(随机选取的)键来取得一个信号量标识符。IPC_CREAT标志的作用是:如果信号量不存在,就创建它。
如果程序带有一个参数,它就负责信号量的初始化工作,这是通过set_semvalue函数来完成的,该函数是针对更通用的semctl函数的简化接口.程序还将根据是否带有参数来决定需要打印哪个字符.sleep函数的作用是,有时间在这个程序实例执行太多循环之前调用其他的程序实例.用函数srand和rand来为程序引入一些伪随机形式的时间分配.
接下来程序循环10次,在临界区域和和非临界区域会分别暂停一段随机的时间.临界区域由semaphore_p和semaphore_v函数前后把守,它们是更通用的semop函数的简化接口.
删除信号量之前,带有参数启动的程序会进入等待状态,以允许其他调用实例都执行完毕.如果不删除信号量,它将继续在系统中存在,即使没有程序在使用它也是如此.