返回

文章详情

Unix GC 重制版

Hacker News2026年6月10日 22:48

引言 AF_UNIX 垃圾收集器是内核中的一个有趣部分。它的存在是因为 socket 可以通过 SCM_RIGHTS 发送,但在用户空间中可能变得不可达,同时仍然被内核保持活动状态,这不是内存高效;在这种情况下,垃圾收集器介入来释放它们。不久前,该子系统基于图/强连通分量模型从头重新编写;但它仍然容易出现错误。本文详细阐述了重写过程,并讨论了一个 Use-After-Free 错误。 AF_UNIX 垃圾收集器 — 背景 每个子系统的垃圾收集器负责回收无法通过用户空间句柄访问的内核对象。对于 AF_UNIX,入口点是 unix_gc() : static DECLARE_WORK (unix_gc_work, __unix_gc); void unix_gc ( void ) { WRITE_ONCE (gc_in_progress, true); queue_work (system_dfl_wq, & unix_gc_work); } 它的实际主体是 __unix_gc() : static void __unix_gc ( struct work_struct * work) { struct sk_buff_head hitlist; struct sk_buff * skb; spin_lock ( & unix_gc_lock); if ( ! unix_graph_maybe_cyclic) { spin_unlock ( & unix_gc_lock); goto skip_gc; } __skb_queue_head_init ( & hitlist); if (unix_graph_grouped) unix_walk_scc_fast ( & hitlist); else unix_walk_scc ( & hitlist); spin_unlock ( & unix_gc_lock); skb_queue_walk ( & hitlist, skb) { if ( UNIXCB (skb).fp) UNIXCB (skb).fp -> dead = true; } __skb_queue_purge_reason ( & hitlist, SKB_DROP_REASON_SOCKET_CLOSE); skip_gc: WRITE_ONCE (gc_in_progress, false); } unix_sock 结构 struct unix_sock { /* 警告:sk 必须是第一个成员 */ struct sock sk; /* 继承 */ struct unix_address * addr; /* 绑定名称 */ struct path path; /* 如果绑定则为文件系统路径 */ struct mutex iolock, bindlock; struct sock * peer; /* 连接的对等体 */ struct list_head link; atomic_long_t inflight; /* [1] SCM_RIGHTS fd 计数 */ /* ... */ struct sk_buff * oob_skb; }; GC 的关键字段是 inflight ( [1] ). 当它的 struct file * 作为 SCM_RIGHTS 负载发送时,socket 是“在途中” — 由进程 A 发送,尚未被进程 B 接受。每次发送时,inflight 增加;每次接收时,inflight 减少。GC 正在寻找 file_count == inflight 的 sockets:剩余的引用仅是被困在其他 sockets 的接收队列中的引用,即没有用户空间句柄可以再访问它们。LWN 的“AF_UNIX GC 重构”文章更简明地表达了这一点:假设我们将 AF_UNIX socket A 的 fd 发送给 B,反之亦然,并关闭这两个 sockets。当创建时,每个 socket 的 struct file 最初有一个引用。在 fd 交换后,两个引用计数都增加到 2。然后,close() 将两个引用计数减少到 1。从这一点开始,没有人可以触碰该文件/socket。然而,struct file 仍有一个引用计数,因此不再调用 AF_UNIX socket 的 release() 函数。这就是为什么我们需要跟踪所有 inflight AF_UNIX sockets 并运行垃圾收集。内核维护一个全局的 unix_tot_inflight 计数器,在每次 inflight 转换时递增,在每次 accept 时递减。何时运行 GC 有两个触发器: inflight sockets 太多: 如果 ( READ_ONCE (unix_tot_inflight) > UNIX_INFLIGHT_TRIGGER_GC && ! READ_ONCE (gc_in_progress)) unix_gc (); ( UNIX_INFLIGHT_TRIGGER_GC == 16000 .) 如果 socket 关闭时,有任何 inflight: static const struct proto_ops unix_stream_ops = { .family = PF_UNIX, .owner = THIS_MODULE, .release = unix_release, /* ... */ }; static void unix_release_sock ( struct sock * sk, int embrion) { /* ... */ if ( READ_ONCE (unix_tot_inflight)) unix_gc (); } 旧 GC 2024 年之前的收集器在谷歌 P0 文章“Linux 内核垃圾收集的量子状态”中有很好的描述,涵盖了算法和 2021 年野外 Android 攻击。该文章是推荐的伴随阅读;这里只是一个总结:旧 GC 遍历 inflight 图,标记循环,并检查 inflight != refcount 来决定每个循环是否可被收集。这是一个很好的美人鱼图: 新 GC 从 GC 重构公告中: [它] 替换了当前的 GC 实现,该实现锁定每个 inflight socket 的接收队列,并在其他地方需要一些技巧。新的 GC 不会锁定每个 socket 的队列,以最小化其影响,并且在没有循环引用或 inflight fd 图的形状没有更新时尽量轻量。 图形表示 每个 inflight socket 变为一个顶点;在 SCM_RIGHTS cmsg 中携带的每个支持 struct file * 变为一个有向边(前驱 → 后继)。 示例 — 发送 A 到 C,C 到 D,B 到 D。三个 inflight sockets(A、B、C — 不是 D),给出图形: Tarjan 的算法将该图分成强连通分量。 为什么 SCC?对于任何有向图,任何大于一个顶点的 SCC 必然至少包含一个循环:循环是顶点可回收的必要但不充分条件:回收要求顶点在 inflight 状态,且不可达。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡