一.概述
System V信号量与System V消息队列不同。它不是用来在进程间传递数据。它主要是来同步进程的动作。
1.一个信号量是一个由内核维护的整数。其值被限制为大于或等于0。
2.可以在信号量上加上或减去一个数量。
3.当一个减操作把信号量减到小于0时,内核会阻塞调用进程。直到另一操作把信号恢复,阻塞才会解除。
4.常用的信号量是二进制信号量。即操作0和1来控制临界区。
二.函数接口
1.创建或打开一个信号量
1 #include <sys/sem.h> 2 3 int semget(key_t key, int nsems, int semflg);
key和semflg跟消息队列一样,这里不再介绍,上面已给出消息队列的文章连接。
nsems:指定信号量数目,一般都是1。
2.控制信号量
1 #include <sys/sem.h> 2 3 int semctl(int semid, int semnum, int cmd, ...);
semid:semget()返回的信号量标识符。
semnum:信号量编号,如果是成组的信号量,就要用到它,否则是0。
cmd:控制信号量的命令。SETVAL初始化一个值,IPC_RMID删除信号量。
第四个参数是一个union semun结构。如果有的Linux版本头文件没有这个结构,需要自己定义:
1 union semun { 2 int val; /* Value for SETVAL */ 3 struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ 4 unsigned short *array; /* Array for GETALL, SETALL */ 5 struct seminfo *__buf; /* Buffer for IPC_INFO 6 (Linux-specific) */ 7 };
3.改变信号量的值
1 #include <sys/sem.h> 2 3 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.封装二进制信号量
1 /** 2 * @file binary_sem.h 3 */ 4 5 #ifndef _BINARY_SEM_H_ 6 #define _BINARY_SEM_H_ 7 8 union semun { 9 int val; /* Value for SETVAL */ 10 struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ 11 unsigned short *array; /* Array for GETALL, SETALL */ 12 struct seminfo *__buf; /* Buffer for IPC_INFO 13 (Linux-specific) */ 14 }; 15 16 /* 设置信号量 */ 17 int sem_set(int sem_id); 18 /* 增大信号量 */ 19 int sem_up(int sem_id); 20 /* 减小信号量 */ 21 int sem_down(int sem_id); 22 /* 删除信号量 */ 23 int sem_delete(int sem_id); 24 25 #endif
1 /** 2 * @file binary_sem.c 3 */ 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <sys/sem.h> 9 10 #include "binary_sem.h" 11 12 static union semun sem_union; 13 static struct sembuf sem_buf; 14 15 16 /* 设置信号量 */ 17 int sem_set(int sem_id) 18 { 19 sem_union.val = 1; 20 if (semctl(sem_id, 0, SETVAL, sem_union) == -1) 21 return -1; 22 return 0; 23 } 24 25 /* 改变信号量为1 */ 26 int sem_up(int sem_id) 27 { 28 sem_buf.sem_num = 0; 29 sem_buf.sem_flg = SEM_UNDO; 30 sem_buf.sem_op = 1; 31 if (semop(sem_id, &sem_buf, 1) == -1) 32 return -1; 33 return 0; 34 } 35 36 /* 信号量减1 */ 37 int sem_down(int sem_id) 38 { 39 sem_buf.sem_num = 0; 40 sem_buf.sem_flg = SEM_UNDO; 41 sem_buf.sem_op = -1; 42 if (semop(sem_id, &sem_buf, 1) == -1) 43 return -1; 44 return 0; 45 } 46 47 /* 删除信号量 */ 48 int sem_delete(int sem_id) 49 { 50 if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) 51 return -1; 52 return 0; 53 }
2.打印基数的程序
1 /** 2 * @file sem1.c 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <stdlib.h> 8 #include <sys/sem.h> 9 #include <unistd.h> 10 11 #include "binary_sem.h" 12 13 #define SEM_KEY 7788 14 15 void err_exit(const char *err_msg) 16 { 17 printf("error: %s\n", err_msg); 18 exit(1); 19 } 20 21 int main(int argc, const char *argv[]) 22 { 23 int sem_id, i; 24 25 /* 创建信号 */ 26 if ((sem_id = semget(SEM_KEY, 1, 0666 | IPC_CREAT)) == -1) 27 err_exit("semget()"); 28 29 /* 初始化信号为1 */ 30 if (sem_set(sem_id) == -1) 31 err_exit("sem_set()"); 32 33 for (i = 1; i < 10; i += 2) 34 { 35 /* 信号减一 */ 36 if (sem_down(sem_id) == -1) 37 err_exit("sem_down()"); 38 39 sleep(3); 40 printf("%s:%d\n", argv[0], i); 41 42 /* 信号加一 */ 43 if (sem_up(sem_id) == -1) 44 err_exit("sem_up()"); 45 46 sleep(1); 47 } 48 return 0; 49 }
对信号量加减操作之间的代码就是临界区。程序第39行的sleep()是为了有足够的时间来允许第二个程序,46行的sleep()是尽量让其他程序来获取信号量。
3.只打印偶数的程序
1 /** 2 * @file sem2.c 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <stdlib.h> 8 #include <unistd.h> 9 #include <sys/sem.h> 10 11 #include "binary_sem.h" 12 13 #define SEM_KEY 7788 14 15 void err_exit(const char *err_msg) 16 { 17 printf("error: %s\n", err_msg); 18 exit(1); 19 } 20 21 int main(int argc, const char *argv[]) 22 { 23 int sem_id, i; 24 25 /* 创建信号 */ 26 if ((sem_id = semget(SEM_KEY, 1, 0666 | IPC_CREAT)) == -1) 27 err_exit("semget()"); 28 29 for (i = 2; i <= 10; i += 2) 30 { 31 /* 信号减一 */ 32 if (sem_down(sem_id) == -1) 33 err_exit("sem_down()"); 34 35 printf("%s:%d\n", argv[0], i); 36 37 /* 信号加一 */ 38 if (sem_up(sem_id) == -1) 39 err_exit("sem_up()"); 40 41 sleep(1); 42 } 43 return 0; 44 }
四.实验
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信号量。