Linux编程之信号量

信号量分 System V 信号量和 POSIX 信号量,这里仅介绍 POSIX 信号量。

1. 概述

2. 命令信号量

3. 信号量操作

3.1 等待一个信号量

sem_wait() 函数会递减(减小 1)sem 引用的信号量的值。

#include <semaphore.h>

int sem_wait(sem_t *sem);
  • 如果信号量的当前值大于 0,那么 sem_wait() 会立即返回。
  • 如果信号量的当前值等于 0,那么 sem_wait() 会阻塞直到信号量的值大于 0 为止,当信号量值大于 0 时该信号量值就被递减并且 sem_wait() 会返回。

如果一个阻塞的 sem_wait() 调用被一个信号处理器中断了,那么它就会失败并返回 EINTR 错误,不管在使用 sigaction() 建立这个信号处理器时是否采用了 SA_RESTART 标记。(在其他一些 UNIX 实现上,SA_RESTART 会导致 sem_wait() 自动重启。)

3.1.1 sem_trywait()

sem_trywait() 函数是 sem_wait() 的一个非阻塞版本。

#include <semaphore.h>

int sem_trywait(sem_t *sem);

如果递减操作无法立即被执行,那么 sem_trywait() 就会失败并返回 EAGAIN 错误。

3.1.2 sem_timedwait()

sem_timedwait() 函数是 sem_wait() 的另一个变体,它允许调用者为调用被阻塞的时间量指定一个限制。

#define _XOPEN_SOURCE 600
#include <semaphore.h>

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

如果 sem_timedwait() 调用因超时而无法递减信号量,那么这个调用就会失败并返回 ETIMEDOUT 错误。

abs_timeout 参数是一个结构,它将超时时间表示成了自新纪元到现在为止的秒数和纳秒数的绝对值。如果需要指定一个相对超时时间,那么就必须要使用 clock_gettime() 获取 CLOCK_REALTIME 时钟的当前值并在该值上加上所需的时间量来生成一个适合在 sem_timedwait() 中使用的 timespec 结构。

3.2 发布一个信号量

sem_post() 函数递增(增加 1)sem 引用的信号量的值。

#include <semaphore.h>

int sem_post(sem_t *sem);

如果在 sem_post() 调用之前信号量的值为 0,并且其他某个进程(或线程)正在因等待递减这个信号量而阻塞,那么该进程会被唤醒,它的 sem_wait() 调用会继续往前执行来递减这个信号量。如果多个进程(或线程)在 sem_wait() 中阻塞了,并且这些进程的调度采用的是默认的循环时间分享策略,那么哪个进程会被唤醒并允许递减这个信号量是不确定的。

3.3 获取信号量的当前值

sem_getvalue() 函数将 sem 引用的信号量的当前值通过 sval 指向的 int 变量返回。

#include <semaphore.h>

int sem_getvalue(sem_t *sem, int *sval);

如果一个或多个进程(或线程)当前正在阻塞以等待递减信号量值,那么 sval 中的返回值将取决于实现。

注意在 sem_getvalue() 返回时,sval 中的返回值可能已经过时了。依赖于 sem_getvalue() 返回的信息再执行后续操作时未发生变化的程序将会碰到检查时、使用时(time-of-check、time-of-use)的竞争条件。

4. 未命令信号量

未命名信号量(也被称为基于内存的信号量)是类型为 sem_t 并存储在应用程序分配的内存中的变量。通过将这个信号量放在由几个进程或线程共性的内存区域中就能够使这个信号量对这些进程或线程可用。

操作未命名信号量所使用的函数与操作命令信号量使用的函数是一样的(sem_wait()、sem_post() 以及 sem_getvalue()等)。此外,还需要用到另外两个函数。

  • sem_init() 函数对一个信号量进行初始化并通知系统该信号量会在进程间共享还是在单个进程中的线程间共享。
  • sem_destroy(sem) 函数销毁一个信号量。

这两个函数不可以被应用到命名信号量上。

未命名与命名信号量对比

未命名信号量无需为信号量创建一个名字,这种做法在如下情况比较常见:

  • 在线程间共享的信号量不需要名字。将一个未命名信号量作为一个共享(全局或堆上的)变量自动会使之对所有线程可访问。
  • 在相关进程间共享的信号量不需要名字。如果一个父进程在一块共享内存区域中(如一个共享匿名映射)分配了一个未命名信号量,那么作为 fork() 操作的一部分,子进程会自动继承这个映射,从而继承这个信号量。
  • 如果正在构建的是一个动态数据结构(如二叉树),并且其中的每一项都需要一个关联的信号量,那么最简单的做法是在每一项中都分配一个未命名信号量。为每一项打开一个命名信号量需要为如何生成每一项中的信号量名字(唯一的)和管理这些名字设计一个规则(如当不需要它们时就对它们进行断开链接操作)。

4.1 初始化一个未命名信号量

sem_init() 函数使用 value 中指定的值来对 sem 指向的未命名信号量进行初始化。

#include <semaphort.h>

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

        Returns 0 on success, or -1 on error.

pshared 参数表明这个信号量是在线程间共享还是在进程间共享。

  • 如果 pshared 等于 0,那么信号量将会在调用进程中的线程间进行共享。在这种情况下,sem 通常被指定成一个全局变量的地址或分配在一个堆上的一个变量的地址。线程共享的信号量具备进程持久性,它在进程终止时会被销毁。
  • 如果 pshared 不等于 0,那么信号量将会在进程间共享。在这种情况下,sem 必须是共享内存区域(一个 POSIX 共享内存对象、一个使用 mmap() 创建的共享映射、或一个System V 共享内存段)中的某个位置的地址。信号量的持久性与它所处的共享内存的持久性是一样的。(通过其中大部分技术创建的共享内存区域具备内核持久性。但共享匿名映射是一个例外,只要存在一个进程维持着这种映射,那么它就一直存在下去。)由于通过 fork() 创建的子进程会继承其父进程的内存映射,因此进程共享的信号量会被通过 fork() 创建的子进程继承,这样父进程和子进程也就能够使用这些信号量来同步它们的动作了。

之所以需要 pshared 参数是因为如下原因:

  • 一些实现不支持进程间共享的信号量。在这些系统上为 pshared 指定一个非零值会导致 sem_init() 返回一个错误。Linux 直到内核 2.6 以及 NPTL 线程化技术的出现之后才开始支持未命名的进程间共享的信号量。
  • 在同时支持进程间共享信号量和线程间共享信号量的实现上,指定采用何种共享方式是有必要的,因为系统必须要执行特殊的动作来支持所需的共享方式。提供此类信息还使得系统能够根据共享的种类来执行优化工作。

NPTL sem_init() 实现会忽略 pshared,因为不管采用何种共享方式都无需执行特殊的工作。

未命名信号量不存在相关的权限设置(即 sem_init() 中并不存在 sem_open() 中所需的 mode 参数)。对一个未命名信号量的访问将由进程在底层共享内存区域上的权限来控制。

4.2 销毁一个未命名信号量

sem_destroy() 函数将销毁信号量 sem,其中 sem 必须是一个之前使用 sem_init() 进行初始化的未命名信号量。只有在不存在进程或线程在等待一个信号量时才能安全销毁这个信号量。

#include <semaphore.h>

int sem_destroy(sem_t *sem);
        Returns 0 on success, or -1 on error.

当使用 sem_destroy() 销毁了一个未命名信号量之后就能够使用 sem_init() 来重新初始化这个信号量了。

一个未命名信号量应该在其底层的内存被释放之前被销毁。如,如果信号量在一个自动分配的变量,那么在其宿主函数返回之前就应该销毁这个信号量。如果信号量位于一个 POSIX 共享内存区域中,那么在所有进程都是用完这个信号量以及在使用 shm_unlink() 对这个共享内存对象执行断开链接操作之前应该销毁这个信号量。

在一些实现上,省略 sem_destroy() 调用不会导致问题的发生,但在其他实现上,不调用 sem_destroy() 会导致资源泄露。

posted @ 2018-06-16 22:42  季末的天堂  阅读(2583)  评论(0编辑  收藏  举报