System V 信号量

System V 信号量

1.1 基本的API介绍

semget
  • 函数功能:创建或者打开一个信号量集合

  • 返回值:成功返回信号量集合的标识符,错误返回-1,并且设置errno值

  • 函数原型:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    int semget(key_t key, int nsems, int semflg);
    
  • 参数:

    1. key:一个键值,和消息队列生成键值的方式一样
    2. nsems:
      • 当创建一个信号量集合的时候:该参数表示信号量集合中信号量的个数
      • 当获取一个既有标识符的信号量集合的时候,该值必须小于或者等于集合的大小,否则返回EINVAL错误
    3. semflg:一个位掩码,相关参数如下,可以用OR或起来:
      • IPC_CREATE
      • IPC_EXCL
semctl
  • 函数功能:在信号量集合或者当中的单个信号量执行相关控制操作

  • 返回值: On failure semctl() returns -1 with errno indicating the error.
    ​ Otherwise the system call returns a nonnegative value depending on cmd

  • 函数原型:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
    
  • 参数:

    1. semid:信号量集合的标识符

    2. semnum:对某一个信号量集合中单个信号量操作的时候,指明了相关信号量的序号,置0表示忽视该参数

    3. cmd:指定了相关控制操作,常见参数如下:

      • IPC_RMID:立即删除信号量集以及相关关联数据结构,所有因为调用semop而等待这个信号量集的进程都会被立即唤醒,semop会报EIDRM错误,该参数忽略arg参数和semnum参数
    4. …:是一个可选参数,是一个联合体,定义如下,不同的unix实现,导致有些版本这个参数不是可选的(原型:int semctl(int semid, int semnum, int cmd, union semun arg);),所以为了保证移植性能,不使用这个参数的时候,需要填写一个dummy的哑参数

      /*the fourth has the type union semun.  The calling  pro‐
             gram must define this union as follows:*/
      
                 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) */
                 };
      

1.2 关联数据结构介绍

struct semid_ds {
     struct ipc_perm sem_perm;  /* Ownership and permissions */
     time_t          sem_otime; /* Last semop time */
     time_t          sem_ctime; /* Last change time */
     unsigned short  sem_nsems; /* No. of semaphores in set 信号集中实际的信号量数量*/
};

1.3 初始化信号集

  • SUSv3要求semget() 不需要对信号量进行初始化,初始化操作应该由程序员手动通过semctl() 系统调用来完成,但是这个时候可能会出现竞争问题,比如进程A和进程B同时执行以下代码:

    #include <sys/types.h>
    #include <sys/sem.h>
    #include <sys/stat.h>
    #include "semun.h"                      /* Definition of semun union */
    #include "tlpi_hdr.h"
    
    int main(int argc, char *argv[])
    {
        int semid, key, perms;
        struct sembuf sops[2];
    
        key = 12345;
        perms = S_IRUSR | S_IWUSR;
        semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
    
        if (semid != -1) {                  /* Successfully created the semaphore */
            union semun arg;
    
            /* XXXX */             /*进程B打断了进程A的执行*/                 
    
            arg.val = 0;                    /* So initialize it */
            if (semctl(semid, 0, SETVAL, arg) == -1)
                errExit("semctl");
    
        } else {                            /* We didn't create semaphore set */
            if (errno != EEXIST) {          /* Unexpected error from semget() */
                errExit("semget 1");
            } else {                        /* Someone else already created it */
                semid = semget(key, 1, perms);      /* So just get ID */
                if (semid == -1)
                    errExit("semget 2");
            }
        }
        /* Now perform some operation on the semaphore */
    
        sops[0].sem_op = 1;         /* Add 1 */
        sops[0].sem_num = 0;        /* ... to semaphore 0 */
        sops[0].sem_flg = 0;
        if (semop(semid, sops, 1) == -1)
            errExit("semop");
    
        exit(EXIT_SUCCESS);
    }
    
    1. 出现问题如下图:

    System V 信号量

    1. 解决办法:semid_ds数据结构中有一个sem_otime 字段,信号量集被创建的时候,会被初始化为0,后续的semop() 会修改这个值的大小,所以进程B中加入一段代码,检测sem_otime 字段,只有这个字段值改变了之后,进程B才进行其它操作,但是如果可以保证信号量集创建和初始化不会被打断,可以不用这么做,相关操作代码如下:

      #include <sys/types.h>
      #include <sys/sem.h>
      #include <sys/stat.h>
      #include "semun.h"                      /* Definition of semun union */
      #include "tlpi_hdr.h"
      
      int
      main(int argc, char *argv[])
      {
          int semid, key, perms;
          struct sembuf sops[2];
      
          if (argc != 2 || strcmp(argv[1], "--help") == 0)
              usageErr("%s sem-op\n", argv[0]);
      
          key = 12345;
          perms = S_IRUSR | S_IWUSR;
      
          semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
      
          if (semid != -1) {                  /* Successfully created the semaphore */
              union semun arg;
              struct sembuf sop;
      
              sleep(5);
              printf("%ld: created semaphore\n", (long) getpid());
      
              arg.val = 0;                    /* So initialize it to 0 */
              if (semctl(semid, 0, SETVAL, arg) == -1)
                  errExit("semctl 1");
              printf("%ld: initialized semaphore\n", (long) getpid());
      
              /* Perform a "no-op" semaphore operation - changes sem_otime
                 so other processes can see we've initialized the set. */
      
              sop.sem_num = 0;                /* Operate on semaphore 0 */
              sop.sem_op = 0;                 /* Wait for value to equal 0 */
              sop.sem_flg = 0;
              if (semop(semid, &sop, 1) == -1)
                  errExit("semop");
              printf("%ld: completed dummy semop()\n", (long) getpid());
      
          } else {                            /* We didn't create the semaphore set */
      
              if (errno != EEXIST) {          /* Unexpected error from semget() */
                  errExit("semget 1");
      
              } else {                        /* Someone else already created it */
                  const int MAX_TRIES = 10;
                  int j;
                  union semun arg;
                  struct semid_ds ds;
      
                  semid = semget(key, 1, perms);      /* So just get ID */
                  if (semid == -1)
                      errExit("semget 2");
      
                  printf("%ld: got semaphore key\n", (long) getpid());
                  /* Wait until another process has called semop() */
      
                  arg.buf = &ds;
                  for (j = 0; j < MAX_TRIES; j++) {
                      printf("Try %d\n", j);
                      if (semctl(semid, 0, IPC_STAT, arg) == -1)
                          errExit("semctl 2");
      
                      if (ds.sem_otime != 0)          /* Semop() performed? */
                          break;                      /* Yes, quit loop */
                      sleep(1);                       /* If not, wait and retry */
                  }
      
                  if (ds.sem_otime == 0)              /* Loop ran to completion! */
                      fatal("Existing semaphore not initialized");
              }
          }
      
          /* Now perform some operation on the semaphore */
      
          sops[0].sem_num = 0;                        /* Operate on semaphore 0... */
          sops[0].sem_op = getInt(argv[1], 0, "sem-op");
          sops[0].sem_flg = 0;
          if (semop(semid, sops, 1) == -1)
              errExit("semop");
      
          exit(EXIT_SUCCESS);
      }
      

1.4 信号量操作

semop
  • 函数功能:系统调用在semid 标识符的信号量集上进行一个或者多个原子操作

  • 返回值:返回0表示成功,错误返回1

  • 函数原型:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    int semop(int semid, struct sembuf *sops, unsigned nsops);
    
  • 参数:

    1. semid :信号量集的标识符
    2. sops :指向一个数组
    3. nsops :指出了sops 指向的数组包含的元素个数
  • struct sembuf 数据结构介绍:

struct sembuf
{
     unsigned short sem_num;  /* semaphore number(信号集中单个信号量的序号) */
     short          sem_op;   /* semaphore operation */
     short          sem_flg;  /* operation flags */
}
  • sem_op 参数选项介绍:
    1. sem_op 大于0:调用进程具备在信号量上面的写权限。将sem_op 值加到信号量上面,等待减少该信号量值得进程会被唤醒
    2. sem_op 等于0:调用进程具备在信号量上面的读权限。调用进程检查信号量值是否为0,如果等于0,操作立即结束,否则阻塞到等于0为止
    3. sem_op 小于0:调用进程具备在信号量上面的写权限。当前信号量的值减去sem_op 大于等于0,操作结束,否则阻塞进程知道操作之后不会把信号量变为负值为止
  • sem_flg :指定IPC_NOWAIT ,本来semop() 会阻塞,但是指定了这个标志之后,会返回EAGAIN错误
  • sem_op 调用阻塞被唤醒的一般情况:
    1. 等待的信号量发生了变化
    2. 一个信号中断了sem_op() ,返回EINTR错误
    3. 信号量被删除,返回EIDRM错误
  • 注意
    1. semop 可以同时操作信号集中多个信号量,但是存在一个问题,semop 要么立即执行,要么阻塞到能够对信号集中的所有信号量操作为止
    2. 多个进程同时对一个信号量进行操作的时候,一定要避免某一个进程出现饥饿现象
      • 信号量1的value为0,进程A打算在信号量减1,进程B也打算在信号量1上减1,当信号量变为1的时候,进程A和进程B谁先执行了,这个主要根据内核的进程调度算法解决了
      • 当进程A和B对信号量减去的值不相等的时候,假如进程A减2,进程B减1,当信号量值变为1的时候,不管如何进程B都会先执行,主要是为了满足,先满足条件先执行的顺序执行进程
semtimedop
  • 函数功能:相比于semop 多了一个阻塞时间设置参数timeout ,如果timeout 设置为NULL 那么就和semop 没有差别了
  • 函数原型:
int semtimedop(int semid, struct sembuf *sops, unsigned nsops,
               struct timespec *timeout);

  • 返回值:成功返回1,错误返回-1
  • 参数:
    1. timeout:表示可以指定一个时间,在信号量操作完成之前,如果超过这个时间,函数返回EAGAIN错误,如果置为0,函数就和semop 功能一样了

1.5 信号量撤销值

  • semop() 在修改一个信号量值得时候可以使用SEM_UNDO 标记,当指定了这个标记,内核会记录信号量操作的效果,然后再进程终止的时候,会撤销semop() 操作

1.6 System V信号量的限制

  • 信号量通过标识符来标识,而其它大多数I/O操作和IPC采用文件描述符来表示
  • 使用键值而不是文件名来生成标识符,增加了编程复杂度
  • 创建和初始化信号量是分开的操作,可能会出现竞争问题,增加了编程的复杂度
  • 内核不会维护一个引用该信号量集的进程数量,所以会造成删除了一个信号量集的不确定因素
  • 编程接口过于复杂
  • 信号量操作存在许多限制
上一篇:c信号量demo


下一篇:Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)