第 53 章 POSIX 信号量 (POSIX Semaphores)

      +

      核心结论

      • 两种 POSIX 信号量:命名(sem_open,有名字跨进程共享)和无名(sem_init,放在共享内存或全局变量)——前者无关进程可用,后者适合线程/父子进程。

      • 信号量本质:内核维护的整数,永不降到 0 以下——sem_wait() 减 1(值为 0 时阻塞或失败);sem_post() 加 1(唤醒一个等待者)。

      • 核心 API:sem_open/close/unlink、sem_init/destroy、sem_wait/trywait/timedwait、sem_post、sem_getvalue。

      • POSIX vs SysV 对比:POSIX 简单(一次操作一个 sem,±1);SysV 复杂(semop 多 sem 任意值);POSIX 低竞争性能优 10×(用 futex,无需 syscall);POSIX 缺点:无 undo。

      • pshared 标志:sem_init(sem, pshared, value) 中 pshared=0 为线程共享(全局/堆),非 0 为进程共享(必须在共享内存中)。

      • sem_post 异步信号安全:sem_post 是少数 async-signal-safe 的同步原语——可在 signal handler 内调用唤醒线程;pthread mutex 不行。

      • Linux 实现:2.6+ 完整支持;用 futex(kernel 2.6 + NPTL);命名 sem 在 /dev/shm/sem.<name>;同步语义「不是排队机制」。

      • 限制:SEM_NSEMS_MAX(Linux 仅受内存限制);SEM_VALUE_MAX = INT_MAX。

      本章主旨

      POSIX 信号量是 SysV 信号量的现代简化版——核心改进是「单 sem 操作」「futex 实现」「引用计数」。读者需要建立三组对比:(1) 命名 vs 无名——按使用场景选择;(2) POSIX sem vs pthread mutex——线程内首选 mutex,跨进程才用 sem;(3) POSIX sem vs SysV sem——POSIX 更简单更快,SysV 更可移植有 undo。本章是第 47 章 SysV 信号量、第 30 章 pthread 同步的衔接点。

      一、核心概念

      本章围绕 6 个核心概念展开:命名 vs 无名、sem_wait/post 语义、pshared 标志、引用计数、futex 实现、与 pthread mutex 对比。

      概念 定义 + 重要性 实现提示

      命名信号量

      sem_open(name, oflag, mode, value) 返回 sem_t*;无关进程用同一名字访问;引用计数删除(sem_unlink);创建与初始化原子完成

      §53.2;Linux 命名 sem 在 /dev/shm/sem.<name>(tmpfs);2.6+ 支持;SEM_FAILED 错误返回

      无名信号量

      sem_init(sem, pshared, value) 初始化共享内存或全局变量中的 sem_t;用 sem_destroy 销毁

      §53.4;pshared=0 线程共享;非 0 必须放在共享内存;典型嵌入动态数据结构

      sem_wait / sem_trywait / sem_timedwait

      sem_wait 减 1(值 0 时阻塞);sem_trywait 立即失败 EAGAIN;sem_timedwait 阻塞到 abs_timeout(ETIMEDOUT)

      §53.3.1;信号 handler 打断 sem_wait 必返回 EINTR(即使 SA_RESTART);SUSv3 规定不可 restart

      sem_post

      加 1;若值从 0 升 1,唤醒一个等待者;唤醒哪个由调度策略决定(默认 round-robin 不确定)

      §53.3.2;唯一 async-signal-safe 的同步原语之一;可在 signal handler 内调用;real-time 调度下唤醒最高优先最长等待

      pshared 标志

      sem_init 第二个参数;0=线程共享;非 0=进程共享(sem 必须在共享内存);NPTL 忽略 pshared(旧 LinuxThreads 报 ENOSYS)

      §53.4.1;Linux 2.6+ 才支持 pshared≠0;可移植代码仍应正确指定

      POSIX vs SysV vs pthread mutex

      POSIX 简单(一次 1 个,±1);SysV 复杂(多 sem 任意值,有 undo);pthread mutex 有 ownership 适合线程内

      §53.5;POSIX 低竞争下性能优 10×(futex 无 syscall);线程内首选 mutex;跨进程用 POSIX sem

      二、详细笔记

      53.1 命名信号量生命周期

      Whatsem_open / sem_close / sem_unlink——POSIX 命名 sem 的三件套。

      Why:与 POSIX IPC 三件套同款 API——引用计数删除;创建+初始化原子。

      How

      // 摘自《The Linux Programming Interface》 第 53 章
      #include <fcntl.h>
      #include <sys/stat.h>
      #include <semaphore.h>
      
      sem_t *sem_open(const char *name, int oflag, ...);
      int sem_close(sem_t *sem);
      int sem_unlink(const char *name);
      
      /* 创建(创建+初始化原子) */
      sem_t *sem = sem_open("/demo", O_CREAT|O_EXCL, 0660, 0);
      if (sem == SEM_FAILED) errExit("sem_open");
      
      /* 打开已存在 */
      sem_t *sem = sem_open("/demo", 0);
      
      /* 用完 */
      sem_close(sem);
      sem_unlink("/demo");     /* 引用归零销毁 */

      Linux 命名 sem 位于 /dev/shm/sem.<name>(tmpfs 文件系统);创建需 root 或目录有写权限。

      When:(1) 无关进程间同步;(2) 守护进程创建并 unlink(避免名字残留);(3) 创建时务必给足够权限(很多应用 O_RDWR)——sem 实际无 O_RDONLY 等。

      Example:第 53 章 psem_create -cx /demo 666 0 创建权限 666、初始值 0 的 sem;ls /dev/shm/sem.* 看到 sem.demo

      53.2 sem_wait / sem_post 操作语义

      Whatsem_wait 减 1(值 0 阻塞);sem_post 加 1(唤醒等待者)。

      Why:核心同步原语——「P/V」操作的现代化实现。

      How

      // 摘自《The Linux Programming Interface》 第 53 章
      #include <semaphore.h>
      
      int sem_wait(sem_t *sem);                  /* 阻塞减 1 */
      int sem_trywait(sem_t *sem);               /* 非阻塞,失败 EAGAIN */
      int sem_timedwait(sem_t *sem,
                        const struct timespec *abs_timeout);  /* 超时减 1,ETIMEDOUT */
      int sem_post(sem_t *sem);                  /* 加 1,唤醒等待者 */
      
      /* 经典生产者-消费者 */
      sem_t empty, full;
      void *producer(void *arg) {
          /* produce item */
          sem_wait(&empty);              /* 等待空槽 */
          /* put item in buffer */
          sem_post(&full);               /* 通知消费者 */
      }
      void *consumer(void *arg) {
          sem_wait(&full);               /* 等待数据 */
          /* get item from buffer */
          sem_post(&empty);              /* 通知生产者 */
      }

      When:(1) 互斥——初始值 1 的 sem 当 mutex 用(但首选 pthread mutex);(2) 资源计数——初始值 N 表示 N 个可用资源;(3) 信号——sem_post 在 handler 内通知工作线程。

      Example:第 53 章示例——psem_create -c /demo 600 0 创建初始 0;后台 psem_wait /demo 阻塞;前台 psem_post /demo 唤醒。

      53.3 无名信号量与 pshared

      Whatsem_init(sem, pshared, value) 初始化内存中的 sem_t;sem_destroy 销毁。

      Why:(1) 线程间共享——全局变量或堆 sem 即可,无需名字;(2) 父子进程共享——sem 放在 mmap SHARED|ANON 或 POSIX shm。

      How

      // 摘自《The Linux Programming Interface》 第 53 章
      #include <semaphore.h>
      
      int sem_init(sem_t *sem, int pshared, unsigned int value);
      int sem_destroy(sem_t *sem);
      
      /* 线程共享(pshared=0) */
      sem_t sem;
      sem_init(&sem, 0, 1);                       /* 当 mutex 用 */
      pthread_mutex_lock(&sem_to_mutex(&sem));    /* 推荐用 mutex */
      
      /* 进程共享(pshared!=0,sem 必须在共享内存) */
      struct shm {
          sem_t sem;
          /* 共享数据 */
      };
      int fd = shm_open("/myshm", O_CREAT|O_RDWR, 0660);
      ftruncate(fd, sizeof(struct shm));
      struct shm *p = mmap(NULL, sizeof(struct shm),
                           PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
      sem_init(&p->sem, 1, 1);                    /* 跨进程 mutex */

      pshared 行为:

      • pshared = 0 — 线程共享(sem 在全局/堆);NPTL 忽略但建议正确指定。

      • pshared != 0 — 进程共享(sem 必须在共享内存中);Linux 2.6+ 支持;旧 LinuxThreads 报 ENOSYS。

      When:(1) 线程间同步——首选 pthread mutex,sem 仅作互斥不推荐(无 ownership);(2) 父子进程同步——sem 在 mmap SHARED|ANON;(3) 无关进程同步——用命名 sem。

      Example:第 53 章 thread_incr_psem.c 用 sem_init(&sem, 0, 1) 实现两线程对全局变量互斥递增。

      53.4 引用计数删除 vs SysV 显式删除

      What:POSIX sem_unlink 立即删名字,引用归零才真销毁;SysV semctl IPC_RMID 需显式删除。

      Why:POSIX 避免「最后一个用户忘记删」导致的资源泄漏。

      How

      场景 POSIX sem SysV sem

      创建+初始化

      sem_open 原子(避免竞争)

      semget 后 semctl SETVAL(非原子)

      删除

      sem_unlink 立即删名字;引用归零销毁

      semctl IPC_RMID 显式删除;最后用户负责

      进程退出

      自动 close(减引用)

      不自动删除;内核持久

      资源计数

      自动

      需应用管理

      When:(1) 守护进程——创建后立即 unlink(避免名字残留,引用归零自动销毁);(2) SysV 移植——确保退出前 IPC_RMID;(3) 大量动态 sem——无名 sem 嵌入数据结构,进程退出自动消失。

      Example:POSIX 守护进程模板:sem_open("/x", O_CREAT, …​)sem_unlink("/x") → 用 sem;进程退出后 sem 自动消失。

      53.5 POSIX vs SysV 信号量

      What:POSIX 简单(一次 1 个,±1);SysV 复杂(semop 多 sem 任意值,有 undo)。

      Why:理解两者取舍——决定项目用哪套。

      How

      维度 POSIX sem SysV sem

      操作粒度

      一次 1 个;±1

      semop 多 sem 任意值

      wait-for-zero

      有(sem_op=0)

      undo

      SEM_UNDO(进程退出回滚)

      性能(低竞争)

      快 10×(futex 无 syscall)

      每次都 syscall

      命名

      名字 + sem_open

      key + semget

      可移植性

      Linux 2.6+;SUSv3 可选

      SUSv3 强制;几乎所有 UNIX

      创建初始化

      原子

      非原子(需技巧避免竞争)

      When:(1) 新项目 Linux 为主——POSIX 更简单更快;(2) 多 UNIX 平台——SysV 更安全;(3) 需要 undo(防死锁)——SysV;(4) 需要 wait-for-zero——SysV;(5) 高频 sem 操作——POSIX(futex 优势)。

      Example:第 47 章 SysV sem 实现 svsem_xfr vs 第 53 章 POSIX sem 实现 thread_incr_psem——POSIX 代码明显简洁。

      53.6 POSIX sem vs pthread mutex

      What:POSIX sem 和 pthread mutex 都可同步线程——但 mutex 有 ownership,sem 没有。

      Why:线程内首选 mutex(ownership 强制代码结构);sem 仅在跨进程或 async-signal 场景用。

      How

      维度 pthread mutex POSIX sem

      ownership

      ✓(只有 locker 可 unlock)

      ✗(线程 A wait,线程 B post)

      性能

      相当(都用 futex)

      相当(低竞争)

      异步信号安全

      ✗(不可在 handler 内用)

      ✓(sem_post 可在 handler 内)

      跨进程

      默认不行(需放共享内存且用 PTHREAD_PROCESS_SHARED 属性)

      ✓(命名 sem 或共享内存无名 sem)

      代码结构

      强制清晰

      易混乱(称为「goto of concurrent programming」)

      When:(1) 线程内互斥——首选 pthread_mutex;(2) 跨进程同步——POSIX sem;(3) signal handler 唤醒工作线程——sem_post;(4) 高频 sem 操作——POSIX 比 SysV 快。

      Example:典型 pthread mutex 用法——pthread_mutex_lock / pthread_mutex_unlock;POSIX sem 互斥——sem_wait / sem_post,但任何线程可 post,需更谨慎。

      三、关键图表

      非可视化条目(API / 比较)
      类别 内容

      命名 sem API

      sem_open(name, oflag, mode, value) / sem_close / sem_unlink;返回 sem_t* 或 SEM_FAILED

      无名 sem API

      sem_init(sem, pshared, value) / sem_destroy;sem 在共享内存/全局/堆

      阻塞/非阻塞/超时

      sem_wait / sem_trywait(EAGAIN) / sem_timedwait(ETIMEDOUT)

      加 1

      sem_post;唯一 async-signal-safe

      读值

      sem_getvalue;Linux 不返回负值(SUSv3 允许)

      Linux 命名 sem 位置

      /dev/shm/sem.<name>(tmpfs)

      Linux 支持

      kernel 2.6+;NPTL 实现

      实现机制

      futex(低竞争无 syscall)

      pshared

      0=线程;非 0=进程(必须共享内存)

      SEM_NSEMS_MAX

      Linux 几乎无限;SUSv3 ≥256

      SEM_VALUE_MAX

      Linux INT_MAX(2,147,483,647);SUSv3 ≥32,767

      vs SysV

      POSIX 简单(±1);SysV 复杂(任意值 + undo);POSIX 性能优 10×(低竞争)

      vs pthread mutex

      mutex 有 ownership 适合线程内;sem 跨进程或 async-signal

      四、思维导图

      mindmap
        root((第 53 章 POSIX 信号量))
          两种类型
            命名 sem_open
            无名 sem_init
            命名 跨进程
            无名 线程或父子
          sem_wait post
            sem_wait 减1
            sem trywait EAGAIN
            sem timedwait ETIMEDOUT
            sem_post 加1
            sem_post 唤醒等待者
            sem_post 异步信号安全
          pshared
            0 线程共享
            非0 进程共享
            必须共享内存
            Linux 2.6+ 支持
          引用计数
            sem_unlink 删名字
            引用归零销毁
            避免最后用户负责
            SysV 显式 IPC_RMID
          futex 实现
            低竞争无 syscall
            性能 优 10 倍
            NPTL 线程库
            kernel 2.6
          vs SysV pthread
            SysV 任意值 undo
            SysV 可移植性强
            pthread mutex ownership
            mutex 线程内首选
            sem 跨进程首选
      
      == 五、重点与易错点
      
      . *POSIX sem 必须 `-lrt` 链接*——glibc 部分版本需要;现在很多发行版已集成。
      . *Linux 完整支持需 2.6+*——LinuxThreads(glibc 2.3 之前)只支持线程共享;NPTL(2.6+)支持进程共享。
      . *sem_open 失败返回 SEM_FAILED*——是 `((sem_t *) 0)`(Linux)或 `((sem_t *) -1)`;不要用 NULL 判断。
      . *sem_post 唯一 async-signal-safe 同步原语*——可在 signal handler 内调用唤醒工作线程;pthread_mutex_lock 不行。
      . *sem_wait 被信号打断返回 EINTR*——即使 SA_RESTART 也不自动重启;必须手动循环或用 sem_timedwait。
      . *POSIX sem 不是排队机制*——多个等待者唤醒哪个由调度策略决定;不要依赖 FIFO。
      . *POSIX 无 undo*——进程退出时不会回滚 sem 状态;SysV 有 SEM_UNDO;小心死锁。
      . *pshared 非 0 时 sem 必须在共享内存*——否则 fork 后子进程看不到 sem;用 mmap SHARED|ANON 或 POSIX shm。
      . *sem_init 仅能调一次*——重复初始化未定义行为;设计时确保单一进程/线程初始化。
      . *sem_destroy 之前必须无等待者*——否则未定义行为;用 sem_getvalue + 循环等待。
      . *不要对 sem_t 副本操作*——只能对原 sem 操作;`sem2 = *sp; sem_wait(&sem2)` 是 UB。
      . *命名 sem 创建需足够权限*——Linux 用 `/dev/shm` 下文件,目录需有写权限;普通用户一般可创建。
      . *POSIX vs SysV 性能*——低竞争 POSIX 优 10×(futex);高竞争相当;POSIX 缺点是无法在 handler 内用 SysV semctl。
      . *POSIX sem 与 pthread mutex 选择*——线程内 mutex 首选(ownership);跨进程 POSIX sem;async-signal 用 sem_post。
      . *跨章衔接*:第 30 章 pthread mutex/cond(线程内同步);第 47 章 SysV sem(可移植 + undo);第 49 章 mmap SHARED|ANON(无名 sem 的进程共享载体);第 54 章 POSIX shm(无名 sem 也可放 POSIX shm)。