一.概述
System V信号量与System V消息队列不同。它不是用来在进程间传递数据。它主要是来同步进程的动作。
1.一个信号量是一个由内核维护的整数。其值被限制为大于或等于0。
2.可以在信号量上加上或减去一个数量。
3.当一个减操作把信号量减到小于0时,内核会阻塞调用进程。直到另一操作把信号恢复,阻塞才会解除。
4.常用的信号量是二进制信号量。即操作0和1来控制临界区。
二.函数接口
1.创建或打开一个信号量
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
key和semflg跟消息队列一样,这里不再介绍,上面已给出消息队列的文章连接。
nsems:指定信号量数目,一般都是1。
2.控制信号量
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...);
semid:semget()返回的信号量标识符。
semnum:信号量编号,如果是成组的信号量,就要用到它,否则是0。
cmd:控制信号量的命令。SETVAL初始化一个值,IPC_RMID删除信号量。
第四个参数是一个union semun结构。如果有的Linux版本头文件没有这个结构,需要自己定义:
union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ };
3.改变信号量的值
#include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops);
sops:指向一个sembuf结构,该结构成员如下:
unsigned short sem_num:信号量编号,如果不是一组信号,一般都为0。
short sem_op:对信号量加减操作,如:-1,+1,1。
short sem_flg:通常设置为SEM_UNDO。如果进程终止时没有释放该信号量,内核会释放它。
三.简单例子
我们封装一个简单的二进制信号量,写2个小程序,一个只打印基数,一个只打印偶数,通过二进制信号量来控制它们按顺序打印1-10。
1.封装二进制信号量
/** * @file binary_sem.h */ #ifndef _BINARY_SEM_H_ #define _BINARY_SEM_H_ union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ }; /* 设置信号量 */ int sem_set(int sem_id); /* 增大信号量 */ int sem_up(int sem_id); /* 减小信号量 */ int sem_down(int sem_id); /* 删除信号量 */ int sem_delete(int sem_id); #endif
/** * @file binary_sem.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/sem.h> #include "binary_sem.h" static union semun sem_union; static struct sembuf sem_buf; /* 设置信号量 */ int sem_set(int sem_id) { sem_union.val = ; , SETVAL, sem_union) == -) ; ; } /* 改变信号量为1 */ int sem_up(int sem_id) { sem_buf.sem_num = ; sem_buf.sem_flg = SEM_UNDO; sem_buf.sem_op = ; ) == -) ; ; } /* 信号量减1 */ int sem_down(int sem_id) { sem_buf.sem_num = ; sem_buf.sem_flg = SEM_UNDO; sem_buf.sem_op = -; ) == -) ; ; } /* 删除信号量 */ int sem_delete(int sem_id) { , IPC_RMID, sem_union) == -) ; ; }
2.打印基数的程序
/** * @file sem1.c */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/sem.h> #include <unistd.h> #include "binary_sem.h" #define SEM_KEY 7788 void err_exit(const char *err_msg) { printf("error: %s\n", err_msg); exit(); } int main(int argc, const char *argv[]) { int sem_id, i; /* 创建信号 */ , | IPC_CREAT)) == -) err_exit("semget()"); /* 初始化信号为1 */ ) err_exit("sem_set()"); ; i < ; i += ) { /* 信号减一 */ ) err_exit("sem_down()"); sleep(); printf(], i); /* 信号加一 */ ) err_exit("sem_up()"); sleep(); } ; }
对信号量加减操作之间的代码就是临界区。程序第39行的sleep()是为了有足够的时间来允许第二个程序,46行的sleep()是尽量让其他程序来获取信号量。
3.只打印偶数的程序
/** * @file sem2.c */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/sem.h> #include "binary_sem.h" #define SEM_KEY 7788 void err_exit(const char *err_msg) { printf("error: %s\n", err_msg); exit(); } int main(int argc, const char *argv[]) { int sem_id, i; /* 创建信号 */ , | IPC_CREAT)) == -) err_exit("semget()"); ; i <= ; i += ) { /* 信号减一 */ ) err_exit("sem_down()"); printf(], i); /* 信号加一 */ ) err_exit("sem_up()"); sleep(); } ; }
四.实验
1.为了测试方便,上面2个程序的key统一用一个值为7788的宏来指定。
2.两个程序执行的流程:sem1初始化一个为1的信号量,进入循环后对信号执行减一,并进入临界区。休眠3秒的时间我执行第二个程序sem2,sem2进入循环后也执行减一操作,但此时信号已被sem1减为0,此时内核会阻塞sem1的操作,因为再减就小于0了。回到sem1,3秒过后,打印基数,并把信号重置为1,离开临界区,休眠1秒。此时内核发现信号是1,可以执行减操作,内核会对sem2放行,sem2进入临界区打印偶数。刚刚休眠1秒的sem1再次进入临界区,发现信号量被sem2减到了0,此时被内核阻塞,直到sem2重置信号量。如此反复!!!
最后:其实System V信号量的语义和API都比较复杂,有机会再探讨比它简单的POSIX信号量。