53 POSIX 信号量

大概特点

POSIX 信号量有两种:命名信号量和匿名信号量,前者和 System V 信号量比较相似(System V 的 IPC 都用 key 来标识,因此相当于是命名的)。

POSIX 信号量使用了 futex(2) 来实现,在没有争抢的情况下,不会发生系统调用,因此效率比 System V 实现更高。在争抢频繁的情况下,两者性能差不多。

和 POSIX 消息队列类似,Linux 上的 POSIX 信号量被挂载在 /dev/shm 目录这个 tmpfs 文件系统下。该文件系统具有内和持久性。

(二元)信号量和 pthreads mutex 相比:

  1. 前者是异步信号安全的,后者不是。但是处理信号还是建议用 sigwaitinfo()
  2. 前者可以由任何线程释放资源(sem_post()),后者只能由锁的持有者释放,否则是未定义行为。

命名信号量

可以用 sem_open() 来打开或创建命名信号量。这个过程和打开文件很像,如果需要创建命名信号量,则需要额外提供 mode 和 value 参数。这保证了命名信号量创建和初始化过程的原子化,这是 System V 没有的保证

sem_open() 的返回值是 sem_t *,这意味着我们要固定地址来使用信号量,不能复制它的值。如果打开 / 创建命名信号量失败,则会返回 SEM_FAILED

关闭信号量用 sem_close(),进程结束时也会自动关闭。删除信号量用 sem_unlink(),命名信号量和文件一样有引用计数:尽管已经调用 sem_unlink(),只有所有进程都关闭命名信号量后,该信号量才会真正被删除。

信号量操作

As with a System V semaphore, a POSIX semaphore is an integer that the system never allows to go below 0.

信号量的值永远不会小于 0。不要以为可以先扣到负数再增加!

POSIX 信号量的 sem_wait()sem_post() 操作每次只能操作一个信号量,而且只能将该信号量值的绝对值改变 1(如果操作成功)。这是和 SysV 不同的。

sem_wait() 如果被中断会失败并设置 EINTR 错误(英文书上说的是 fails with the error EINTR,翻译的是“返回 EINTR 错误”,让人以为是设置了返回值,实际上是设置了 errno)。即便信号量处理器建立时启用了 SA_RESTART 标记,Linux 也不会自动重启这个系统调用 —— 但是有的 UNIX 实现会重启。

除了 sem_wait()sem_post() 之外,还能用 sem_getvalue() 获得信号量的当前值。如果有进程正在阻塞等待该信号量,Linux 等实现获取到的信号量值是 0,而有些 UNIX 实现获取到的信号量值是负数。

未命名(Unamed)信号量

为什么不说是匿名信号量呢?

未命名信号量可以使用和命名信号量一样的操作,但是创建和销毁的方式不同。

创建用 sem_init()。在创建之前要先分配好信号量所需的内存。

SYNOPSIS
       #include <semaphore.h>

       int sem_init(sem_t *sem, int pshared, unsigned int value);

如果 pshared 非 0,则信号量可以用于进程间共享,这个时候需要将信号量放在共享内存段上(否则实际上是不共享的)。如果 pshared 为 0,则信号量只能在此进程使用(线程间可以共享)。尽管很多系统包括带有 NTPL 实现的 Linux 上 pshared 没有什么用,但为了可移植性还是要认真写。

一个信号量只能被初始化一次,初始化多次是未定义行为。

sem_destroy() 可以销毁信号量。很多系统上未命名信号量在进程结束之后(随着共享内存段)一起释放,但是有些系统上信号量是一种需要小心管理的资源,因此为了可移植性最好显式销毁

SYNOPSIS
       #include <semaphore.h>

       int sem_destroy(sem_t *sem);

未命名信号量没有访问权限的设置,控制其访问权限可以通过修改其所在共享内存段的访问权限来完成。

信号量的限制

SEM_NSEMS_MAX:一个进程能拥有的最大信号量数目。SUSv3 要求这个值至少为 256,Linux 上这个值受限于可用的内存。(没有整个系统上的限制!)

SEM_VALUE_MAX:一个 POSIX 信号量可取的最大值。SUSv3 要求这个值至少为 32767,Linux 上这个值为 INT_MAX。