1. 基础知识


    1.1 什么是信号量

       信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。


        1.2  为什么要有信号量

生动做个比如:

  以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

  在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。


   1.3 信号量的特性

  信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。


 1.4 信号量的分类

        a.二进制信号量(binary semaphore):只允许信号量取0或1值,其同时只能被一个线程获取。  
      b.整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。  
      c.记录型信号量(record semaphore):每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线
程,让其获得信号量,同时信号量再减一。  


2. 相关函数

 1)创建

semget()

wKiom1cQ9y2z51I2AAAjiZdSI5k235.png

wKiom1cTd-HQHcGOAAAo-83vabU527.png

 2)操作

semop

wKioL1cQ_bGA92LRAAAk-GkfwSk094.png

wKioL1cTeTiD9EDfAAAhXosOgU0698.png

结构体

wKioL1cQ_riwsVXtAAA7w9NsEsA857.png

  3)清理

semctl

wKiom1cRBd7RjqCRAAAfYahkHH0765.png

wKiom1cTeRGjKT8zAAAqFL9ItOI384.png

3.代码实现

 //sem.h
  1 #ifndef __SEM__
  2 #define __SEM__
  3 
  4 #include<stdio.h>
  5 #include<stdlib.h>
  6 #include<sys/ipc.h>
  7 #include<sys/types.h>
  8 #include<sys/sem.h>
  9 #include<errno.h>
 10 #include<unistd.h>
 11 #include<string.h>
 12 
 13 #define PATH "."
 14 #define PROJ_ID 77
 15 
 16 static int comm_sem(int flag);
 17 int create_sem(int semset_num);
 18 int get_sem();
 19 int init_sem(int sem_id,int which);
 20 static int op_sem(int sem_id,int op,int which);
 21 int sem_p(int sem_id,int which);
 22 int sem_v(int sem_id,int which);
 23 int destroy_sem(int sem_id);
 24 
 25 #endif
 
 //sem.c
  1 #include "sem.h"
  2 
  3 #define nsems 1
  4 
  5 static int comm_sem(int flag)
  6 {
  7     key_t _key=ftok(PATH,PROJ_ID);
  8     if(_key<0)
  9     {
 10         perror("ftok");
 11         return -1;
 12     }
 13     int sem_id=semget(_key,nsems,flag);
 14     return sem_id;
 15 }
 16 int create_sem(int semset_num)
 17 {
 18     int flag=IPC_CREAT|IPC_EXCL;
 19     return comm_sem(flag);
 20 }
 21 int get_sem()
 22 {
 23     int flag=IPC_CREAT;
 24     return comm_sem(flag);
 25 }
 26 
 27 int init_sem(int sem_id,int which)
 28 {
 29      union semun
 30      {
 31         int val;   /* Value for SETVAL */
 32         struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
 33         unsigned short *array;   /* Array for GETALL, SETALL */
 34            struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
 35      }semun;
 36     semun.val=1;
 37     int ret=semctl(sem_id,which,SETVAL,semun);
 38     if(ret==-1)  //failure
 39     {
 40         perror("semctl");
 41         return -1;
 42     }
 43     else
 44         return ret;
 45 }
 46 static int op_sem(int sem_id,int op,int which)
 47 {
 48 //  struct sembuf 
 49 //  {
 50 //      unsigned short sem_num;
 51 //      short sem_op;
 52 //      short sem_flg;    //SEM_UNDO
 53 //  }sem;
 54     struct sembuf sem[nsems];
 55     memset(&sem,'\0',sizeof(sem));
 56     sem[0].sem_num=which;
 57     sem[0].sem_op=op;
 58     sem[0].sem_flg=0;
 59     if (semop(sem_id,&sem,1)<0)
 60     {
 61         perror("semop");
 62         return -1;
 63     }
 64     return 0;
 65 }
 66 int sem_p(int sem_id,int which)
 67 {
 68     int ret=op_sem(sem_id,-1,which);
 69     if(ret==0)
 70     {
 71 //      printf("P operation is success! errno code is:%d\n",errno);
 72     }
 73     else
 74     {
 75 //      printf("P operation is failed! errno code is:%d\n",errno);
 76     }
 77     return ret;
 78 }
 79 int sem_v(int sem_id,int which)
 80 {
 81     int ret=op_sem(sem_id,1,which);
 82     if(ret==0)
 83     {
 84 //      printf("V operation is success! errno code is:%d\n",errno);
 85     }
 86     else
 87     {
 88 //      printf("V operation is failed! errno code is:%d\n",errno);
 89     }
 90     return ret;
 91 }
 92 int destroy_sem(int sem_id)
 93 {
 94     int ret=semctl(sem_id,0,IPC_RMID,NULL);
 95     if(ret==-1)
 96     {
 97         perror("semctl");
 98         return -1;
 99     }
100     else
101         return ret;
102 }

//test.c
  1 #include "sem.h"
  2 int main()
  3 {
  4     int sem_id=create_sem(1);
  5     init_sem(sem_id,0);
  6     pid_t id=fork();
  7     if(id<0)
  8     {
  9         perror("fork");
 10         return -1;
 11     }
 12     else if(id==0)  //child
 13     {
 14         int _sem_id=get_sem();
 15         while(1)
 16         {
 17             sem_p(_sem_id,0);
 18 
 19             printf("A");
 20             sleep(1);
 21             fflush(stdout);
 22 
 23             printf("A");
 24             sleep(3);
 25             fflush(stdout);
 26 
 27             sem_v(_sem_id,0);
 28         }
 29     }
 30     else  //father
 31     {
 32         int _sem_id=get_sem();
 33         while(1)
 34         {
 35             sem_p(_sem_id,0);
 36 
 37             printf("B");
 38             sleep(1);
 39             fflush(stdout);
 40 
 41             printf("B");
 42             sleep(2);
 43             fflush(stdout);
 44 
 45             sem_v(_sem_id,0);
 46         }
 47         waitpid(id,NULL,0);
 48     }
 49     destroy_sem(sem_id);
 50     return 0;
 51 }
 
 //makefile
  1 test:test.c sem.c
  2     gcc -o $@ $^
  3 .PHONY:clean
  4 clean:
  5     rm -f test

输出结果:

wKiom1cSTmey4deBAAB7xbYB1d0389.png

结果分析:

   1.A和B成对出现,不会交叉出现。保证了原子操作,即要么做,要么不做,不会做到一半被阻碍,如不存在只打印一个A或B再打印另一个不相同的字母

   2.实现了控制作用


4.相关小知识

sem.c中的sem_op函数中结构体sembuf中的一个变量sem_flg注释为SEM_UNDO,下面就解释一下它的作用

     a.   IPC_UNDO标志保证进程终止后,它对信号量的修改都撤销,好像它从来没有操作过信号量一样。

     b.   UNDO标志就是用来撤销该进程对指定信号量操作的,目的就是为了防止当一个进程获取到一个临界资源后,突然死掉,让其他要使用该资源的进程永远处于等待状态

     举个例子就是,一个进程使用semop()函数对一个信号量的计数器的值增加了1,把另一个信号量的计数器的值减2,该函数又指定了UNDO标志,当进程退出时,就会对第一个信号量减1,对第2个信号量加2。而其他进程对这2个信号量所做的改变却任然有效。
      其实,每个内核对IPC信号量资源所执行的可撤销操作,都存放在一个叫sem_undo的数据结构中,比如上个例子中semop()做的+1,-2这两个操作,在进程结束时,内核就做了一个sem_undo的数据结构中记录的数值的反向操作,来达到IPC信号量计数器回滚的目的

     c.   一般来说二值信号量(用于互斥),为了防止进程意外退出引起的问题,需要设置SEM_UNDO标志。
但是其他的信号量(比如生产者消费者问题中empty和full信号量), 就不能设置SEM_UNDO标志。

     d.   UNIX环境高级编程中有如下描述:

“如果指定了undo标志,则也从该进程的此型号量调整值中减去sem_op“(见425页)