【操作系统】进程间的通信——信号量

4

进程间的通信-信号量

  • 信号量就类似与马路上的红绿灯,来控制人们在各个路口朝各个方向上的行进,从而更好地有规划的使用这条道路。
  • 在程序中,信号则对进程们的执行进行控制。

什么是信号量

  • 问题:

    • 在程序中,有时会存在一种特殊代码,同一时间只允许一个进程执行该部分代码。这部分区域,被称为"临界区"
    • 然后在多进程并发执行中,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。
  • 解决办法:——使用信号量

  • 什么是信号量?

  • 信号量是一种特殊的变量。

  • 我们只能对信号量执行P操作和V操作。

    • P操作:申请资源。
    • 如果信号量的值>0,则把该信号量-1。
    • 如果信号量的值=0,则挂起该进程。
    • V操作:释放资源。
    • 如果有进程因该信号量而被挂起,则恢复当前进程运行。
    • 如果没有进程因该信号量而被挂起,则把该信号量+1。
  • 注意:

    • P操作、V操作都是原子操作,即,其在执行期间,不会被中断
    • 这里指的信号量是指System V IPC的信号量,与线程所使用的信号量不同。该信号量用于进程间通信

信号量的使用

信号量的获取

  • semget
  • 函数原型:int semget(key_t key, int nsems, int semflg);
  • 功能:获取一个已存在的、或创建一个新的信号量,并返回该信号量的标识符。
  • 参数:
    • key:键值,该键值对应一个唯一的信号量。类似于共享内存的键值。
    • 不同的可通过该键值和semget获取唯一的信号量。
    • 特殊键值——IPC_PRIVAT,该信号量只允许创建者进程才可以访问,可用于父子进程间通信。
    • nsems:需要的信号量数目,一般为1。
    • semflag:访问权限。
      • 若设置为IPC_CREAT,则如果该信号量未存在,则创建该信号量,如果该信号量已经存在,也不会发生错误。
  • 返回值:
    • 成功:返回一个正整数。
    • 失败:返回-1。

信号量的操作

  • semop

  • 函数原型:int semop(int semid, struct sembuf *sops, unsigned nsops);

  • 功能:改变信号量的值,即对信号量执行P操作、V操作。

  • 参数:

    • semid:信号量标识符,即semget函数的返回值。

    • sops:是一个数组,元素类型为struct sembuf。

    • 
      struct sembuf {
         short  sem_num;  //信号量组中的编号(即指定对哪个信号量操作)
                         //semget实际是获取一组信号量
                         //信号量组中的编号从0开始
         short  sem_op;     //操作类型:-1表示P操作;  1表示V操作
         short  sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量         
      }        
    • nsops:表示第二个参数sops所表示的数组大小,即有几个struct sembuf。

  • 返回值:

    • 成功:返回0。
    • 失败:返回-1。
  • 相关参考与补充:Linux进程间通信(五):信号量 semget()、semop()、semctl()


信号量的控制

  • semctl

    • 函数原型:int semctl(int semid, int sem_num, int cmd, ...);

    • 功能:对信号量进行控制。

    • 参数:

    • semid:信号量标识符。

    • sem_num:信号量组中的编号,如果只有一个信号量,则取0。

    • cmd:通常是下面两个值的其中一个。

      • SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
      • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
    • 参数四类型为:union semun

      • union  semun {
                    int     val;      // SETVAL命令要设置的值
                    struct  semid_ds  *buf;
                    unsigned short    *array;
        }
      • union semun有些Linux发行版在sys/sem.h中定义,有些则没有定义,可自行定义:

      • #if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)         #else
        union semun {
        int val;                             
        struct semid_ds *buf;    
        unsigned short int *array; 
        struct seminfo *__buf;  
        };
        #endif     
    • 返回值:略,详见-semctl(2) — Linux manual page

    • 相关参考与补充:Linux进程间通信(五):信号量 semget()、semop()、semctl()


示例

  • 示例1:不使用信号量,并发执行多个程序,观察对临界区的访问。
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int i;
    pid_t pd = fork();
    for (i=0; i<5; i++) {

        /* 模拟临界区----begin */
        printf("Process(%d) In\n", getpid());     
        sleep(1);
        printf("Process(%d) Out\n", getpid());
         /* 模拟临界区----end */ 
        sleep(1);
    }
    return 0;
}

image-20220824164003543

可见并不是我们想要的效果,我们想要的是一个进去了,另外一个就不可以进去了,出去一个,另外一个才可以进去。


  • 示例2:使用信号量,并发指定多个进程,观察对临界区的访问。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>

#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)                         
#else
    union semun {
        int val;                             
        struct semid_ds *buf;    
        unsigned short int *array; 
        struct seminfo *__buf;  
    };
#endif     

//信号的初始化
static sem_initial(int semid){
    int ret;
    union semun semun;
    semun.val = 1;
    ret = semctl(semid,0,SETVAL,semun);
    if(ret == -1){
        fprintf(stderr, "semctl failed!\n");
    }
    return  ret;
}

//将p v操作封装成函数
//p操作
static int sem_p(int semid){
    int ret;
    struct sembuf sembuf;
    sembuf.sem_op = -1;//操作类型,设置为-1即p操作。
    sembuf.sem_num = 0;//指定信号量在信号量组中的编号
    sembuf.sem_flg = SEM_UNDO;//让操作系统跟踪信号,如果进程忘记释放,则操作系统进行负责释放。
    ret = semop(semid,&sembuf,1);//根据设置对信号量进行操作
    if (ret == -1) {
        fprintf(stderr, "sem_p failed!\n");
    }
    return ret;
}
//v操作
static int sem_v(int semid){
    int ret;
    struct sembuf sembuf;
    sembuf.sem_op = 1;//操作类型,设置为1即v操作。
    sembuf.sem_num = 0;//指定信号量在信号量组中的编号
    sembuf.sem_flg = SEM_UNDO;//让操作系统跟踪信号,如果进程忘记释放,则操作系统进行负责释放。
    ret = semop(semid,&sembuf,1);//根据设置对信号量进行操作
    if (ret == -1) {
        fprintf(stderr, "sem_v failed!\n");
    }
    return ret;
}

int main(int argc, char* argv[]) {
    int semid;

    //获取信号
    semid =semget((key_t)1234,1,0666 | IPC_CREAT);
    if(semid == -1){
        printf("semget failed!\n");
        exit(1);
    }
    //信号量的初始化
    if (argc > 1) {
        int ret = sem_initial(semid);
        if (ret == -1) {
            exit(1);
        }
    }
    for(;;){
        if(sem_p(semid) == -1){//p操作,申请,若无可用资源,则挂起等待。
            exit(1);
        }

        /* 模拟临界区----begin */
        printf("Process(%d) In\n", getpid());     
        sleep(3);
        printf("Process(%d) Out\n", getpid());
        /* 模拟临界区----end */ 
        if(sem_v(semid) == -1){//v操作,释放。
            exit(1);
        }
    }

    return 0;
}

image-20220824170933245

可以看到,一个出来,另一个才可以进去。