开发者生态
morning
Unix GC 重制版
2026-06-11
1 阅读
mananaysiempre
简介 AF_UNIX 垃圾收集器是内核中一个有趣的部分。它的存在是因为套接字可以使用 SCM_RIGHTS 发送,但它们可能无法从用户空间访问,同时仍由内核保持活动状态,这不具有内存效率;在这种情况下,垃圾收集器会介入以释放它们。不久前,该子系统在图/强连接组件模型之上从头开始重写;但它仍然容易出现错误。这篇文章从头到尾介绍了重写过程,并讨论了释放后使用错误。 AF_UNIX 垃圾收集器 — 背景 每个子系统的垃圾收集器负责回收无法再通过用户空间句柄访问的内核对象。对于 AF_UNIX,入口点是 unix_gc() : static DECLARE_WORK (unix_gc_work, __unix_gc); void unix_gc ( void ) { WRITE_ONCE (gc_in_progress, true);队列工作(system_dfl_wq,&unix_gc_work);它的真正主体是 __unix_gc() : static void __unix_gc ( struct work_struct * work) { struct sk_buff_head hitlist;结构体sk_buff * skb; spin_lock(&unix_gc_lock); if (!unix_graph_maybe_circular) { spin_unlock (&unix_gc_lock);转到skip_gc; } __skb_queue_head_init (&命中列表); if (unix_graph_grouped) unix_walk_scc_fast (& hitlist);否则 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 路径; /* 文件系统路径(如果绑定)*/ struct mutex iolock, bindlock;结构袜子*同级; /* 连接的对等点 */ struct list_head link; atomic_long_t 飞行中; /* [1] SCM_RIGHTS fd 计数 */ /* ... */ struct sk_buff * oob_skb; }; GC 的关键领域是飞行中 ([1])。当套接字的结构文件 * 作为 SCM_RIGHTS 有效负载运行时,套接字处于“飞行中”状态 — 由进程 A 发送,尚未被进程 B 接受。每次发送时,飞行中都会递增;每次收到时,飞行中的值都会减少。 GC 正在寻找 file_count == inflight 的套接字:唯一剩余的引用是陷入其他套接字接收队列中的引用,即没有用户空间句柄可以再次访问它们。 LWN“AF_UNIX GC rework”文章说得更简洁:假设我们将 AF_UNIX 套接字 A 的 fd 发送到 B,反之亦然,然后 close() 两个套接字。创建时,每个套接字的结构文件最初都有一个引用。在 fd 交换之后,两个引用计数都增加到 2。然后,close() 将两者都减少到 1。从这一点开始,没有人可以触摸该文件/套接字。然而,结构文件有一个引用计数,因此永远不会调用 AF_UNIX 套接字的release()函数。这就是为什么我们需要跟踪所有正在运行的 AF_UNIX 套接字并运行垃圾收集。内核维护一个全局 unix_tot_inflight 计数器,在每次飞行转换时递增,并在每次接受时递减。 GC 运行时有两个触发器: Too much inflight sockets: if ( READ_ONCE (unix_tot_inflight) > UNIX_INFLIGHT_TRIGGER_GC && ! READ_ONCE (gc_in_progress)) unix_gc (); ( UNIX_INFLIGHT_TRIGGER_GC == 16000 。) 套接字关闭,如果有任何正在运行: 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 年之前的收集器在 Google P0 帖子“Linux 内核垃圾收集的量子状态”中得到了很好的描述,其中涵盖了算法和 2021 年 Android 的野外漏洞利用。推荐阅读该文章;这里只是一行摘要:旧的 GC 遍历 inflight 图表,标记周期,并检查 inflight != refcount 来决定每个周期是否可收集。这是一个漂亮的美人鱼图:来自 GC 重做公告的新 GC:[它] 取代了当前的 GC 实现,该实现锁定每个飞行套接字的接收队列,并在其他地方需要技巧。新的 GC 不会锁定每个套接字的队列以最大程度地减少其影响,并且在没有循环引用或 inflight fd 图的形状没有更新的情况下尝试轻量级。图形表示每个飞行套接字成为一个顶点; SCM_RIGHTS cmsg 中携带的每个支持结构文件 * 成为有向边(前驱 → 后继)。示例 — 将 A 发送到 C、C 发送到 D、B 发送到 D。三个飞行套接字(A、B、C — 不是 D),给出图表:然后,Tarjan 算法将该图划分为强连接组件。为什么选择 SCC?对于任何有向图,任何一个以上顶点的 SCC 都必然包含至少一个循环: 循环是顶点可收集的必要但非充分条件:收集要求顶点处于飞行状态,且未反应