第 48 章 System V 共享内存 (System V Shared Memory)

      +

      核心结论

      • 共享内存本质:多个进程把同一物理页映射到自己的虚拟地址空间——「无内核介入」最快的 IPC;写后其他进程立即可见。

      • shmget 创建/打开shmget(key, size, shmflg) 创建/打开指定字节的 shm 段(按页对齐);返回 shmid。

      • shmat / shmdt 映射/解除shmat(id, NULL, 0) 把 shm attach 到进程虚拟地址空间返回指针(推荐 NULL 让内核选地址);shmdt(addr) 解除(进程退出自动 detach)。

      • 共享内存坐标 = 指针 vs 偏移:因为 shm 在不同进程可能 attach 到不同地址——存储「指针」用 偏移(相对 baseaddr),不要存绝对指针;链表/树用 offset 串起来。

      • shmctl 控制:IPC_RMID 标记待删(nattch=0 才真删);IPC_STAT/SET;SHM_LOCK/UNLOCK 锁页(2.6.10+ 非特权可锁 own 段)。

      • 典型用法:和 SysV 信号量(binary sema)配合——一个 sem 控制「共享内存互斥访问」,一个 sem 控制「读/写轮流」——见 svshm_xfr_writer/reader.c

      • Limits:SHMMNI (段数)、SHMMAX (单段最大)、SHMALL (系统总页);/proc/sys/kernel/shm* 调整;SHM_HUGETLB 用 huge pages。

      本章主旨

      SysV 共享内存是「最快的 IPC」——一次 shmat 后,进程对 shm 内存的读写直接命中物理页,不经内核。本章覆盖 (1) shmget/shmat/shmdt/shmctl 的具体语义;(2) shmid_ds 字段;(3) 偏移而非指针存储(关键点!);(4) 与 sem 配合的经典模式(svshm_xfr);(5) /proc/PID/maps 中的表现。读者应理解 shm 不是「IPC 自动同步」——必须用 sem 或 file lock 协调;用绝对指针会因不同进程 attach 地址不同而崩溃。

      一、核心概念

      本章围绕 6 个核心概念展开:从 shm 速度本质、shmget/shmat/shmdt、shmid_ds、shmctl、坐标存储、Limits。

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

      shm 本质(最快 IPC)

      多进程把同一物理页映射到自己虚拟地址;写后其他进程立即可见;不需内核介入;user→user 而非 user→kernel→user

      §48.0;shm 不是「自动同步」——必须用 sem/file lock;shm 段用 tmpfs 实现(§48.5)

      shmget + shmat + shmdt

      shmget(key, size, …​) 创建/打开 N 字节段(按页对齐);shmat(id, NULL, 0) attach 返回指针;shmdt(ptr) 解除

      §48.2-3;shmat NULL 让内核选地址(最常见);SHM_RDONLY 只读 attach;shmat 后像普通 C 指针

      shmid_ds 关联数据

      shm_perm/shm_segsz/shm_atime/shm_dtime/shm_ctime/shm_cpid/shm_lpid/shm_nattch——nattch = 0 时段被实际删除

      §48.8;shm_perm.mode 含 SHM_DEST(待删)和 SHM_LOCKED(已锁)两个 read-only flag

      shmctl IPC_RMID 延迟删除

      IPC_RMID 把 shm 段标记为「待删除」;nattch=0 时才真删——不阻塞 detach 进程;Linux 还允许「已 IPC_RMID 但 nattch>0」的 attach(其他 UNIX 多禁止)

      §48.7;server 创建段后立即 IPC_RMID——「unlink 立即删」语义;不需要最末用户记 IPC_RMID

      坐标用偏移(关键)

      shm 在不同进程 attach 到不同地址——结构内的「指针」用 offset(相对 baseaddr);*(int *)p = (int)((char *)target - (char *)baseaddr);dereference 时 baseaddr + *p

      §48.6;树/链表在 shm 里都用 offset 串起来;用绝对指针 = 数据损坏

      shm + sem 协调模式

      svshm_xfr_writer + svshm_xfr_reader:两个 sem (WRITE_SEM, READ_SEM) 控制「writer 写时 reader 等;reader 读时 writer 等;EOF = cnt=0」——经典 1-reader-1-writer 协议

      §48.4;binary_sems.c 的 reserveSem/releaseSem 简化实现;shm 段含 struct shmseg { int cnt; char buf[BUF_SIZE]; }

      二、详细笔记

      48.1-48.2 shmget 创建/打开

      Whatshmget(key, size, shmflg) 创建/打开共享内存段。

      Why:最快的 IPC——写后立即可见。

      How

      // 摘自《The Linux Programming Interface》 第 48 章
      #include <sys/shm.h>
      int shmget(key_t key, size_t size, int shmflg);    /* 返 shmid 或 -1 */
      
      /* 创建 1K 段 */
      int shmid = shmget(IPC_PRIVATE, 1024, 0660);
      
      /* 打开已存在 */
      int shmid = shmget(key, 0, 0);    /* size 忽略但须 ≤ 实际大小 */

      shmflg flags: . 9 个低 bit = mode。 . IPC_CREAT/IPC_EXCL 同其他 SysV IPC。 . SHM_HUGETLB(Linux 2.6+):特权 (CAP_IPC_LOCK) 才能用——用 huge pages 减少 TLB miss。 . SHM_NORESERVE(Linux 2.6.15+):不预留 swap(同 MAP_NORESERVE)。

      size 按页对齐——实际段大小 = ceil(size / page_size) * page_size。

      When:server 启动用 shmget(IPC_PRIVATE, …​) 或 ftok 后 shmget(key, IPC_CREAT, …​)

      48.3 shmat + shmdt 映射/解除

      What:把 shm 段 attach 到当前进程的虚拟地址空间;返回的指针可像普通 C 指针使用。

      Why:attach 后程序可读写;detach 后虚拟地址释放。

      How——推荐用法(NULL 让内核选地址):

      // 摘自《The Linux Programming Interface》 第 48 章
      void *shmat(int shmid, const void *shmaddr, int shmflg);
      int   shmdt(const void *shmaddr);
      
      /* NULL shmaddr = 内核选地址(最安全) */
      struct shmseg *shmp = (struct shmseg *)shmat(shmid, NULL, 0);
      if (shmp == (void *)-1) errExit("shmat");
      
      /* 只读 attach */
      shmp = (struct shmseg *)shmat(shmid, NULL, SHM_RDONLY);
      
      /* 解除 */
      shmdt(shmp);

      shmflg flags: . SHM_RDONLY:只读 attach。 . SHM_RND:shmaddr 非空时,向下舍入到 SHMLBA 倍数(避免缓存一致性问题)。 . SHM_REMAP(Linux 扩展):替换 shmaddr 处的旧 mapping。

      shmaddr 建议*:永远传 NULL——非空是高级用法(在不同进程固定地址、缓存控制)。

      attach 权限:shmat 需要 r/w 权限(除非 SHM_RDONLY)。

      exec + shm: . fork() 继承 shm(子共享 attach)。 . exec() 自动 detach 所有 shm。 . 进程终止自动 detach。

      When:所有 shm 程序都按此模式。

      48.4 svshm_xfr——shm + sem 经典实例

      Whatsvshm_xfr_writer.c + svshm_xfr_reader.c —— 1-reader-1-writer 块传输协议。

      Why:理解 shm 与 sem 如何配合的实际模板。

      How——3 文件:

      // 摘自《The Linux Programming Interface》 第 48 章 Listing 48-1
      /* svshm_xfr.h */
      struct shmseg {
          int  cnt;                          /* buf 字节数 */
          char buf[BUF_SIZE];                /* 数据 */
      };
      #define SHM_KEY 0x1234
      #define SEM_KEY 0x5678
      #define WRITE_SEM 0
      #define READ_SEM  1
      // 摘自《The Linux Programming Interface》 第 48 章 Listing 48-2
      /* writer:先 init 两个 sem(自己 init sem),create shm,循环 reserve WRITE_SEM */
      /* 写数据 → release READ_SEM;EOF = cnt=0 */
      int main(void) {
          int semid = semget(SEM_KEY, 2, IPC_CREAT | OBJ_PERMS);
          initSemAvailable(semid, WRITE_SEM);
          initSemInUse(semid, READ_SEM);
          int shmid = shmget(SHM_KEY, sizeof(struct shmseg), IPC_CREAT | OBJ_PERMS);
          struct shmseg *shmp = shmat(shmid, NULL, 0);
          for (;;) {
              reserveSem(semid, WRITE_SEM);
              shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
              releaseSem(semid, READ_SEM);
              if (shmp->cnt == 0) break;
          }
          /* 清理:等 reader 完成后删 */
          reserveSem(semid, WRITE_SEM);
          semctl(semid, 0, IPC_RMID, NULL);
          shmdt(shmp);
          shmctl(shmid, IPC_RMID, NULL);
      }
      // 摘自《The Linux Programming Interface》 第 48 章 Listing 48-3
      /* reader:循环 reserve READ_SEM;写 stdout;release WRITE_SEM;EOF 退出 */
      shmp = shmat(shmid, NULL, SHM_RDONLY);
      for (;;) {
          reserveSem(semid, READ_SEM);
          if (shmp->cnt == 0) break;
          write(STDOUT_FILENO, shmp->buf, shmp->cnt);
          releaseSem(semid, WRITE_SEM);
      }
      shmdt(shmp);
      releaseSem(semid, WRITE_SEM);    /* 通知 writer 可清理 */

      When:写「shm 块传输」应用的标准模板;可扩到 N-reader-N-writer(用 4 个 sem)。

      48.5 共享内存虚拟地址位置

      What:shm attach 的虚拟地址在 task_unmapped_base 之上(x86-32 = 0x40000000);mmap 也在同一区域。

      Why:理解 /proc/PID/maps 的输出——shm 段 vs mmap vs shared library。

      How——/proc/PID/maps 行:

      b7bed000-b7f0d000 rw-s 00000000 00:09 9666565  /SYSV00000000 (deleted)
      b7f0d000-b7f26000 rw-s 00000000 00:09 9633796  /SYSV00000000 (deleted)

      解读: . 起始-结束地址 . 权限 rw-s(read+write+shared) . 文件偏移(shm 总是 0) . dev:inode(shm 是 tmpfs 文件的 inode) . 名称:/SYSV<key>(IPC_PRIVATE 时是 0) . (deleted):tmpfs 文件被 unlink 但 mapping 还在——是 Linux 共享内存的实现方式

      When:调试时 /proc/PID/maps 是最直接的 shm 调试工具。

      48.6 共享内存中存指针——必须用偏移

      What:shm 在不同进程 attach 到不同地址——存「绝对指针」会让读端拿到错的值。

      Why:*最常见 shm bug*之一。

      How——target = p 的正确做法:

      // 摘自《The Linux Programming Interface》 第 48 章
      char *baseaddr = (char *)shmat(...);  /* 自己 attach 的地址 */
      
      /* 设:p 指向的字段应存 target 的「相对 baseaddr 的偏移」 */
      *((long *)p) = (long)(target - baseaddr);
      
      /* 用:把偏移加回 baseaddr 得到 target */
      target = baseaddr + *((long *)p);
      ```
      
      或者用「数组 index」——把 shm 当成 fixed-size struct 数组,index 就是天然 offset。
      
      *When*:在 shm 里建链表/树/图结构时永远用 offset。
      
      === 48.7 shmctl 控制
      
      *What*:`shmctl(shmid, cmd, buf)` 提供控制操作。
      
      *Why*:删除、统计、锁页、改权限。
      
      *How*——常见 cmd:
      
      [source,c]

      shmctl(shmid, IPC_RMID, NULL); /* 标记待删(nattch=0 时真删) / shmctl(shmid, IPC_STAT, &ds); / 取 shmid_ds / shmctl(shmid, IPC_SET, &ds); / 改 uid/gid/mode / shmctl(shmid, SHM_LOCK, NULL); / 锁页(特权 / own segment + RLIMIT_MEMLOCK) / shmctl(shmid, SHM_UNLOCK, NULL); / 解锁 */

      **IPC_RMID 立即删 vs 标记删**:
      . shm 不像 msg/sem 立即删;标记为「待删」等 nattch=0。
      . 应用模式:server 创建 + attach 后立即 `IPC_RMID`——「unlink 立即删」语义;最末用户 detach 时实际删除。
      . Linux 还允许「已 IPC_RMID 但 nattch>0」的 attach(其他 UNIX 多禁止)——可移植代码不要依赖。
      
      *SHM_LOCK 锁页*:
      . 把 shm 锁在物理 RAM——不被 swap。
      . 2.6.10 之前需要 CAP_IPC_LOCK。
      . 2.6.10+ 非特权可锁 own 段(需 RLIMIT_MEMLOCK)。
      
      *When*:server 启动 shm 后立即 `IPC_RMID`;高频实时处理时考虑 `SHM_LOCK`。
      
      === 48.8 shmid_ds 关联数据
      
      *What*:每 shmid 对应 `struct shmid_ds` 9 字段(§48.8)。
      
      *Why*:决定 IPC_STAT/SET 行为、决定何时真删。
      
      *How*——关键字段:
      
      [source,c]

      struct shmid_ds { struct ipc_perm shm_perm; /* 权限 / size_t shm_segsz; / 段大小(创建时请求) / time_t shm_atime; / 最后 shmat / time_t shm_dtime; / 最后 shmdt / time_t shm_ctime; / 最后变化 / pid_t shm_cpid; / 创建者 PID / pid_t shm_lpid; / 最后 shmat/shmdt PID / shmatt_t shm_nattch; / 当前 attach 数 */ };

      `shm_perm.mode` 含两个 read-only flag:
      . `SHM_DEST`:段被标记待删。
      . `SHM_LOCKED`:段已被锁页。
      
      *When*:监控用 `IPC_STAT` 读;`nattch==0` 才是真删。
      
      === 48.9 共享内存限制
      
      *What*:kernel 限制 SysV shm 数量与大小。
      
      *Why*:防资源耗尽。
      
      *How*——Limits(来自 §48.9 Table 48-2):
      
      [cols="1,3,2", options="header"]
      |===
      | 限制 | 含义 | Linux 调整
      
      | SHMMNI
      | 最大段数
      | ≤ 32768 (IPCMNI);`/proc/sys/kernel/shmmni`
      | SHMMAX
      | 单段最大字节
      | 实际由 RAM + swap 决定;`/proc/sys/kernel/shmmax`
      | SHMALL
      | 系统级总页数
      | 实际由 RAM + swap 决定;`/proc/sys/kernel/shmall`
      | SHMMIN
      | 最小段大小
      | 固定 1(实际 = page size)
      |===
      
      *Linux 没有 SHMSEG*(per-process 限制)。
      
      *SHM_HUGETLB 2.6+*:用 huge page 减少 TLB 压力。
      
      *When*:装大内存应用时调 SHMMAX + SHMALL;做实时/科学计算时考虑 SHM_HUGETLB。
      
      == 三、关键图表
      
      [NOTE]
      .非可视化条目(shm 关键 syscall 与 flags)
      ====
      [cols="1,3", options="header"]
      |===
      | 项 | 描述
      
      | `shmget(key, size, shmflg)`
      | 创建/打开;按页对齐
      | `shmat(id, NULL, 0)`
      | attach 到虚拟地址;返指针
      | `shmat(id, NULL, SHM_RDONLY)`
      | 只读 attach
      | `shmdt(ptr)`
      | 解除 attach
      | `shmctl(id, IPC_RMID, NULL)`
      | 标记待删(nattch=0 时真删)
      | `shmctl(id, IPC_STAT/SET, &ds)`
      | 读/写 shmid_ds
      | `shmctl(id, SHM_LOCK/UNLOCK, NULL)`
      | 锁页 / 解锁
      | `SHM_HUGETLB`
      | Linux:用 huge pages
      | `SHM_NORESERVE`
      | Linux:不预留 swap
      | `SHM_RDONLY / SHM_RND / SHM_REMAP`
      | 只读 / 舍入 / 替换
      | `fork()`
      | 继承 shm attach
      | `exec()`
      | detach 所有 shm
      | `_exit()`
      | 自动 detach
      | `struct shmid_ds`
      | 9 字段:perm/segsz/atime/dtime/ctime/cpid/lpid/nattch
      | `shm_nattch`
      | 当前 attach 进程数;0 = 真删
      | `shm_perm.mode`
      | 含 SHM_DEST + SHM_LOCKED 标志
      | `proc PID maps`
      | 看 shm 映射:`rw-s ... /SYSV<key> (deleted)`
      | Limits: SHMMNI/SHMMAX/SHMALL
      | 调优点
      |===
      ====
      
      == 四、思维导图
      
      [source,mermaid]

      mindmap root第 48 章 SysV 共享内存 最快 IPC user user 而非 user kernel user 共享物理页 无自动同步 shmget 按页对齐 IPC_PRIVATE 或 ftok SHM_HUGETLB shmat shmdt NULL 让内核选地址 SHM_RDONLY 只读 exec 时自动 detach 偏移而非指针 不同进程 attach 不同地址 target baseaddr 数组 index svshm_xfr 模式 writer reserve WRITE SEM read 写 shm release READ SEM reader 反之 cnt 0 EOF shmid_ds nattch 0 才真删 SHM_DEST 标志 SHM_LOCKED IPC_RMID 立即删 server create 后立即删 nattch 0 时真删 limits SHMMNI 段数 SHMMAX 单段 SHMALL 总页

      五、重点与易错点

      1. shm 是最快的 IPC 但要自己同步——必须有 sem 或 file lock 配合;不要以为「写后立即可见」就够了。

      2. 存指针用偏移——target = baseaddr + *p;绝对指针在不同进程 attach 地址不同时会错位(最常见 shm bug)。

      3. shmget 按页对齐——实际段大小 = ceil(size / page_size) * page_size;多分配一些无妨。

      4. shmat(NULL, …​) 推荐——非 NULL 是高级用法(避免 cache aliasing 等),多数情况 NULL 就够。

      5. shmat 允许多次 attach——同一进程可多次 attach;权限可不同(read-only + read-write 混用)。

      6. shmctl(IPC_RMID) 标记待删——nattch=0 时真删;不是立即删。

      7. 「server 创建后立即 IPC_RMID」是推荐模式——最末 detach 时实际删除;不需要最末用户记 IPC_RMID。

      8. fork 继承 shm——父子共享 attach;exec 自动 detach。

      9. SHM_LOCK 锁页——2.6.10+ 非特权可锁 own 段(用 RLIMIT_MEMLOCK);实时应用考虑。

      10. Linux 允许「已 IPC_RMID 但 nattch>0」时 attach——可移植代码不要依赖(其他 UNIX 多禁止)。

      11. SHM_HUGETLB——2.6+;用 huge pages 减少 TLB miss;需 CAP_IPC_LOCK。

      12. Limits 调优——/proc/sys/kernel/shmmax/shmall/shmmni 调大后装大内存应用。

      13. tmpfs 实现——shm 段本质是 tmpfs 文件 unlink 后的引用;/proc/PID/maps 显示 /SYSV<key> (deleted)

      14. 0 长度 mtext——writer 用 cnt=0 表示 EOF(reader 检测 cnt==0 退出)。

      15. 跨章衔接:第 24 章 fork;第 27 章 exec;第 44 章 管道;第 47 章 SysV sem(与 shm 配合);第 49 章 mmap;第 54 章 POSIX shm。

      Asciidoc lint check

      asciidoctor: 无警告。