第 47 章 System V 信号量 (System V Semaphores)

      +

      核心结论

      • 信号量本质:内核维护的非负整数;P 减(值 < 0 阻塞)、V 加(值=0 时唤醒等待者);常用于「资源计数」(如 N 个共享资源)或「互斥」(二值 semaphore)。SysV 分配在*集合*中(一个 semget 可创建多个),单次 semop 可原子操作多个。

      • semget 创建/打开:同 SysV IPC 模式——semget(key, nsems, mode[, IPC_CREAT][, IPC_EXCL]);返回 set id(不是单个 sem);nsems 是 set 中 sem 数。

      • semop 原子操作struct sembuf { sem_num, sem_op, sem_flg }sem_op > 0 加、sem_op == 0 等 0(否则阻塞)、sem_op < 0 减(值够才立即执行,否则阻塞);一次可执行 N 个操作(原子)。

      • semctl 控制:GETVAL/SETVAL/GETALL/SETALL/GETPID/GETNCNT/GETZCNT/IPC_RMID/IPC_STAT/IPC_SET;UNION semun 必须程序自己定义。

      • 初始化竞态:get 与 setval 是两次调用,peer 进程要「先创建者用 sem_otime 标志初始化完成」——多进程协作的「谁的 init」问题。

      • SEM_UNDO 限制:进程退出时自动撤销 semop,但「撤销值过大」会丢;Linux 限制 semadj ≤ SEMVMX (32767);SETVAL/SETALL 会清零所有进程的 semadj。

      本章主旨

      SysV 信号量是「内核维护的整数 + 原子操作」——专门为*同步*而非数据传输设计。本章覆盖 (1) 信号量的本质(P/V 操作 + 集合概念);(2) semget 创建;(3) semop 原子操作(三类操作:加、等、减);(4) semctl 控制(含 semun union);(5) 初始化竞态与 sem_otime 协议;(6) SEM_UNDO 风险;(7) Limits 与替代品。读者应理解「binary sem」是更易用的常见协议——SysV 提供的 semop 集合操作虽灵活但 API 复杂;写库用「1 个 sem 表示 0/1」+ reserveSem/releaseSem 包装是常见做法。

      一、核心概念

      本章围绕 6 个核心概念展开:从信号量本质、semget/semop/semctl、init 竞态、SEM_UNDO、二值信号量、limits。

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

      信号量本质(集合概念)

      内核维护非负整数;P 减、V 加;SysV 一次 get 可创建 N 个(集合),单次 semop 可原子操作 N 个——比 POSIX sem 灵活但 API 复杂

      §47.0-1;semun 联合必须程序自己定义(SUSv3);semget 返回 set id(不是单个 sem id)

      semop 三类操作

      sem_op > 0 加(value += sem_op);sem_op == 0 等 0(value != 0 阻塞);sem_op < 0 减(value ≥ |sem_op| 才立即执行);一次多操作原子

      §47.6;struct sembuf { sem_num, sem_op, sem_flg };flags 可 IPC_NOWAIT、SEM_UNDO

      semctl 控制

      GETVAL/SETVAL/GETALL/SETALL 取设值;GETPID/GETNCNT/GETZCNT 取 wait 信息;IPC_RMID 删;IPC_STAT/SET 改关联数据

      §47.3;UNION semun SUSv3 强制程序自己定义;SETVAL/SETALL 唤醒等待者(且清所有 semadj)

      初始化竞态(sem_otime 协议)

      多 peer 进程同时 init 时——「谁的 init 是合法的」用 sem_otime != 0 协议:先创 set 的人用 sem_op=0 触发 sem_otime 变更

      §47.5;BSD 一些版本不更新 sem_otime,可移植代码慎用

      SEM_UNDO 风险

      进程退出时自动撤销 semop——per-process semadj 总和;不能超过 SEMVMX (32767);SETVAL/SETALL 清零所有进程的 semadj;fork 不继承

      §47.8;不能完全代替「进程退出释放资源」——资源本身可能还泄漏;通常仍要 atexit/exit handler 清理

      二进制信号量协议

      1 表示可用、0 表示占用;reserve = sem_op=-1(可能阻塞);release = sem_op=+1;binary_sems.c(Listing 47-10)实现 reserveSem/releaseSem + bsUseSemUndo/bsRetryOnEintr 选项

      §47.9;用于「单一共享资源互斥」——比 full semop 简单;常作为更上层 API

      二、详细笔记

      47.1 概述 + 信号量集

      What:SysV 信号量分配在「集合」中——semget(key, nsems, …​) 一创建就是 N 个;操作时指定 sem_num 索引。

      Why:让「多 sem 原子操作」成为可能(一个 semop 可同时操作 N 个——保证 N 个资源的「全部可用」才进入临界区)。

      How——四步使用流程(来自 §47.1):

      1. semget() 创建/打开 set。

      2. semctl(…​ SETVAL/SETALL) 初始化——只一个进程做。

      3. semop() 实际操作。

      4. semctl(…​ IPC_RMID) 删——只一个进程做。

      When:所有 SysV sem 程序都按此流程。

      47.2 semget 创建/打开

      Whatsemget(key, nsems, mode[, IPC_CREAT][, IPC_EXCL]) 创建或打开 N 个 sem 的集合。

      Why:与 msgget/shmget 一致的「key → id」入口。

      How

      // 摘自《The Linux Programming Interface》 第 47 章
      #include <sys/sem.h>
      int semget(key_t key, int nsems, int semflg);
      
      /* 创建一个 sem 的集合 */
      int semid = semget(IPC_PRIVATE, 1, 0660);   /* IPC_PRIVATE 一定新建 */
      int semid = semget(key, 5, IPC_CREAT | 0660);  /* 找或建 5 个 sem 的集 */
      int semid = semget(key, 0, 0);                  /* 找(取已存在 id) */

      nsems: . 创建时 = set 中 sem 数(> 0)。 . 打开时 ≤ set 实际大小(否则 EINVAL)。 . 改不了大小——一创建定终身。

      When:写 sem 程序几乎都用此入口。

      47.3 semctl 控制

      Whatsemctl(semid, semnum, cmd, …​) 控制单个 sem 或 set。

      Why:初始化值、查状态、删除、改权限。

      How——UNION semun 必定义(SUSv3 强制):

      // 摘自《The Linux Programming Interface》 第 47 章 Listing 47-2
      union semun {
          int                 val;       /* SETVAL */
          struct semid_ds *   buf;       /* IPC_STAT, IPC_SET */
          unsigned short *    array;     /* SETALL, GETALL */
      #if defined(__linux__)
          struct seminfo *    __buf;     /* IPC_INFO, SEM_INFO */
      #endif
      };

      常见 cmd

      /* 初始化 set 中所有 sem */
      unsigned short vals[5] = {1, 0, 1, 0, 1};
      union semun arg;
      arg.array = vals;
      semctl(semid, 0, SETALL, arg);
      
      /* 取第 3 个 sem 的值 */
      int v = semctl(semid, 2, GETVAL, dummy);
      
      /* 删 set */
      semctl(semid, 0, IPC_RMID, dummy);

      注意SETVAL 会清除所有进程的 semadj 记录(SETALL 同)——见 §47.8。

      When:server 启动时 SETALL 一次;监控用 GETVAL/GETNCNT/GETZCNT。

      47.4 semid_ds 关联数据

      Whatstruct semid_ds 4 字段:sem_perm/sem_otime(最后 semop 时间)/sem_ctime(最后变化时间)/sem_nsems(set 中 sem 数)。

      Why:记录 set 状态;监控用。

      How

      // 摘自《The Linux Programming Interface》 第 47 章
      struct semid_ds ds;
      union semun arg = {.buf = &ds};
      semctl(semid, 0, IPC_STAT, arg);
      printf("nsems: %lu, last semop: %s\n", ds.sem_nsems, ctime(&ds.sem_otime));

      When:写监控/管理工具时读。

      47.5 初始化竞态

      What:semget 和 semctl SETVAL 是两次 syscall;多 peer 进程「都想 init 时」——可能出现「A 创建 + 还没 SETVAL 时 B 已用」。

      Why:用未初始化的 sem 是灾难性的——值任意。

      How——sem_otime 协议(来自 §47.5 Listing 47-6):

      // 摘自《The Linux Programming Interface》 第 47 章 Listing 47-6
      semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
      if (semid != -1) {                              /* 我们是创建者 */
          union semun arg = {.val = 0};
          semctl(semid, 0, SETVAL, arg);                /* init */
          struct sembuf sop = {.sem_num = 0, .sem_op = 0};  /* wait for 0 */
          semop(semid, &sop, 1);                        /* 触发 sem_otime 变更 */
      } else {                                          /* 已存在 */
          int tries = 10;
          while (tries-- > 0) {
              struct semid_ds ds;
              union semun arg = {.buf = &ds};
              semctl(semid, 0, IPC_STAT, arg);
              if (ds.sem_otime != 0) break;              /* 已被 init */
              sleep(1);
          }
          if (tries == 0) fatal("Existing semaphore not initialized");
      }

      Why this works:sem_otime 仅在 semop 成功后变更;创建者用 sem_op=0(等 0)后立即通过;后续「打开者」看到 sem_otime != 0 即可安心使用。

      When:多个 peer 进程可创建/初始化同一 sem 时必用——单 server 创建可省略。

      Limitation:BSD 一些版本 semop 不更新 sem_otime;可移植代码要小心。

      47.6 semop 原子操作

      Whatsemop(semid, sops, nsops) 原子执行 1..N 个 sembuf 操作。

      Why:让「多 sem 协调」成为可能——不会被打断。

      How——三类操作(来自 §47.6):

      // 摘自《The Linux Programming Interface》 第 47 章
      struct sembuf sops[3] = {
          {.sem_num = 0, .sem_op = -1, .sem_flg = 0},                  /* 减 1 */
          {.sem_num = 1, .sem_op = +2, .sem_flg = 0},                  /* 加 2 */
          {.sem_num = 2, .sem_op =  0, .sem_flg = IPC_NOWAIT},         /* 等 0 (非阻塞) */
      };
      semop(semid, sops, 3);    /* 全部按序执行,原子 */

      操作语义: . sem_op > 0:value += sem_op;唤醒等待者;需 alter (write) 权限。 . sem_op == 0:value == 0 才立即通过;否则阻塞;需 read 权限。 . sem_op < 0:value ≥ |sem_op| 才立即通过;否则阻塞;需 alter 权限。

      atomicity:所有 N 个 op 要么全做要么全不做;中间状态对其他进程不可见。

      flags: . IPC_NOWAIT:不阻塞;条件不满足立即 EAGAIN。 . SEM_UNDO:记录 undo——进程退出时反向操作。

      EINTR/EIDRM:信号打断 → EINTR(不自动重启);set 被 IPC_RMID → EIDRM。

      When:所有 sem 程序都依赖 semop 的「阻塞」+「原子性」。

      47.7 多个阻塞 op 的处理顺序

      What:当多个进程同时等待 sem 时——减量相同时顺序未定义;减量不同时按「能完成」顺序;可能*饿死*(starvation)。

      Why:不正确的 sem 设计会导致「某些进程永远等不到」。

      How——starvation 例子(来自 §47.7):

      1. sem 初始值 0;A 想减 2;B 想减 1。

      2. 第三方加 1。

      3. B 先得(因为 B 要 1 即可,A 要 2)。

      4. 如果系统不断「+1 → B 走 → +1 → B 走」,A 永远等不到 2。

      When:设计多 sem 协议时务必想 starvation 场景——用「所有 sem 同号」或「有序等待」避免。

      47.8 SEM_UNDO 风险

      WhatSEM_UNDO 让进程退出时自动撤销之前 semop 的影响——per-process semadj 总和。

      Why:进程异常退出时不留「资源被占用」状态。

      How

      struct sembuf sop = {.sem_num = 0, .sem_op = -1, .sem_flg = SEM_UNDO};
      semop(semid, &sop, 1);   /* value--; 进程退出时 value++ */

      但有限制(§47.8):

      1. 不能完全恢复资源——只恢复 sem,没恢复资源本身(如未释放的文件锁、未关闭的 fd)。

      2. Linux 处理「调整时 value 不足」——减到 0 后跳过;SUSv3 留空。

      3. 跨 SEMVMX (32767) 上限——undo 时 value > SEMVMX 也强加,导致 sem 值越界。

      4. SETVAL/SETALL——会清所有进程的 semadj(破坏性)。

      5. fork — 不继承;exec — 保留。

      6. Linux 2.6 + NPTL:threads 共用 semadj(CLONE_SYSVSEM)。

      When:仅在「进程退出时确实想撤销 sem 影响」的场景使用——常见「signal handler 跳过退出」的进程不必用。

      47.9 二值信号量协议

      What:binary semaphore——只用 0/1 两个值表示「可用/占用」;reserve=减、release=加;比 full semop 简单。

      Why:大多数应用只要互斥——用 binary 协议简单。

      How——binary_sems.c(Listing 47-10):

      // 摘自《The Linux Programming Interface》 第 47 章
      int initSemAvailable(int semId, int semNum) {
          union semun arg = {.val = 1};
          return semctl(semId, semNum, SETVAL, arg);
      }
      int initSemInUse(int semId, int semNum) {
          union semun arg = {.val = 0};
          return semctl(semId, semNum, SETVAL, arg);
      }
      int reserveSem(int semId, int semNum) {
          struct sembuf sops = {.sem_num = semNum, .sem_op = -1,
                                .sem_flg = bsUseSemUndo ? SEM_UNDO : 0};
          while (semop(semId, &sops, 1) == -1)
              if (errno != EINTR || !bsRetryOnEintr) return -1;
          return 0;
      }
      int releaseSem(int semId, int semNum) {
          struct sembuf sops = {.sem_num = semNum, .sem_op = +1,
                                .sem_flg = bsUseSemUndo ? SEM_UNDO : 0};
          return semop(semId, &sops, 1);
      }

      全局开关bsUseSemUndo(是否带 SEM_UNDO)、bsRetryOnEintr(EINTR 是否 retry)。

      When:写 SysV sem 库时标准实现;用于共享内存互斥(见第 48 章 svshm_xfr)。

      47.10 SysV Sem Limits

      What:kernel 通过 /proc/sys/kernel/sem(4 个值)限制 sem。

      Why:防资源耗尽。

      How——Limits(来自 §47.10 Table 47-1):

      限制 含义 Linux 调整

      SEMMNI

      最大 set 数

      ≤ 32768 (IPCMNI);/proc/sys/kernel/sem 第 4 个

      SEMMSL

      set 最大 sem 数

      ≤ 65536;实际推荐 ≤ 8000;sem 第 1

      SEMMNS

      系统级总 sem 数

      ≤ INT_MAX;sem 第 2

      SEMOPM

      单次 semop 最大操作数

      推荐 ≤ 1000;sem 第 3

      SEMVMX

      sem 最大值

      固定 32767

      SEMAEM

      semadj 最大值

      固定 32767

      查看

      $ cat /proc/sys/kernel/sem
      250     32000   32      128     # SEMMSL SEMMNS SEMOPM SEMMNI
      $ sudo sh -c "echo '500 64000 64 256' > /proc/sys/kernel/sem"

      When:装高并发 sem 程序时调 SEMMNS。

      47.11 SysV Sem 缺点

      What:SysV sem 与 SysV msg 共享许多设计缺陷。

      Why:新代码用 POSIX sem 或其他机制更优。

      How——7 个主要缺点(来自 §47.11):

      1. int id 非 fd:不能 select/poll/epoll。

      2. key 而非 pathname:定位比 fd 麻烦。

      3. get/setval 分开:要小心 init 竞态。

      4. 无引用计数:何时删需要协调。

      5. API 复杂:集合概念、超集 op 罕见用。

      6. limits 复杂:要在装服务器时调。

      7. 替代品丰富:POSIX sem(53 章)、file lock(55 章)。

      When:避免新代码用 SysV sem;维护老代码理解即可。

      三、关键图表

      非可视化条目(semop 操作与 semctl 命令)
      操作 行为 阻塞条件

      sem_op > 0

      value += sem_op;唤醒等 0/减的

      不阻塞

      sem_op == 0

      value == 0 通过;否则阻塞

      value != 0

      sem_op < 0

      value ≥ |sem_op| 时 value -= sem_op;否则阻塞

      value < |sem_op|

      semctl 命令一览
      命令 描述

      GETVAL

      取 semnum 索引 sem 的当前值

      SETVAL

      初始化 semnum 索引 sem 为 arg.val

      GETALL

      取全部 sem 值到 arg.array

      SETALL

      初始化全部 sem 为 arg.array

      GETPID

      取最后 semop 的 PID

      GETNCNT

      取等 value 增加的进程数

      GETZCNT

      取等 value == 0 的进程数

      IPC_RMID

      立即删除 set

      IPC_STAT

      取 semid_ds 到 arg.buf

      IPC_SET

      写 semid_ds 中部分字段

      IPC_INFO

      Linux 扩展:返 seminfo

      SEM_INFO

      Linux 扩展:取资源使用

      SEM_STAT

      Linux 扩展:按 index 取 semid

      四、思维导图

      mindmap
        root((第 47 章 SysV 信号量))
          集合概念
            semget N 个 sem
            semop 原子多 op
          semop 三类
            加 sem_op 大于 0
            等 0 sem_op 等 0
            减 sem_op 小于 0
          semctl
            GETVAL SETVAL
            GETALL SETALL
            IPC RMID STAT SET
            UNION semun 自定义
          init 竞态
            sem_otime 协议
            创建者先 SETVAL
            sem_op 0 触发 otime
          SEM_UNDO
            semadj 记录
            进程退出撤销
            SEMVMX 32767 限制
          二值 sem
            0 占用 1 可用
            reserve release
            binary_sems.c
          limits
            SEMMNI SEMMSL SEMMNS
            SEMOPM SEMVMX
            proc sys kernel sem

      五、重点与易错点

      1. 信号量是非负整数——所有操作保证不降到 0 以下;阻塞语义是「值不够时挂起」。

      2. 集合(set)是 SysV 特点——一个 semget 创建 N 个;一次 semop 可操作 N 个(原子);比 POSIX sem 复杂但灵活。

      3. semop 多操作原子——3.4 章 N 个 sops 全部要能完成才做;有一个不能就全不做并阻塞。

      4. sem_op == 0 用来「等 0」——常用于「全部释放」后才让一个进程继续;也可用于 init 竞态 协议。

      5. UNION semun 必须程序自己定义(SUSv3 强制)——#include "semun.h";glibc < 2.1 提供,新 glibc 不再提供(_SEM_SEMUN_UNDEFINED macro)。

      6. init 竞态用 sem_otime 协议——「创建者用 sem_op=0 触发 otime 变化」+「非创建者轮询 otime != 0」;不依赖 fork 顺序。

      7. SEM_UNDO 谨慎使用——只能回滚 sem 影响,不能释放资源;过 SEMVMX 越界;SETVAL/SETALL 会清所有进程 semadj。

      8. SEM_UNDO 不 fork 继承——子进程不继承父的 semadj;exec 保留。

      9. 阻塞 op 的顺序:相同减量顺序不定;不同减量按「能完成」顺序;可能 starvation——设计要避免「某些 op 永远完不成」。

      10. binary semaphore 是简化协议——大多数应用只要互斥;reserve/release 比 semop 全语义简单得多。

      11. semget 的 nsems 不可改——创建后不能扩缩。

      12. SETVAL 会唤醒等待者——如果新值满足之前阻塞的 op 条件,OS 自动唤醒。

      13. Linux 的 SEMVMX 固定 32767——sem 不能涨到此值以上(除非 SEM_UNDO 越界)。

      14. limits 在 /proc/sys/kernel/sem——4 个空格分隔值;可 echo 改(运行时)。

      15. 跨章衔接:第 38 章 set-UID 安全;第 48 章 SysV shm 用 sem 协调;第 53 章 POSIX sem;第 55 章 file lock。