Unix/Linux编程:System V信号量

System V信号量不是用来在进程间传输数据的,而是用来同步进程的动作。信号量的一个常见用途是同步对一块共享内存的访问以防止出现一个进程在访问共享内存的同时另一个进程更新这块内存的情况

一个信号量是一个由内核维护的整数,其值被限制为大于或者等于0。在一个信号量上可以执行各种操作(即系统调用),包括:

  • 将一个信号设置成一个绝对值
  • 在信号量当前值的基础上加上一个数量
  • 在信号量当前值的基础上减去一个数量
  • 等待信号量的值等于 0。

上面操作中的后两个可能会导致调用进程阻塞。当减小一个信号量的值时,内核会将所有试图将信号量值降低到 0 之下的操作阻塞。类似的,如果信号量的当前值不为 0,那么等待信号量的值等于 0 的调用进程将会发生阻塞。不管是何种情况,调用进程会一直保持阻塞直到其他一些进程将信号量的值修改为一个允许这些操作继续向前的值,在那个时刻内核会唤醒被阻塞的进程。下图显示了使用一个信号量来同步两个交替将信号量的值在 0 和 1 之间切换的进程的动作
在这里插入图片描述
在控制进程的动作方面,信号量本身并没有任何意义,它的意义仅由使用信号量的进程赋予其的关联关系来确定。一般来讲,进程之间会达成协议将一个信号量与一种共享资源关联起来,如一块共享内存区域。信号量还有其他用途,如在 fork()之后同步父进程和子进程

概述

使用System V信号量的常规步骤如下:

  • 使用semget()创建或者打开一个信号量集
  • 使用semctl() SETVAL 或 SETALL 操作初始化集合中的信号量(只有一个进程需要完成这个任务)
  • 使用semop()操作信号量的值。使用信号量的进程通常会使用这些操作来标识一种共享资源的获取和释放
  • 当所有进程都不再需要使用信号量集之后semctl() IPC_RMID操作删除这个集合(只有一个进程需要完成这个任务)。

大多数操作系统都为应用程序提供了一些信号量原语。但System V信号量表项出了不同寻常的复杂性,因为它们的分配是以被称为信号量集的组为单位进行的。在使用semget()系统调用创建集合的时候需要指定集合中的信号量数量。虽然同一时刻通常只会操作一个信号量,但通过semop()系统调用可以原子的在同一个集合中的多个信号量之上执行一组操作。

semget:创建或者打开一个信号量集

semget()系统调用创建一个新信号量集或者获取一个既有集合的标识符

NAME
       semget - get a System V semaphore set identifier

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semget(key_t key, int nsems, int semflg);

key 参数是使用这里描述的方法之一生成的一个键(即通常是值 IPC_PRIVATE 或ftok()返回的一个键)

nsems: 不是指资源的数量,而是表示可以创建多少个信号量,形成一组信号量,也就是说,如果你有多种信号需要管理,可以创建一个信号量组

  • 如果semget()创建一个新信号集,那么nsems会指定集合中信号量的数量,并且其值必须大于0
  • 如果semget()是获取一个既有集的标识符,那么nsems必须小于等于集合的大小(否则会发生 EINVAL 错误)。无法修改一个既有集中的信号量数量

semflg是一个位掩码,它指定了施加于新信号量集之上的权限或需检查一个既有集合的权限。指定权限的方式与为文件指定权限的方式是一样的。此外,在 semflg中可以通过对下列标记中的零个或多个取 OR 来控制 semget()的操作

  • IPC_CREAT :如果不存在与指定的 key 相关联的信号量集,那么就创建一个新集合。
  • IPC_EXCL :如果同时指定了 IPC_CREAT 并且与指定的 key 关联的信号量集已经存在,那么返回EEXIST 错误。

semget()在成功时会返回新信号量集中既有信号量集的标识符。后继引用单个信号量的系统调用必须要同时指定信号量标识符和信号量在集合中的序号。一个集合中的信号量从0开始计数。

semctl:信号量控制操作

semctl()系统调用在一个信号量集或集合中的单个信号量上执行各种控制操作

NAME
       semctl - System V semaphore control operations

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semctl(int semid, int semnum, int cmd, ... /* union semun arg*/);

semid:操作所施加的信号量集的标识符

semnum:

  • 对于那些在单个信号量上执行的操作,标识处了集合中的具体信号量
  • 对于其他操作则会忽略这个参数,并且可以将其设置为 0

cmd:指定要执行的操作。

一些特定的操作需要向 semctl()传入第四个参数arg,这个操作是一个union。在程序中必须要显示的定义这个union:

#ifndef SEMUN_H
#define SEMUN_H                 /* Prevent accidental double inclusion */

#include <sys/types.h>          /* For portability */
#include <sys/sem.h>

#if ! defined(__FreeBSD__) && ! defined(__OpenBSD__) && \
                ! defined(__sgi) && ! defined(__APPLE__)
                /* Some implementations already declare this union */

union semun {                   /* Used in calls to semctl() */
    int                 val;   /* Value for SETVAL */
    struct semid_ds *   buf;   /* Buffer for IPC_STAT, IPC_SET */
    unsigned short *    array;  /* Array for GETALL, SETALL */
#if defined(__linux__)
    struct seminfo *    __buf;  /* Buffer for IPC_INFO(Linux-specific) */
#endif
};

#endif

#endif
  • 虽然将 semun union 的定义放在标准头文件中是比较明智的做法,但 SUSv3 要求程序员显式地定义这个 union。然而,一些 UNIX 实现在<sys/sem.h>中提供了这个定义。glibc较早以前的版本(2.0 以下,包括 2.0)也提供了这个定义。为了与 SUSv3 保持一致,glibc最近的版本并没有提供这个定义,并且通过将<sys/sem.h>中_SEM_SEMUN_UNDEFINED宏的值定义为 1 来表明这个事实(即使用 glibc 编译的应用程序可以通过测试这个宏来确定程序自己是否需要定义 semun union)。
  • 但一些(主要是较早之前的)UNIX 实现(以及 glibc 的早期版本)第四个参数是必需的。为使程序能够完全可移植,在那些无需最后一个参数的 semctl()调用中需要传入一个哑参数。

下面介绍一些通过cmd 参数可指定的各种控制操作
(1)常规控制操作(所有这些操作都会忽略semnum 参数)

  • IPC_RMID:
    • 立即删除信号量集及其关联的semid_ds 数据结构
    • 所有因在semop()调用中等待这个集合中的信号量而阻塞的进程都会立即被唤醒,semop()会报告错误EIDRM。这个操作无需 arg 参数
  • IPC_STAT :arg.buf 指向的缓冲器中放置一份与这个信号量集相关联的 semid_ds 数据结构的副本。
  • IPC_SET:使用 arg.buf 指向的缓冲器中的值来更新与这个信号量集相关联的 semid_ds 数据结构中选中的字段。
    (2)获取和初始化信号量值:
  • GETVAL:semctl()返回由semid指定的信号量集中第semnum个信号量的值。这个操作无需 arg 参数
  • SETVAL:将由 semid 指定的信号量集中第 semnum 个信号量的值初始化为 arg.val。
  • GETALL:
    • 获取由semid指向的信号量集中所有信号量的值并将它们放在arg.array指向的数组中。
    • 程序员必须要确保该数组具备足够的空间(通过由IPC_STAT操作返回的semid_ds数据结构中的sem_nsems字段可以获取集合中的信号量数量)。
    • 这个操作将忽略 semnum 参数
  • SETALL:
    • 使用 arg.array 指向的数组中的值初始化 semid 指向的集合中的所有信号量。这个操作将忽略 semnum 参数

获取一个信号量的值需具备在信号量上的读权限,而初始化该值则需要修改(写)权限

如果存在一个进程正在等待在由 SETVAL 或 SETALL 操作所修改的信号量上执行一个操作并且对信号量所做的变更将允许该操作继续向前执行,那么内核就会唤醒该进程。

使用SETVAL 或 SETALL 修改一个信号量的值会在所有进程中清除该信号量的撤销条目。

注意 GETVAL 和 GETALL 返回的信息在调用进程使用它们时可能已经过期了。所有依赖由这些操作返回的信息保持不变这个条件的程序都可能会遇到检查时(time-of-check)和使用时(time-of-use)的竞争条件

(3)获取单个信号量的信息

  • GETPID :
    • 返回上一个在该信号量上执行 semop()的进程的进程 ID;这个值被称为 sempid 值
    • 如果还没有进程在该信号量上执行过 semop(),那么就返回 0
  • GETNCNT :返回当前等待该信号量的值增长的进程数;这个值被称为 semncnt 值
  • GETZCNT :返回当前等待该信号量的值变成 0 的进程数;这个值被称为 semzcnt 值。

这些操作返回(通过函数结果值)semid 引用的集合中第 semnum 个信号量的信息。所
有这些操作都需要在信号量集合中具备读权限,并且无需 arg 参数

与上面介绍的 GETVAL 和 GETALL 操作一样,GETPID、GETNCNT 以及 GETZCNT 操作返回的信息在调用进程使用它们时可能已经过期了。

信号量关联数据结构

每个信号量集都有一个关联的semid_ds数据结构,其形式如下:

/* Data structure describing a set of semaphores.  */
struct semid_ds
{
  struct ipc_perm sem_perm;		/* operation permission struct */
  time_t sem_otime;			/* last semop() time */
  time_t sem_ctime;			/* last time changed by semctl() */
  unsigned long sem_nsems;		/* number of semaphores in set */
};


struct ipc_perm
  {
    __key_t __key;			/* Key.  */
    __uid_t uid;			/* Owner's user ID.  */
    __gid_t gid;			/* Owner's group ID.  */
    __uid_t cuid;			/* Creator's user ID.  */
    __gid_t cgid;			/* Creator's group ID.  */
    unsigned short int mode;		/* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;		/* Sequence number.  */
    unsigned short int __pad2;
  };

各种信号量系统调用会隐式地更新 semid_ds 结构中的字段,使用 semctl() IPC_SET 操作能够显式地更新 sem_perm 字段中的特定子字段,其细节信息如下。

  • sem_perm:创建信号量时会初始化这个子结构中的字段。通过IPC_SET能够更新uid、gid、mode子字段
  • sem_otime:
    • 创建信号量集时会将这个字段设置为0,然后每次成功的semop()调用或者当信号量指应该SEM_UNDO操作而发生变更时将这个字段设置为当前时间
    • 这个字段和sem_ctime 的类型为 time_t,它们存储自新纪元到现在的秒数。
  • sem_ctime:在创建信号量时以及每个成功的 IPC_SET、SETALL 和 SETVAL 操作执行完毕之后将这个字段设置为当前时间。(在一些UNIX 实现上,SETALL 和SETVAL 操作不会修改sem_ctime。
  • sem_nsems :在创建集合时将这个字段的值初始化为集合中信号量的数量。

semop:信号量操作

semop()系统调用在 semid 标识的信号量集中的信号量上执行一个或多个操作

NAME
       semop, semtimedop - System V semaphore operations

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semop(int semid, struct sembuf *sops, unsigned nsops);

       int semtimedop(int semid, struct sembuf *sops, unsigned nsops,
                      struct timespec *timeout);


  • id:信号量组的id,一次可以操作多个信号量
  • sops:
    • 它是一个指向数组的指针
    • 有多少个操作,就将这些操作放到这个数组中。操作的类型是struct sembuf,具体见下面
    • 操作将会按照在数组中的顺序以原子的方式被执行
  • nsops :
    • sops数组的大小
    • (数组至少需包含一个元素)
struct sembuf
{
  unsigned short int sem_num;	// 信号量组中对应的序号,0~sem_nums-1
  short int sem_op;		 // 信号量值在一次操作中的改变量
  short int sem_flg;	 // IPC_NOWAIT, SEM_UNDO
};
  • sem_num:这个操作的对象是哪个信号量。
  • sem_op :要对这个信号量做多少改变
    • 如果sem_op大于0:
      • 表示进程归还相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则唤醒它们。
      • 调用进程必须要具备在信号量上的修改(写)权限。
    • 如果 sem_op 等于 0,那么就对信号量值进行检查以确定它当前是否等于 0。
      • 如果等于0,那么操作将立即结束,否则 semop()就会阻塞直到信号量值变成 0 为止
      • 调用进程必须要具备在信号量上的读权限
    • 如果 sem_op 小于 0:
      • 表示请求资源。也就是说需要将信号量值减去 sem_op的绝对值。
      • 如果相应的资源数可以满足请求( 如果信号量的当前值大于或等于 sem_op 的绝对值),则将该信号量的值减去 sem_op 的绝对值,函数马上返回。
      • 当相应的资源数不能满足请求时,就要看 sem_flg 了
        • 如果把 sem_flg 设置为 IPC_NOWAIT,也就是没有资源也不等待,则 semop 函数出错返回 EAGAIN。
        • 如果 sem_flg 没有指定 IPC_NOWAIT,则进程挂起(阻塞),直到:
          • 当相应的资源数可以满足请求(其他进程归还了资源)
          • 一个信号中断了 semop()调用。发生这种情况时会返回 EINTR 错误(semop()在被一个信号处理器中断之后是不会自动重启的)
          • 另一个进程删除了semid 引用的信号量。发生这种情况时semop()会返回EIDRM 错误。
      • 调用进程必须要具备在信号量上的修改权限

从语义上来讲,增加信号量值对应于使一种资源变得可用以便其他进程可以使用它,而减小信号量值则对应于预留(互斥地)进程需使用的资源。在减小一个信号量值时,如果信号量的值太低——即其他一些进程已经预留了这个资源——那么操作就会被阻塞。

尽管通常一次只会操作一个信号量,但也可以通过一个 semop()调用在一个集合中的多个信号量上执行操作。这里需要指出的关键一点是这组操作的执行是原子的,即 semop()要么立即执行所有操作,要么就阻塞直到能够同时执行所有操作。

semtimedop()系统调用与 semop()执行的任务一样,但它多了一个 timeout 参数,通过这个参数可以指定调用所阻塞的时间上限

  • 如果在信号量操作完成之前所等待的时间已经超过了规定的时间间隔,那么 semtimedop()会返回 EAGAIN 错误。
  • 如果将 timeout 指定为 NULL,那么semtimedop()就与 semop()完全一样了。

信号量和共享内存都比较复杂,两者还要结合起来用,就更加复杂,它们内核的机制就更加复杂。

多个阻塞信号量操作的处理

如果多个因减少一个信号量值而发生阻塞的进程对该信号量减去的值是一样的,那么当条件允许是到底哪个进程会先被允许执行操作是不确定的、

另一方面,如果多个因减少一个信号量而发生阻塞的进程对该信号量减去的值是不同的,那么会按照先满足条件先满足的顺序来进行:

  • 假设一个信号量的当值为 0,进程 A 请求将信号量值减去 2,然后进程 B 请求将信号量值减去 1。如果第三个进程将信号量值加上了 1,那么进程 B 首先会被解除阻塞并执行它的操作
  • 在一个糟糕的应用程序设计中,这种场景可能会导致饿死情况的发生,即一个进程因信号量的状态无法满足所请求的操作继续往前执行的条件而永远保持阻塞。

当一个进程因试图在多个信号量上执行操作而发生阻塞时也可能会出现饿死的情况。考虑下面的这些在一组信号量上执行的操作,两个信号量的初始值都为 0

  • 进程 A 请求将信号量 0 和 1 的值减去 1(阻塞)。
  • 进程 B 请求将信号量 0 的值减去 1(阻塞)
  • 进程 C 将信号量 0 的值加上 1

此刻,进程 B 解除阻塞并完成了它的请求,即使它发出请求的时间要晚于进程 A。

信号量撤销值

假设一个进程在调整完一个信号量值之后主动或被动的终止了,这样可能会给其他使用这个信号量的进程带来问题,因为它们可能因等待这个信号量而被阻塞着——即等待已经被终止的进程撤销对信号量所做的变更

为了避免这种问题的发生,在通过semop()修改一个信号量值时可以使用SEM_UNDO标记。当指定这个标记时,内核会记录信号量操作的效果,然后在进程终止(主动或被动)时撤销这个操作。

内核无需为私有使用SEM_UNDO的操作都保存一份记录。只需要记录一个进程在一个信号量上使用SEM_UNDO操作所做出的调整总和即可,它是一个被称为semadj(信号量调整)的整数。当进程终止后,所有需要做的就是从信号量的当前值上减去这个总和。

当使用semctl SETVAL或者SETALL操作设置一个信号量时,所有使用这个信号量的进程中相应的 semadj 会被清空(即设置为 0)。这样做是合理的,因为直接设置一个信号量的值会破坏与 semadj 中维护的历史记录相关联的值

通过 fork()创建的子进程不会继承其父进程的 semadj 值,因为对于子进程来讲撤销其父进程的信号量操作毫无意义。另一方面,semadj 值会在 exec()中得到保留。这样就能在使用SEM_UNDO 调整一个信号量值之后通过 exec()执行一个不操作该信号量的程序,同时在进程终止时原子地调整该信号量。(这项技术可以允许另一个进程发现这个进程何时终止。)

SEM_UNDO 的效果举例

下面的 shell 会话日志显示了在两个信号量上执行操作的效果:一个操作使用了SEM_UNDO 标记,另一个没有使用

先创建一个包含两个信号量的集合
在这里插入图片描述
接着执行一个命令在两个信号量上都加上 1,然后终止。信号量 0 上的操作指定了SEM_UNDO 标记
在这里插入图片描述
检查信号量的状态。
在这里插入图片描述

从上面输出的最后两行中的信号量值可以看出信号量 0 上的操作被撤销了,但信号量 1上的操作没有被撤销

SEM_UNDO 的限制

最后需要指出的是,SEM_UNDO 其实并没有其一开始看起来那样有用,原因有两个。一个原因是由于修改一个信号量通常对应于请求或释放一些共享资源,因此仅仅使用SEM_UNDO 可能不足以允许一个多进程应用程序在一个进程异常终止时恢复。除非进程终止会原子地将共享资源的状态返回到一个一致的状态(在很多情况下是不可能的),否则撤销一个信号量操作可能不足以允许应用程序恢复。

第二个影响 SEM_UNDO 的实用性的因素是在一些情况下,当进程终止时无法对信号量进行调整。考虑下面应用于一个初始值为 0 的信号量上的操作。

1.进程 A 将信号量值增加 2,并为该操作指定了 SEM_UNDO 标记。
2.进程 B 将信号量值减去 1,因此信号量的值将变成 1。
3.进程 A 终止。

此时就无法完全撤销进程 A 在第一步中的操作中所产生的效果,因为信号量的值太小了。解决这个问题的潜在方法有三种。

  • 强制进程阻塞直到能够完成信号量调整。
  • 尽可能地减小信号量的值(即减到 0)并退出。
  • 退出,不执行任何信号量调整操作。

第一个解决方案是不可行的,因为它可能会导致一个即将终止的进程永远阻塞。Linux 采用了第二种解决方案。其他一些 UNIX 实现采纳了第三种解决方案。

实现一个二元信号量协议【待实现】

一个二元信号量有两个值:可用(空闲)或预留(使用中)。二元信号量有两个操作。

  • 预留:试图预留这个信号量以便互斥的使用。如果信号量已经被另一个进程预留了,那么将会阻塞直到信号量被释放为止
  • 释放:释放一个当前被预留的信号量,这样另一个进程就可以预留这个信号量了

有时还会定义第三个参数

  • 有条件的预留:非阻塞的尝试预留这个信号量以便互斥的使用。如果信号量已经被预留了,那么立即返回一个状态标识这个信号量不可用。

实现这个协态的最佳方式是使用值 1 表示空闲和值 0 表示预留,同时预留和释放操作分别为将信号量的值减 1 和加 1。

信号量限制

大多数 UNIX 实现都对 System V 信号量的操作进行了各种各样的限制。下面列出了 Linux 上信号量的限制。括号中给出了当限制达到时会受影响的系统调用及其所返回的错误。

  • SEMAEM:
    • 在semadj总和总能够记录的最大值。
    • 与SEMVMX的值是一样的(semop(), ERANGE)
  • SEMMNI:
    • 这是系统级别的一个限制,它限制了所能创建的信号量标识符的数量(semget(), ENOSPC)
  • SEMMSL:
    • 一个信号量集中能分配的信号量的最大数量。(semget(), EINVAL)
  • SEMMNS :
    • 这是系统级别的一个限制,它限制了所有信号量集中的信号量数量。
    • 系统上信号量的数量还受 SEMMNI 和 SEMMSL 的限制。实际上,SEMMNS 的默认值是这两个限制的默认值的乘积。(semget(), ENOSPC)
  • SEMOPM:每个 semop()调用能够执行的操作的最大数量。(semop(), E2BIG)
  • SEMVMX :一个信号量能取的最大值。(semop(), ERANGE)

大多数 UNIX 实现都定义了上面列出的限制。一些 UNIX 实现(不包括 Linux)在信号量撤销操作方面(参见 47.8 节)还定义了下面的限制

  • SEMMNU :
    • 这是系统级别的一个限制,它限制了信号量撤销结构的总数量。
    • 撤销结构是分配用来存储 semadj 值的
  • SEMUME :
    • 每个信号量撤销结构中撤销条目的最大数量

在系统启动时,信号量限制会被设置成默认值。不同的内核版本中的默认值可能会不同。(一些内核厂商设置的默认值与 vanilla 内核设置的默认值可能会不同。)其中一些限制可以通过修改存储在 Linux 特有的/proc/sys/kernel/sem 文件中的值来改变。这个文件包含了四个用空格分隔的数字,它们按序定义了 SEMMSL、SEMMNS、SEMOPM 以及 SEMMNI 限制。(SEMVMX 和 SEMAEM 限制是无法修改的,它们的值都被定义成 32767。)

$ cat /proc/sys/kernel/sem 
250	32000	32	128

Linux 特有的 semctl() IPC_INFO 操作返回一个类型为 seminfo 的结构,它包含了各种信号量限制的值。

union semun arg;
struct seminfo info;

arg._buf = &buf;
semctl(0, 0, IPC_INFO, arg);

相关的Linux特有的SEM_INFO操作会返回包含与信号量对象实际消耗的资源相关的信息的seminfo 结构

int
main(int argc, char *argv[])
{
    struct seminfo info;
    union semun arg;

    arg.__buf = &info;

    int s = semctl(0, 0, SEM_INFO, arg);
    if (s == -1){
        printf("semctl");
        exit(EXIT_FAILURE);
    }
        

    printf("Maximum ID index = %d\n", s);
    printf("sets in_use      = %ld\n", (long) info.semusz);
    printf("used_sems        = %ld\n", (long) info.semaem);
    exit(EXIT_SUCCESS);
}

缺点

System V 信号量存在的很多缺点与消息队列(参见 46.9 节)的缺点是一样的,包括以下几点

  • 信号量是通过标识符而不是文件描述符引用的。这使得执行一些操作变得比较麻烦
  • 使用key而不是文件名来标识信号量增加了额外的编程复杂度
  • 创建和初始化信号量需要单独的系统调用意味着在一些情况下必须要做一些额外的工作来防止竞争条件
  • 内核不会维护引用一个信号量集的进程数量,这就给确定何时删除一个信号量集增加了难度,而且无法确保一个不再使用的信号量集被删除
  • System V 提供的编程接口过于复杂。在通常情况下,一个程序只会操作一个信号量。同时操作集合中多个信号量的能力有时侯是多余的。
  • 信号量的操作存在诸多限制。这些限制是可配置的,但如果一个应用程序超出了默认限制的范围,那么在安装应用程序时就需要完成额外的工作了。

不管怎样,与消息队列所面临的情况不同,替代 System V 信号量的方案不多,其结果是在很多情况下都必须要用到它们。信号量的一个替代方案是记录锁。此外,从内核 2.6 以及之后的版本开始,Linux 支持使用 POSIX 信号量来进行进程同步。

实例

创建和操作 System V 信号量

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>

union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};
char *
currTime(const char *format)
{
#define BUF_SIZE 100 
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}
int main(int argc, char *argv[]){
    int semid;
    
    if (argc < 2 || argc > 3 || strcmp(argv[1], "--help") == 0){
        printf("%s init-value\n"
               "   or: %s semid operation\n", argv[0], argv[0]);
        exit(EXIT_FAILURE);
    }

    if (argc == 2) {            /* Create and initialize semaphore */
        union semun arg;

        semid = semget(IPC_PRIVATE, 1, S_IRUSR | S_IWUSR);
        if (semid == -1){
            perror("semid");
            exit(EXIT_FAILURE);
        }


        arg.val = atoi(argv[1]);  //"init-value"
        if (semctl(semid, /* semnum= */ 0, SETVAL, arg) == -1){
            perror("semctl");
            exit(EXIT_FAILURE);
        }


        printf("Semaphore ID = %d\n", semid);

    } else {                    /* Perform an operation on first semaphore */

        struct sembuf sop;              /* Structure defining operation */

        semid = atoi(argv[1]);

        sop.sem_num = 0;                /* Specifies first semaphore in set */
        sop.sem_op = atoi(argv[2]);  //"operation"
        /* Add, subtract, or wait for 0 */
        sop.sem_flg = 0;                /* No special options for operation */

        printf("%ld: about to semop at  %s\n", (long) getpid(), currTime("%T"));
        if (semop(semid, &sop, 1) == -1){
            perror("semop");
            exit(EXIT_FAILURE);
        }
            

        printf("%ld: semop completed at %s\n", (long) getpid(), currTime("%T"));
    }

    exit(EXIT_SUCCESS);

}

下面首先创建一个信号量并将其初始化为 0

在这里插入图片描述
然后执行一个后台命令将信号量值减去 2。
在这里插入图片描述
这个命令会阻塞,因为无法将信号量的值减到小于 0。现在执行一个命令将信号量值加上 3

在这里插入图片描述
这个信号量增加操作会立即成功,并且会导致后台命令中的信号量缩减操作能够向前执行,因为在执行该操作之后不会导致信号量值小于 0。

监控一个信号量集

下面使用了各种semctl()操作来显示标识符为命令行参数值的既有信号量集的信息。这个程序首先显示了 semid_ds 数据结构中的时间字段,然后显示了集合中各个信号量的当前值及其 sempid、semncnt 和 semzcnt 值

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};
char *
currTime(const char *format)
{
#define BUF_SIZE 100
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}
int
main(int argc, char *argv[])
{
    struct semid_ds ds;
    union semun arg, dummy;             /* Fourth argument for semctl() */
    int semid, j;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s semid\n", argv[0]);
        exit(EXIT_FAILURE);
    }


    semid = atoi(argv[1]);

    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1){
        perror("semctl");
        exit(EXIT_FAILURE);
    }

    printf("Semaphore changed: %s", ctime(&ds.sem_ctime));
    printf("Last semop():      %s", ctime(&ds.sem_otime));

    /* Display per-semaphore information */

    arg.array = static_cast<unsigned short *>(calloc(ds.sem_nsems, sizeof(arg.array[0])));
    if (arg.array == NULL){
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    if (semctl(semid, 0, GETALL, arg) == -1){
        perror("semctl-GETALL");
        exit(EXIT_FAILURE);
    }


    printf("Sem #  Value  SEMPID  SEMNCNT  SEMZCNT\n");

    for (j = 0; j < ds.sem_nsems; j++)
        printf("%3d   %5d   %5d  %5d    %5d\n", j, arg.array[j],
               semctl(semid, j, GETPID, dummy),
               semctl(semid, j, GETNCNT, dummy),
               semctl(semid, j, GETZCNT, dummy));

    exit(EXIT_SUCCESS);
}

在这里插入图片描述

初始化一个集合中的所有信号量

下面程序初始化一个既有集合中的所有信号量。第一个命令行参数是待初始化的信号量集的标识符。剩下的命令行参数指定了每个信号量所初始化的值(参数的数量必须要与集合中信号量的数量一致)。

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};

int
main(int argc, char *argv[])
{
    struct semid_ds ds;
    union semun arg;                    /* Fourth argument for semctl() */
    int j, semid;

    if (argc < 3 || strcmp(argv[1], "--help") == 0){
        printf("%s semid val...\n", argv[0]);
        exit(EXIT_FAILURE);
    }
        

    semid = atoi(argv[1]);

    /* Obtain size of semaphore set */

    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1){
        printf("semctl");
        exit(EXIT_FAILURE);
    }
        

    /* The number of values supplied on the command line must match the
       number of semaphores in the set */

    if (ds.sem_nsems != argc - 2){
        printf("Set contains %ld semaphores, but %d values were supplied\n",
               (long) ds.sem_nsems, argc - 2);
        exit(EXIT_FAILURE);
    }
        

    /* Set up array of values; perform semaphore initialization */

    arg.array = static_cast<unsigned short *>(calloc(ds.sem_nsems, sizeof(arg.array[0])));
    if (arg.array == NULL){
        printf("calloc");
        exit(EXIT_FAILURE);
    }
        

    for (j = 2; j < argc; j++)
        arg.array[j - 2] = atoi(argv[j]);

    if (semctl(semid, 0, SETALL, arg) == -1){
        printf("semctl-SETALL");
        exit(EXIT_FAILURE);
    }
        
    printf("Semaphore values changed (PID=%ld)\n", (long) getpid());

    exit(EXIT_SUCCESS);
}

信号量初始化竞争条件

根据SUSv3的要求,实现无需对由semget()创建的集合中的信号量值进行初始化。相反,程序员必须使用semctl()系统调用显式初始化信号量(在Linux上,semget()返回的信号量实际上会被初始化为0,但是为取得移植性就不能依赖于此)。信号量的创建和初始化必须要通过单独的系统调用而不是单个原子步骤来完成的事实可能会导致在初始化一个信号量时出现竞争条件。我们必须避免这个条件

假设一个引用程序由多个地位平等的进程构成,这些进程使用一个信号量来协调相互之间的动作。由于无法保证哪个进程会首先使用信号量量(这就是地位平等的含义),因此每个进程都要做好信号量不存在时创建和初始化信号量的准备。但是,我们不能像下面这样写:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};

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 */

        arg.val = 0;                    /* So initialize it */
        if (semctl(semid, 0, SETVAL, arg) == -1){
            perror("semctl");
            exit(EXIT_FAILURE);
        }
            

    } else {                            /* We didn't create semaphore set */
        if (errno != EEXIST) {          /* Unexpected error from semget() */
            perror("semget 1");
            exit(EXIT_FAILURE);
        } else {                        /* Someone else already created it */
            semid = semget(key, 1, perms);      /* So just get ID */
            if (semid == -1){
                perror("semget 2");
                exit(EXIT_FAILURE);
            }
        }
    }

    /* 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){
        perror("semop");
        exit(EXIT_FAILURE);
    }
        

    exit(EXIT_SUCCESS);
}

问题在于:首先,进程 B 在一个未初始化的信号量(即其值是一个任意值)上执行了一个 semop()。其次,进程 A 中的 semctl()调用覆盖了进程 B 所做出的变更
在这里插入图片描述
这个问题的解决方案依赖于一个现已成为标准的特性,即与这个信号量集相关联的semid_ds 数据结构中的 sem_otime 字段的初始化。在一个信号量集首次被创建时,sem_otime字段会被初始化为 0,并且只有后续的 semop()调用才会修改这个字段的值。因此可以利用这个特性来消除上面描述的竞争条件,即只需要插入额外的代码来强制第二个进程(即没有创建信号量的那个进程)等待直到第一个进程既初始化了信号量又执行了一个更新 sem_otime字段但不修改信号量的值的 semop()调用为止

但是这个解决方案无法在所有 UNIX 实现上正常工作。在一些现代 BSD 衍生版中,semop()不会更新 sem_otime 字段。

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};

int
main(int argc, char *argv[])
{
    int semid, key, perms;
    struct sembuf sops[2];

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s sem-op\n", argv[0]);
        exit(EXIT_FAILURE);
    }
        

    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){
            printf("semctl 1");
            exit(EXIT_FAILURE);
        }
           
        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){
            printf("semop");
            exit(EXIT_FAILURE);
        }
            
        printf("%ld: completed dummy semop()\n", (long) getpid());

    } else {                            /* We didn't create the semaphore set */

        if (errno != EEXIST) {          /* Unexpected error from semget() */
            printf("semget 1");
            exit(EXIT_FAILURE);
        } 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){
                printf("semget 2");
                exit(EXIT_FAILURE);
            }
                

            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){
                    printf("semctl 2");
                    exit(EXIT_FAILURE);
                }
                    

                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! */
            {
                printf("Existing semaphore not initialized");
                exit(EXIT_FAILURE);
            }
        }
    }

    /* Now perform some operation on the semaphore */

    sops[0].sem_num = 0;                        /* Operate on semaphore 0... */
    sops[0].sem_op = atoi(argv[1]);
    sops[0].sem_flg = 0;
    if (semop(semid, sops, 1) == -1){
        printf("semop");
        exit(EXIT_FAILURE);
    }
        

    exit(EXIT_SUCCESS);
}

总结

System V信号量允许进程同步它们的动作。这在当一个进程必须要获取对某些共享资源(比如一块共享内存)的互斥性访问时是比较有用的

信号量的创建和操作是以集合为单位的,一个集合包含一个或多个信号量。集合中的每个信号量都是一个整数,其值永远大于或等于 0。semop()系统调用允许调用者在一个信号量上加上一个整数、从一个信号量中减去一个整数、或等待一个信号量等于 0。后两个操作可能会导致调用者阻塞。

如果多个进程对该信号量减去的值是一样的,那么当条件允许时到底哪个进程会首先被允许执行操作是不确定的。但如果多个进程对信号量减去的值是不同的,那么会按照先满足条件先服务的顺序来进行并且需要小心避免出现一个进程因信号量永远无法达到允许进程操作继续往前执行的值而饿死的情况

SEM_UNDO 标记允许一个进程的信号量操作在进程终止时自动撤销。这对于防止出现进程意外终止而引起的信号量处于一个会导致其他进程因等待已终止的进程修改信号量值而永远阻塞情况来讲是比较有用的

System V 信号量的分配和操作是以集合为单位的,并且对其增加和减小的数量可以是任意的。它们提供的功能要多于大多数应用程序所需的功能。对信号量常见的要求是单个二元信号量,它的取值只能是 0 和 1。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值