linux进程间的通信(C): 信号量

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux进程间的通信(C): 信号量相关的知识,希望对你有一定的参考价值。

一、信号量简介
信号量: 用于管理对资源的访问。
荷兰计算机科学家Edsger Dijkstra提出的信号量概念
是在并发编程领域迈出的重要一步。
 
信号量是一个特殊的变量,
它只取正数值,
并且程序对其访问都是原子操作。
 
二、信号量的定义
它是一个特殊变量,
只允许对它进行等待(wait)和发送信号(signal)这两种操作,
 P(信号量变量): 用于等待。
 V(信号量变量): 用于发送信号。
 
这两个字母分别源于荷兰语单词
passeren, 传递,      就好像进入临界区域之前的检查点,(或开信号标志, up);
vrijgeven,给予或释放,就好像放弃对临界区域的控制权,  (或关信号标志,down);
 
P(sv)  如果 sv的值 >  零, 就给它减去1;
       如果 sv的值 == 零, 就挂起该进程的执行.
V(sv)  如果 有其它进程因等待sv而被挂起, 就让它恢复运行;
       如果 没有进程因等待sv而被挂起,   就给它加1.
 
信号量分成:
二进制信号量, 取值只能是0和1;
通用信号量,   可以取多个正整数值;
 
三、工作原理
伪码如下:
  1. semaphore sv = 1;
  2. loop forever {
  3.   noncritical code section;
  4.   P(sv);
  5.   critical code section; 
  6.   V(sv);
  7.   noncritical code section;
  8. }
图示如下:
技术分享
 
四、Linux的信号量机制
所有的Linux信号量函数都是针对成组的通用信号量进行操作,
而不是针对单个的二进制信号量。
 
信号量的函数定义如下所示:

  1. #include <sys/sem.h>
  2. int semget(key_t key, int num_sems, int sem_flags);
  3. int semctl(int sem_id, int sem_num, int command, ...);
  4. int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
1. semget函数
  1. 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函数
用于改变信号量的值。
它的一切动作都是一次性完成的,
这是为了避免出现因使用多个信号量而可能发生的竞争现象。
  1. 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函数
用来直接控制信号量信息。
  1. 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结构,
            至少包含以下成员:
  1. union semun 
  2. {
  3.   int val;
  4.   struct semid_ds *buf;
  5.   unsigned short *array;
  6. }
返回值:
将根据command参数的不同而返回不同的值。
对于SETVAL和IPC_RMID,
成功时,返回0;
失败进,返回-1;
 
五、示例
用两个不同字符的输出来表示进入和离开临界区域。
如果程序启动时带有一个参数,
它将在进入和退出临界区域时打印字符X; 
而程序的其它运行实例将在进入和退出临界区域时打印字符O;
 
因为在任一给定时刻,只能有一个进程可以进入临界区域,
所以字符X和O应该是成对出现的.
 
(1) 调用semget来创建一个信号量
该函数将返回一个信号量标识符;
如果程序是第一个被调用的(也就是说调用时带有一个参数,argc>1),
就调用set_semvalue初始化信号量
并将op_char设置为X:

  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <sys/sem.h>
  5. #include "semun.h"
  6. static int set_semvalue(void);
  7. static void del_semvalue(void);
  8. static int semaphore_p(void);
  9. static int semaphore_v(void);
  10. static int sem_id; // 信号量标识符
  11. int main(int argc, char *argv[])
  12. {
  13.   int i;
  14.   int pause_time;
  15.   char op_char = ‘O’;
  16.   
  17.   srand((unsigned int)getpid());
  18.   
  19.   /* 创建信号量 */
  20.   sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
  21.   if (argc > 1) 
  22.   {
  23.     if (!set_semvalue()) 
  24.     {
  25.       fprintf(stderr, “Failed to initialize semaphore\n”);
  26.       exit(EXIT_FAILURE);
  27.     }
  28.     op_char = ‘X’;
  29.     sleep(2);
  30.   }
(2) 接下来是一个循环,它进入和离开临界区10次。
在每次循环的开始时,
首先调用semaphore_p函数,
它在程序进入临界区域时设置信号量以等待进入:

  1.   for(i = 0; i < 10; i++) 
  2.   {
  3.     if (!semaphore_p()) // P操作, 等待 
  4.       exit(EXIT_FAILURE);
  5.     
  6.     /* 临界区域代码 */
  7.     printf(“%c”, op_char);fflush(stdout);
  8.     pause_time = rand() % 3;
  9.     sleep(pause_time);
  10.     printf(“%c”, op_char);fflush(stdout);
  11.     /* 临界区域结束 */
(3) 在临界区域之后,
调用semaphore_v来将信号量设置为可用,
然后等待一段随机时间,
再进入下一次循环。
在整个循环语句执行完毕后,
调用del_semvalue函数来清理信号量
  1.     if (!semaphore_v()) // V操作,发送信号
  2.       exit(EXIT_FAILURE);
  3.     pause_time = rand() % 2;
  4.     sleep(pause_time);
  5.   } // end of for
  6.   printf(“\n%d - finished\n”, getpid());
  7.   if (argc > 1) 
  8.   {
  9.     sleep(10);
  10.     del_semvalue(); // 清理信号量
  11.   }
  12.   exit(EXIT_SUCCESS);
  13. // end of main
 
(4)函数set_semvalue通过调用semctl的command参数SETVAL,
来初始化信号量。
在使用信号量之前必须这样做。

  1. static int set_semvalue(void)
  2. {
  3.   union semun sem_union;
  4.   sem_union.val = 1;
  5.   if (semctl(sem_id, 0, SETVAL, sem_union) == -1) 
  6.     return(0);
  7.   return(1);
  8. }
 
(5)函数del_smvalue通过调用semctl的command参数IPC_RMID,
来删除信号量ID。
实际编程时一次要在执行结束前进行信号量删除,
以防导致下次程序引用时出错。

  1. static void del_semvalue(void)
  2. {
  3.   union semun sem_union;
  4.   if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
  5.     fprintf(stderr, “Failed to delete semaphore\n”);
  6. }
(6)函数semaphore_p对信号做减1操作(等待):

  1. static int semaphore_p(void)
  2. {
  3.   struct sembuf sem_b;
  4.   sem_b.sem_num = 0;
  5.   sem_b.sem_op = -1; /* P() */
  6.   sem_b.sem_flg = SEM_UNDO;
  7.   if (semop(sem_id, &sem_b, 1) == -1) {
  8.     fprintf(stderr, “semaphore_p failed\n”);
  9.     return(0);
  10.   }
  11.   return(1);
  12. }
 
(7)函数semaphore_v将sembuf结构中的sem_op设置为1(释放):

  1. static int semaphore_v(void)
  2. {
  3.   struct sembuf sem_b;
  4.   sem_b.sem_num = 0;
  5.   sem_b.sem_op = 1; /* V() */
  6.   sem_b.sem_flg = SEM_UNDO;
  7.   if (semop(sem_id, &sem_b, 1) == -1) {
  8.     fprintf(stderr, “semaphore_v failed\n”);
  9.     return(0);
  10.   }
  11.   return(1);
  12. }
 
下面是两个程序调用实例时的一些样本输出:

  1. $ cc sem1.c -o sem1
  2. $ ./sem1 1 &
  3. [1] 1082
  4. $ ./sem1
  5. OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
  6. 1083 - finished
  7. 1082 - finished
  8. $
 
NOTE:
如果程序在系统上执行不正常,
可能需要在程序执行之前执行命令
$stty -tostop
以确保产生tty输出后台程序不会引发系统生成一个信号。

以上是关于linux进程间的通信(C): 信号量的主要内容,如果未能解决你的问题,请参考以下文章

深刻理解Linux进程间通信(IPC)

IPC 进程间通信

进程间的通讯方式有几种?有啥优缺点

Linux C与C++一线开发实践之四 Linux进程间的通信

Linux C与C++一线开发实践之四 Linux进程间的通信

Linux进程间通信 共享内存+信号量+简单样例