Linux 邻接(Neighbor)子系统架构与 NUD 状态机
导读
在网络协议栈中,网络层(L3,如 IP)与数据链路层(L2,如 MAC)之间存在一个巨大的鸿沟。当路由器或主机知道要将数据包发给哪个下一跳 IP 时,它并不能直接把 IP 丢进网线,而是必须封装一层链路层的头部(比如 Ethernet 帧头)。
Linux 邻接(Neighbor)子系统正是跨越这道鸿沟的桥梁。无论是 IPv4 的 ARP 协议,还是 IPv6 的 NDISC(Neighbor Discovery),在 Linux 内核中都被抽象封装并统一由邻接子系统进行管理。
本文将摒弃简单的概念普及,深入到 Linux 内核邻接子系统的核心结构体、NUD(Neighbor Unreachability Detection)状态机流转、核心工作流以及垃圾回收(GC)机制,带你彻底看透这个连接 L3 与 L2 的关键枢纽。
一、 核心架构定位与数据结构剥析
在 Linux 内核报文发送链路中,当路由子系统(Routing Subsystem)决定了下一跳的 IP 地址后,报文就会移交给邻接子系统,由其负责查询对应的 MAC 地址并完成最终的物理帧封装。为了实现高并发和抽象,内核定义了三大核心数据结构:
1. 邻居表 struct neigh_table
这是协议级的全局管理者。不同的协议族有自己的邻接表,例如 IPv4 的 arp_tbl 和 IPv6 的 nd_tbl。
- 作用 :维护该协议下的所有邻居表项,提供哈希查找、垃圾回收机制以及统一的函数指针回调(例如
family_ops封装了各个协议如何实际去发送请求报文)。 - 关键机制:由于涉及网络高频查找,内核采用 RCU(Read-Copy-Update)锁配合分段自旋锁来保护哈希桶,允许极高并发下的无锁读。
2. 邻居表项 struct neighbour
代表一个具体的"邻居"(比如局域网内的网关或旁路主机),是内核监控连通性的最小单位。
primary_key:用来进行哈希查找的关键键值,通常是目标 IP 地址。ha(Hardware Address):存放解析成功后的物理 MAC 地址。nud_state:极度核心! 记录当前邻居的连通性状态。timer:内部定时器,驱动 NUD 状态机的运转(比如判定何时超时、何时发送探测探针)。arp_queue:在 MAC 地址还在使用 ARP 解析、未拿到响应的"等待期",上层应用发来的sk_buff(网络数据报文)会被暂存在这个队列中。如果解析失败,这个队列的包将被全部丢弃。
3. 硬件头缓存 struct hh_cache
- 加速原理 :发包是高频操作,如果每个数据包都要找
struct neighbour然后用代码拼接 14 字节的以太网帧头,太耗费 CPU。hh_cache会将已经构建好的完整 L2 链路帧头缓存起来。后续发包时直接 Copy 帧头,极大提升了吞吐性能。
二、 核心机制:NUD(邻居不可达检测)状态机
这是邻接系统中最复杂、也是最精华的部分。内核通过状态机轮转,精确追踪对端设备是不是"活着的"、对端的 MAC 地址有没有发生变更。
上层需要发送数据包\n(触发ARP/ND请求)
收到 ARP Reply (解析出MAC)
连续多次重试无响应超时
经过 reachable_time \n仍无双方流量的确认证明
需要向其发包\n先暂发并留观
delay_probe_time超时\n且未收到上层(如TCP)的确认
收到可达性确认\n(如TCP的ACK等证据)
收到探针的应答\n(对端仍在)
达到探测上限\n依然无响应
等待垃圾回收 (GC) 被清理
NONE
INCOMPLETE
REACHABLE
FAILED
STALE
DELAY
PROBE
【状态详解】
- INCOMPLETE(未完成) :刚才有包要发给它,但内核不知道它的 MAC 地址。目前它已经用尽全力发出去了 ARP Request 请求并在苦苦等待 ARP Reply,同时应用产生的数据包挂在
arp_queue缓存里排队。 - REACHABLE(可达):收到了回复并拿到了 MAC。这是最高效的"黄金状态",此时有包来,网卡直接盖上 MAC 帧头就发,性能最高。
- STALE(陈旧) :对方太久没理我们了(超过
base_reachable_time_ms,默认约 30~40 秒)。此时如果没有发往它的包,大家相安无事。一旦要给它发包,状态机将变道至 DELAY 去重验连通性。 - DELAY(延迟) :给网络一点机会。包照常发出同时启动一个小定时器。如果在此期间上层(如 TCP)传来了接收确认信号(证明路是通的),就直接回到
REACHABLE,省去底层的 ARP 交互;如果没动静,进入PROBE强制探测。 - PROBE(探测):内核开始较真了。主动向对方发送几遍单播 ARP/ND 探针,非要逼问出对方到底还在不在本网段。
- FAILED(失败) :彻底没救(比如网线被拔或对方关机),此时挂在
arp_queue里的数据包全部被抛弃,向上层返回Host Unreachable错误。
三、 核心工作流演示:一个数据包的下行之旅
我们可以用如下时序图,展示一个上层 sk_buff 从网络层下来时发生的底层互动全貌:
目标主机/网关 网卡设备驱动 邻接子系统 (Neigh) 协议栈L3 (IP层) 目标主机/网关 网卡设备驱动 邻接子系统 (Neigh) 协议栈L3 (IP层) alt [表项存在且处于 REACHABLE 状态] [表项不存在 或 状态为 INCOMPLETE] 1. ip_finish_output2() \n(传递 下一跳IP + skb) 2. neigh_lookup() 查找哈希表 3. (Fast Path) 复制 hh_cache 帧头 4. dev_queue_xmit() 下发给网卡 3. (Slow Path) 将 skb 压入 arp_queue 4. 组装 ARP Request 并发送 5. 广播 ARP Request 6. 单播回复 ARP Reply 7. neigh_update() 解析出 MAC 8. 状态火速变更为 REACHABLE 9. 释放 arp_queue 中的所有 skb\n逐个 dev_queue_xmit() 下发
四、 垃圾回收(GC)与阈值保护防御
在高访问量的服务器(尤其是负载均衡节点如 LVS集群、网关)或者面临外网海量 IP 扫描攻击时,邻接表如果不受控制地膨胀,会导致内核 OOM 崩溃(此为早年的 ARP Cache 耗尽攻击)。为此,内核实现了极其严格的 三级阈值 GC 模型:
gc_thresh1(默认 128):系统低负荷时。如果当前存在的邻居表项数少于 128 个,垃圾回收机制纯粹是在"休眠",完全不耗费任何 CPU。gc_thresh2(默认 512) :水位警告区。如果条目超过此阀值并且某些条目已经超过了gc_interval老化周期,触发周期性清理,强制老化删除最陈旧的缓存。gc_thresh3(默认 1024) :水位红线。一旦总缓存数超过此时,任何针对新 IP 的neigh_create()都会受到极严格的审查甚至是直接阻断。内核将发起一波暴力同步回收,誓死保卫核心内存。
架构师排障建议 :
如果你的系统出现局域网丢包,
dmesg里出现Neighbor table overflow.报错,说明缓存已撞破红线。此时应当在sysctl.conf里放宽阈值:
bashnet.ipv4.neigh.default.gc_thresh1 = 1024 net.ipv4.neigh.default.gc_thresh2 = 4096 net.ipv4.neigh.default.gc_thresh3 = 8192
五、 排查利器与用户态(Netlink)控制
Linux 最精妙的设计在于将内核能力完整暴露给用户态。平时我们敲打的 ip 系列命令,本质上是通过 Netlink 套接字同邻接子系统进行对话与操控的。
bash
# 1. 查询当前邻居缓存 (注意观察最后面红圈处的 NUD 状态词)
ip neigh show
# 输出示例:192.168.1.1 dev eth0 lladdr 00:11:22:xx:xx:xx REACHABLE
# 2. 强制刷新表项 (常用于对端路由器/网关热备切换且MAC变动时,避免我们发错洞)
ip neigh flush to 192.168.1.1
# 3. 数据层面的终极观测 (适合排查ARP丢包率)
ip -s neigh show
# 可看到因 ARP 没回来而被迫丢弃的报文数等极其细节的 Metric。
六、 结语
从内核研发的视角看,一切网络协议皆为"状态机+队列"的折腾与调度。邻接子系统利用一个优雅精湛的 NUD 状态检测图,实现了网络上层发送操作解耦、底层的异步等待容错,以及海量缓存重试功能。掌握了 struct neighbour 及其迁移逻辑,以后面对玄学的"局域网丢包"和"首包高延迟",你也能做到了然于胸。