Linux内核分析之进程间通信-02
28.1 System V IPC 通用机制 —— ipc_ids 与 kern_ipc_perm
信号量/消息队列/共享内存三个子系统共享一个对象管理层:每 IPC namespace 一份 ipc_ids[3],每个 IPC 对象以 kern_ipc_perm 开头(权限/属主/键/序号)。这一层解决三件所有 SysV IPC 都需要的事:key→id 的命名协商、id 的防重用序号、生命周期与权限。本节拆解这个通用层。
28.1.1 双索引:idr 与 key 哈希
// include/linux/ipc_namespace.h:18-30
struct ipc_ids {
int in_use; /* 当前对象数 */
unsigned short seq; /* 全局序号种子 */
struct rw_semaphore rwsem; /* 注册/查找的写侧锁 */
struct idr ipcs_idr; /* id → 对象 的 radix 索引 */
int max_idx;
int last_idx; /* For wrap around detection */
#ifdef CONFIG_CHECKPOINT_RESTORE
int next_id;
#endif
struct rhashtable key_ht; /* key → 对象 的哈希索引 */
};
struct ipc_namespace {
struct ipc_ids ids[3]; /* [SEM] [MSG] [SHM] 三套 */
int sem_ctls[4];
...
unsigned int msg_ctlmax; /* 单条消息上限 */
unsigned int msg_ctlmnb; /* 队列总字节上限 */
unsigned int msg_ctlmni;
...
size_t shm_ctlmax; /* 单段上限 */
size_t shm_ctlall; /* 全部段总和上限 */
unsigned long shm_tot;
...
};
双索引的分工:semget(key, ...) 按 key 查找走 key_ht 哈希(ipc_findkey(),ipc/util.c:172-190,命中即 ipc_lock_object 返回);semop(id, ...) 等按 id 操作走 ipcs_idr(radix 树,id 直接编码槽位)。idr 槽位号是 id 的低位,高位是序号——下一节的防重用机制。
IPC namespace 隔离:ids[3] 挂在 namespace 上(9 章的命名空间隔离),新 namespace 三件套从零开始——容器内可以自由使用与宿主相同的 key 而互不干扰。
28.1.2 kern_ipc_perm:对象头
// include/linux/ipc.h:12-30
struct kern_ipc_perm {
spinlock_t lock; /* 对象自旋锁 */
bool deleted; /* RMID 后的墓碑标记 */
int id; /* 完整 id (序号+槽位) */
key_t key;
kuid_t uid; /* 属主 (可经 IPC_SET 改) */
kgid_t gid;
kuid_t cuid; /* 创建者 (不可改) */
kgid_t cgid;
umode_t mode; /* rwx 权限位 */
unsigned long seq; /* 序号 */
void *security; /* LSM 挂点 (52 章) */
struct rhash_head khtnode; /* key_ht 的哈希节点 */
struct rcu_head rcu; /* RCU 释放头 */
refcount_t refcount; /* 对象引用 */
} ____cacheline_aligned_in_smp __randomize_layout;
三个子系统的对象(sem_array/msg_queue/shmid_kernel)都以此结构为第一个字段——container_of 反查本体(shm.c:128 的用法)。权限模型即 Unix 文件位(0600 等)作用在 IPC 对象上:msgsnd/msgrcv/shmat 分别检查 WRITE/READ 位,属主/组/其他三级——这是 SysV IPC 与 POSIX 版(fd 权限)最直观的差异面。
deleted 墓碑是 SysV IPC 的标志性陷阱的一半:IPC_RMID 后对象并不立刻消失(shm 等待 attach 数归零),而是标记墓碑——持有旧 id 的操作返回 -EIDRM。另一半是 id 重用。
28.1.3 ipc_addid:序号防重用
// ipc/util.c:278-330(要点)
int ipc_addid(struct ipc_ids *ids, struct kern_ipc_perm *new, int limit)
{
...
if (ids->in_use >= limit)
return -ENOSPC; /* namespace 限额 (mni/mni/mni) */
id = idr_alloc(&ids->ids->ipcs_idr, ..., &new->id); /* 分配槽位 */
...
ids->in_use++;
...
new->cuid = new->uid = current_euid(); /* 创建者=属主 */
new->gid = new->egid = current_egid();
new->seq = ids->seq++; /* 序号种子递增 */
...
/* 全新 id = (seq << 位宽) | 槽位 */
}
id 的构成与重用防御:
id = seq (高位) | 槽位号 (低位)
▲ ▲
│ └ idr 分配, 对象删除后槽位可复用
└ 全局种子, 每次新建 +1, 环绕周期 = 槽位总数 × 65536
防御场景: 客户端 A 拿到 id=5, 服务端删了该对象又建了新对象,
槽位号恰巧也是 5 —— 但 seq 已变, A 手里的旧 id 高位不匹配
→ idr 查找校验 seq 失败 → -EINVAL 而不是操作到别人的对象
序号只在"新建"时递增 (seq++), 删除不动 —— 序号单调性
与 seqlock (18.2.3 节) 的思想同源: 用"版本变了吗"判别
"对象还是原来那个吗"
28.1.4 ipcget_public:get-or-create 协商
// ipc/util.c:397-430(要点)
static int ipcget_public(struct ipc_namespace *ns, struct ipc_ids *ids,
struct ipc_ops *ops, struct ipc_params *params)
{
...
if (key == IPC_PRIVATE) {
/* 匿名键: 必建新对象, key 仅作标签 */
new = ops->getnew(ns, params);
...
} else {
...
ipcp = ipc_findkey(ids, params->key); /* key 查找 */
if (ipcp) {
/* 已存在: 检查 flag 兼容性 */
if (!ipc_check_perms(ns, ipcp, ops, params)) {
/* IPC_CREAT|IPC_EXCL 且已存在 → -EEXIST */
...
}
} else {
/* 不存在: 必须 IPC_CREAT 才建, 否则 -ENOENT */
...
}
}
...
}
SysV 三件套的 Xget() 首参协商全部收敛在此,标志矩阵:
| 标志组合 | key 已存在 | key 不存在 |
|---|---|---|
| 无 IPC_CREAT | 返回现有 id | -ENOENT |
IPC_CREAT |
返回现有 id(校验权限/参数一致) | 创建 |
IPC_CREAT\|IPC_EXCL |
-EEXIST(防"误连他人对象") |
创建 |
IPC_PRIVATE 键绕开命名空间直接建新对象,父进程把 id 经管道/fork 传给子进程——"命名交给应用层"的另一个极端。ipc_check_perms 同时核对 size/flag 与现存对象一致(如 shmget 的 size ≤ 现有段),防止"同 key 不同用途"的误配对。
小结
SysV IPC 通用层以"每 namespace 三套 ipc_ids(idr+key 哈希双索引)+ 统一 kern_ipc_perm 对象头"支撑三件套:key→id 协商收敛在 ipcget_public 的标志矩阵,id 的高位序号种子提供防重用版本化,deleted 墓碑与 nattch 计数撑起惰性回收,权限模型照搬 Unix 文件位。理解这一层后,三个子系统的"get/ctl/op"形态只是往同一骨架里填不同的数据结构——以下三节依次展开。
28.2 System V 信号量
System V 信号量不是单个计数器而是计数器数组(信号量集),且一次 semop 可以原子地操作数组中的多个成员——"同时锁定多个资源"的原语。它的实现是三件套中最复杂的:per-信号量的细粒度自旋锁、复杂操作的批量等待队列、进程退出时的 undo 撤销。本节逐层拆解。
28.2.1 数据结构:集、成员、队列、撤销
// ipc/sem.c:95-112
struct sem {
int semval; /* current value */
/*
* PID of the process that last modified the semaphore...
* - semop
* - semctl, via SETVAL and SETALL.
* - at task exit when performing undo adjustments (see exit_sem).
*/
struct pid *sempid;
spinlock_t lock; /* spinlock for fine-grained semtimedop */
struct list_head pending_alter; /* pending single-sop operations that alter */
struct list_head pending_const; /* pending single-sop operations that don't alter */
time64_t sem_otime; /* candidate for sem_otime */
} ____cacheline_aligned_in_smp;
// ipc/sem.c:114-129
struct sem_array {
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
time64_t sem_ctime;
struct list_head pending_alter; /* pending operations that alter the array */
struct list_head pending_const; /* pending complex operations */
struct list_head list_id; /* undo requests on this array */
int sem_nsems; /* no. of semaphores in array */
int complex_count; /* pending complex operations */
unsigned int use_global_lock;/* >0: global lock required */
struct sem sems[]; /* 柔性数组: 成员紧跟其后 */
} __randomize_layout;
结构全景:
sem_array (IPC 对象, 1 个信号量集)
├─ sems[0..nsems): 每 sem 一个自旋锁+双等待链
├─ pending_alter/pending_const (数组级)
└─ list_id (本集全部 undo 记录)
sem_queue (一次 semop 调用, 睡眠时存在)
├─ sops/nsops: 待执行的 sembuf 数组
├─ blocking: 卡住的那个操作
├─ alter: 是否有 SEM_UNDO/修改值 (决定挂哪个链)
└─ sleeper: 睡眠的 task
sem_undo (进程退出时的自动补偿记录)
└─ 挂 task->sysvsem.undo_list + sem_array->list_id 双链
细粒度锁的演进是这段代码的看点:早期 sem 用一把全局锁(所有信号量集共享),7.0 的形态是"数组级 sem_perm.lock + 成员级 sem.lock"两级——单成员的简单 semop 只碰自己那把 per-sem 锁,复杂操作(跨成员)才升级全局锁(use_global_lock 字段)。高并发计数器场景(每个 sem 是独立互斥资源)因此伸缩。
28.2.2 semop:原子 sop 数组的执行
// ipc/sem.c:2256(入口)
long ksys_semtimedop(int semid, struct sembuf __user *tsops,
unsigned int nsops, const struct timespec64 __user *timeout)
一次 semop 传 sembuf sops[nsops],内核的执行算法(do_smbop 内部):
semop(sops[nsops]) 的执行协议:
[1] 排序: 按 sem_num 升序排列 sops (固定加锁顺序 → 防死锁)
[2] 快检: 全部操作逐一试算 (semval ± sem_op)
全部通过 → 原子提交, 唤醒因值变化而满足的等待者, 返回
[3] 部分通过: 找到第一个卡住的 op (blocking)
- SEM_UNDO 标记的 op 记入 undo 结构
- 建 sem_queue 挂入对应链 (alter → pending_alter)
- 睡眠 (含 timeout → semtimedop)
[4] 被唤醒 (他人 semop 改变了值):
重新快检 → 通过则提交; 仍不满足 → 再睡
[5] 唤醒他人: 值变化后遍历 pending_alter 链,
逐个试算其 sop 数组, 满足者出队完成
原子性保证: "数组全部满足才整体生效" — 任一 op 卡住
则整个数组等待, 无部分提交 (这就是"同时锁 N 个资源"语义)
SEM_UNDO 与进程死亡是 SysV 信号量的招牌特性:标了 UNDO 的 op 在提交时记录负向补偿(sem_undo 结构),进程无论正常退出还是被 SIGKILL 击毙,exit_sem()(task 退出钩子,sched 关联)都会遍历其 undo 链把值调回去——防止"持锁进程死亡导致信号量永久占用"。代价是每个 undo 需要一次额外记账,且"值基线"在 SEM_SET 后语义微妙(内核的补偿以当前值为准重算)。
28.2.3 唤醒协议与 semctl
值变化后的唤醒按"逐个试算等待者的完整 sop 数组"进行——不是逐 op 唤醒,因为一个等待者要求数组整体满足。complex_count 统计正在等待的"复杂操作"(跨成员/带 undo):其存在迫使修改者走全局锁慢路径,为零时简单操作全程 per-sem 锁。
semctl() 的命令族(GETVAL/SETVAL/GETALL/SETALL/IPC_STAT/IPC_RMID/IPC_INFO)都在 sem_perm.lock 下操作;IPC_RMID 立即唤醒全部等待者并以 -EIDRM 报错——SysV sem 的 RMID 是立即的(与 shm 的惰性回收对照,28.4 节)。
28.2.4 与 futex 的对照
| 维度 | SysV sem | futex (31 章) |
|---|---|---|
| 值位置 | 内核 sem_array |
用户内存(内核只兜底睡眠) |
| 快路径 | 每次都是系统调用 | 值可用时纯用户态原子操作 |
| 多资源原子 | sop 数组原生支持 | 无(应用自己组合多 futex) |
| 死亡清理 | SEM_UNDO 自动 | 需要 robust futex 链表 |
| 持久性 | namespace 内全局存活 | 随内存映射 |
"为什么现代代码偏爱 futex 而数据库仍用 SysV sem"的答案就在表里:sem 的值在内核态,每次 P/V 都是 syscall(微秒级),换来的是多资源原子与死亡自动清理——PostgreSQL 的锁管理器正建立在这两点上。
小结
System V 信号量以"数组集 + 原子 sop 数组 + undo 补偿"三件套提供多资源同步原语:两级锁(数组级+成员级)支撑细粒度并发,sem_queue 按 alter/const 分链等待、唤醒按"整组满足"试算,SEM_UNDO 经 exit_sem 兜底进程死亡。它每次操作都过内核的代价与 futex 的用户态快路径形成对照,而多资源原子性仍是不可替代的语义。下一节看同族的消息队列——从"值同步"走向"数据传输"。
28.3 System V 消息队列
消息队列在管道(字节流)与共享内存(裸内存)之间取中:带类型的离散消息、内核态拷贝传输、可按类型选择性接收。实现上是链表上的 msg_msg 块 + 收发双方各自的睡眠队列,7.0 的亮点是 MSG_BARRIER 锁优化——唤醒后的接收者在"消息已就绪"的确认上完全无锁。本节拆解结构与两条主路径。
28.3.1 数据结构:队列头与消息块
// include/linux/msg.h:9-16
struct msg_msg {
struct list_head m_list;
long m_type; /* 消息类型 (msgrcv 的选择依据) */
size_t m_ts; /* message text size */
struct msg_msgseg *next; /* 大消息的续页链 */
void *security;
/* the actual message follows immediately */ /* 数据内联在头部后 */
};
// ipc/msg.c:49-69
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime;
unsigned long q_cbytes; /* current bytes on queue */
unsigned long q_qnum; /* number of messages */
unsigned long q_qbytes; /* max bytes (msg_ctlmnb) */
struct pid *q_lspid; /* last msgsnd */
struct pid *q_lrpid;
struct list_head q_messages; /* 消息链 */
struct list_head q_receivers; /* 睡眠的接收者 */
struct list_head q_senders; /* 睡眠的发送者 (队列满时) */
} __randomize_layout;
msg_msg 的头体一体设计:消息数据紧贴结构头存放,单条消息超过一页时以 msg_msgseg 续页链摊开——msgsnd 每条消息都有"分配若干块 + 拷贝两次(用户→内核→用户)"的成本,这也是 POSIX mqueue 与共享内存在高吞吐场景胜出的原因。三条队列(消息/接收者/发送者)支撑三种状态:有消息可收、有接收者在等、有发送者在等(队列满)。
28.3.2 msgsnd:入队或排队
// ipc/msg.c:961(入口)
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
int msgflg)
msgsnd 协议:
[1] 权限检查 (q_perm.mode 的 WRITE 位) + 大小校验 (msgsz ≤ msg_ctlmax)
[2] 分配 msg_msg + 拷贝用户数据
[3] 持 q_perm.lock:
若有等待中的接收者 (q_receivers 非空) 且类型匹配:
→ 直接移交! 消息不进 q_messages, 填 r_msg,
smp_store_release 发布 (见 MSG_BARRIER), 唤醒
否则: 挂 q_messages 尾
[4] 队列满 (q_cbytes+msgsz > q_qbytes):
IPC_NOWAIT → -EAGAIN; 否则挂 q_senders 睡眠
类型路由:msgsnd 的 m_type 必须为正;msgrcv 的 msgtyp 三种语义——0 取队首(FIFO)、>0 取第一条该类型(可选 MSG_EXCEPT 取反)、<0 取类型 ≤ |typ| 的最小者(优先级队列语义)。这是"多路复用单一队列"的基础——工作线程用 msgtyp = 自己的工号 认领任务。
28.3.3 msgrcv 与 MSG_BARRIER
// ipc/msg.c:1264(入口)
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
long msgtyp, int msgflg)
接收端在队列上找不到匹配消息时,把自己的描述(msg_receiver:想要的类型模式、缓冲指针 r_msg)挂 q_receivers 睡眠。MSG_BARRIER 优化(msg.c:71-81 注释,全文见本书引用)解决"醒来后再拿锁确认"的开销:
无锁返回路径 (注释归纳):
唤醒链: msgsnd 满足某接收者 → r_msg = 消息指针
(smp_store_release 发布) → wake_q_add_safe 唤醒
接收者醒来后 (syscall 返回路径):
不重新拿锁! 直接 READ_ONCE(r_msg):
- 是消息指针 → smp_acquire__after_ctrl_dep 后直接拷贝返回
- 是 -EIDRM/-EAGAIN 常量 → 按错误返回
锁只在"需要重睡/出队清理"时才拿
前提: 挂起链上每个接收者只可能被"恰好属于它的移交"
触发一次, r_msg 的 release/acquire 配对替代了锁
(与 15.2 节 RELEASE 语义、26.2.1 节 pipe head 的
acquire 读同族 — 无锁快路径的通用配方)
带权限的一次性危险:MSG_COPY 标志(配合 CONFIG_CHECKPOINT_RESTORE)允许 peek 不出队——热更新/CRIU 迁移的关键开关。
28.3.4 生命周期与限额
限额体系 (ipc_namespace 字段, /proc/sys/kernel/ 系列可调):
msg_ctlmax 单条消息上限 (默认 8KB)
msg_ctlmnb 单队列字节上限 (默认 16KB)
msg_ctlmni 系统队列总数上限 (默认 32K)
percpu_msg_bytes/msgs_hdrs 全局用量 per-CPU 计数器 (18 章口径)
回收模型: 无自动回收!
- 队列随 namespace 存活, 进程全退出也不删
- IPC_RMID: 标记 deleted, 唤醒全部等待者报 -EIDRM
- 未删队列的消息内存永久占用 → 泄漏型事故的高发区
("谁创建了队列谁负责删"是应用纪律)
| 维度 | SysV msg | POSIX mqueue (29 章) | pipe (26 章) |
|---|---|---|---|
| 边界 | 消息 | 消息 | 字节流 |
| 选择接收 | msgtyp 路由 | 优先级 + 通知 | 无 |
| 持久性 | 进程全退仍在 | 内核对象 + mq_open 引用计数 | fd 关闭即亡 |
| 与 epoll | 不可 | 可(fd 化) | 可 |
小结
SysV 消息队列以 msg_msg 头体一体块挂在 q_messages 链上,收发两端的睡眠队列支撑"队满/空"两种阻塞;msgtyp 的三种匹配语义让单队列具备多路复用能力;MSG_BARRIER 用 r_msg 的 release/acquire 配对实现了唤醒者免锁返回。它是三件套中"协议完备但拷贝成本固定"的传输层——比管道多了边界与路由,比共享内存多了系统调用的固定税。下一节看三件套的压轴:把拷贝彻底消灭的共享内存。
28.4 System V 共享内存
SysV 共享内存是三件套中性能之最——建段后收发数据零系统调用(读写就是普通访存)。它的内核实现揭示了一个统一的真相:shm 段就是一个 shmem(tmpfs)文件,shmat 不过是把该文件映射进进程地址空间的一次 do_mmap。本节拆解建段、附加、分离与销毁的完整生命周期。
28.4.1 shmid_kernel:段的内核身
// ipc/shm.c:54-79
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm; /* 28.1 节通用头 */
struct file *shm_file; /* 真身: shmem 文件! */
unsigned long shm_nattch; /* attach 计数 */
unsigned long shm_segsz;
time64_t shm_atim; /* 最后 shmat 时间 */
time64_t shm_dtim; /* 最后 shmdt 时间 */
time64_t shm_ctim;
struct pid *shm_cprid;
struct pid *shm_lprid;
struct ucounts *mlock_ucounts; /* SHM_LOCK 记账 */
/*
* The task created the shm object, for
* task_lock(shp->shm_creator)
*/
struct task_struct *shm_creator;
/*
* List by creator. task_lock(->shm_creator) required...
* If list_empty(), then the creator is dead already.
*/
struct list_head shm_clist; /* 挂创建者的回收链 */
struct ipc_namespace *ns;
} __randomize_layout;
shm_file 是全部秘密:newseg() 创建段时调用 shmem_file_setup() 在内部 tmpfs 里建一个文件——段的每一页就是该文件的页缓存页。于是"多个进程共享内存"被规约为"多个进程映射同一文件",页的分配/回收/换出全部复用页缓存与回收子系统(24/36 章)——shm 实现薄到只剩 IPC 语义的包装。
28.4.2 newseg:建段
// ipc/shm.c:702-740(节选)
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
...
if (size < SHMMIN || size > ns->shm_ctlmax) /* 单段限额 */
return -EINVAL;
...
if (ns->shm_tot + numpages < ns->shm_tot ||
ns->shm_tot + numpages > ns->shm_ctlall)
return -ENOSPC; /* 总量限额 */
shp = kmalloc_obj(*shp, GFP_KERNEL_ACCOUNT);
...
shp->shm_perm.key = key;
shp->shm_perm.mode = (shmflg & S_IRWXUGO);
...
file = shmem_file_setup(name, size, acctflag); /* 建真身文件 */
...
shp->shm_file = file;
...
}
限额三查(SHMMIN/shm_ctlmax 单段、shm_ctlall 总量)与 /proc/sys/kernel/shmmax、shmall sysctl 对应——数据库 SGA 的配置校验就在这里报错。SHM_NORESERVE(acctflag)控制是否预扣 swap 记账。建段不分配任何页——页在首次触碰时由 shmem 的缺页路径供货(22.4 节矩阵的 shmem_fault 分支)。
28.4.3 do_shmat:映射
// ipc/shm.c:1519-1660(节选)
long do_shmat(int shmid, char __user *shmaddr, int shmflg,
ulong *raddr, unsigned long shmlba)
{
...
if (shmflg & SHM_RDONLY) {
prot = PROT_READ;
...
} else {
prot = PROT_READ | PROT_WRITE;
...
}
if (shmflg & SHM_EXEC) {
prot |= PROT_EXEC; /* 显式申请执行位 */
...
}
...
/*
* We need to take a reference to the real shm file to prevent the
* pointer from becoming stale in cases where the lifetime of the
* outer file extends beyond that of the shm segment...
*/
base = get_file(shp->shm_file);
shp->shm_nattch++;
size = i_size_read(file_inode(base));
...
file = alloc_file_clone(base, f_flags,
is_file_hugepages(base) ?
&shm_file_operations_huge :
&shm_file_operations); /* clone 外壳文件 */
...
sfd->id = shp->shm_perm.id; /* shm_file_data 填充 */
sfd->ns = get_ipc_ns(ns);
...
/* 最终: do_mmap(file, addr, size, prot, flags, ...) */
}
shmat 的分层结构:
shmid (id) ──查──> shmid_kernel ──> shm_file (shmem 文件, 真身)
│
alloc_file_clone ──>│ 外壳 file (每进程一份,
│ private_data = shm_file_data)
v
do_mmap(file, ...) ──> 进程地址空间 VMA
(22.3 节全流程复用!)
关键设计: 外壳 file 隔离"每进程状态" (f_mode/权限/f_pos)
与"共享数据"(页缓存) — 与 26.3 节 FIFO 嫁接 inode 同思想
is_file_hugepages 分支: 段以 hugetlbfs 建立时
(SHM_HUGETLB 标志) 走大页版 fops (23.2 节联动)
地址协商:shmaddr=NULL 让内核选址(top-down,22.1 节);指定地址时按 shmlba 对齐,SHM_RND 允许向下圆整、SHM_REMAP 允许覆盖既有映射(MAP_FIXED 语义,22.3.1 节的覆写风险同源)。返回的地址就是普通用户指针——此后读写零内核参与。
28.4.4 分离与销毁:惰性回收
生命周期状态机:
shmget ──> [ACTIVE] ──shmat──> [ATTACHED] nattch++
│ │
│ IPC_RMID │ shmdt: nattch--
v v
[MARKED deleted] nattch==0?
│ │ 是 + 已 MARKED
│ 新 shmat → -EIDRM │ → shm_destroy: 文件页回收
└────────────────────┘
否则存活到 namespace 退出
特殊保障 (shm_clist 链): 创建者进程死亡时,
exit 代码遍历其 shm_clist 把孤儿段标记删除
—— 防止"建段进程死了, 段与页永久驻留"
IPC_RMID 的时点陷阱(移植自老 UNIX 的行为差异):Linux 在 RMID 时不立即销毁有活口(nattch>0)的段,新 shmat 报 -EIDRM;而某些系统立即断开全部映射。容器化时代更著名的坑是"RMID 后段还在 /dev/shm 之外占着内存"——ipcs 查 dest 标记即本机制的可见面。shm_destroy 最终 shmem_truncate + 释放,页全部归还伙伴系统。
| 维度 | SysV shm (本节) | POSIX shm (30.1) | mmap MAP_SHARED (30.2) |
|---|---|---|---|
| 命名 | key/id | 路径名 (/dev/shm/) |
已有文件 |
| 建立 | shmget+shmat | shm_open+ftruncate+mmap | mmap |
| 真身 | 内部 shmem 文件 | tmpfs 文件 | 任意文件 |
| 生命周期 | namespace 级+惰性回收 | 引用计数(fd+映射) | 文件页缓存 |
三者最终都收敛到"tmpfs 文件 + do_mmap"的同一内核事实——IPC 的性能上限从来都是内存速度。
小结
SysV 共享内存的实现真相是"shmem 文件映射的 IPC 包装":newseg 在内部 tmpfs 建真身文件,do_shmat 克隆一个每进程外壳 file 后 do_mmap 挂入地址空间,页供给与回收全部复用页缓存机制;shm_nattch 与 RMID 墓碑构成惰性回收,shm_clist 兜底创建者死亡。与 26-28 节对照:管道/消息队列传输数据要过内核拷贝,共享内存只共享内核一次(建段时),此后数据流动完全在用户态——这就是它在数据库等场景不可替代的原因。第五部分的低层三件套至此完成,下一章看 POSIX 消息队列如何用 fd 化重塑消息语义。