Linux内核分析之进程间通信-02

This language version is unavailable; showing the other language.

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 化重塑消息语义。