在并发 PID 查找与移除之间保护对象生命周期
Linux 通过 RCU 保护 PID 查找期间的对象存活,再用引用计数延长返回对象的生命;移除 PID 后先等待 RCU 宽限期,再释放索引持有的引用。
工程问题#
读者查找 PID 对象时,另一个 CPU 可能正在把该对象从命名空间索引中移除。如果刚读到的指针立即被释放,读者甚至来不及安全增加引用计数。
实际实现#
find_get_pid 在 RCU 读侧临界区内查找 PID 并调用 get_pid 增加引用。free_pid 在 pidmap_lock 下移除各层命名空间 IDR 条目,随后用 call_rcu 安排 delayed_put_pid。回调在宽限期后执行 put_pid,只有引用计数归零时才真正回收对象。
处理流程#
- 进入 RCU 读侧临界区,在当前命名空间查找 PID
- 在受保护期间增加对象引用,再退出读侧临界区
- 写侧持锁移除 IDR 中的条目,并安排 RCU 回调
- 宽限期结束后递减保留引用,最后一个引用释放时回收对象
源码片段#
固定版本 · 原文摘录473struct pid *find_get_pid(pid_t nr)474{475 struct pid *pid;476477 rcu_read_lock();478 pid = get_pid(find_vpid(nr));479 rcu_read_unlock();480481 return pid;482}先进入 RCU 读区,再查找并增加引用。退出读区后,返回指针的生命由引用计数继续保障;调用方最终需要 put_pid。
157 }158159 idr_remove(&ns->idr, upid->nr);160 }161 spin_unlock_irqrestore(&pidmap_lock, flags);162163 call_rcu(&pid->rcu, delayed_put_pid);164}free_pid 尾部先从命名空间 IDR 摘除对象并释放 pidmap_lock,再用 call_rcu 安排 delayed_put_pid;并非在移除条目后立即释放内存。
112void put_pid(struct pid *pid)113{114 struct pid_namespace *ns;115116 if (!pid)117 return;118119 ns = pid->numbers[pid->level].ns;120 if (refcount_dec_and_test(&pid->count)) {121 kmem_cache_free(ns->pid_cachep, pid);122 put_pid_ns(ns);123 }124}125EXPORT_SYMBOL_GPL(put_pid);126127static void delayed_put_pid(struct rcu_head *rhp)128{129 struct pid *pid = container_of(rhp, struct pid, rcu);130 put_pid(pid);131}RCU 回调只是递减一个引用。真正的缓存对象释放受 refcount_dec_and_test 控制,因此宽限期结束和引用归零是不同的条件。
三个片段连接查找、移除和最终回收;free_pid 片段是函数尾部,写侧锁获取和各命名空间循环在同一文件的前文。
收益与取舍#
工程分析带来的收益
- PID 查找路径无需为这次查找获取 pidmap_lock
- 把短期查找保护与跨读侧临界区的长期对象持有分开处理
付出的代价
- 被移除对象的内存可能保留到宽限期结束及所有引用释放之后
- RCU 不负责序列化所有写者,也不自动保证对象可变字段的一致快照;这里仍有写侧自旋锁和引用计数
可比较的选择
读写锁保护整个查找与移除可用同一锁协调读写,但读路径需参与锁竞争;若返回后还要使用对象,依然需要生命周期约定。
版本与适用范围#
对应 Linux v6.12 的 struct pid 对象查找和释放。拿到 PID 对象引用不代表相关进程仍在运行;RCU 读侧保护也不能代替跨临界区使用所需的额外引用。
参考来源#
源码、官方文档与原始论文kernel/pid.c · find_get_pid / free_pid / delayed_put_pid / put_pid
相关实现与比较#
1 个相关案例QEMU 重建 FlatView 后发布新指针,已有读者继续使用旧视图,直到引用归还且 RCU 宽限期结束才销毁。