3、链路层实现:以太网帧格式解析、LLC子层功能、ARP协议在内核中的实现(邻居子系统)
链路层,说白了就是网卡和内核协议栈之间的「翻译官」。数据从网线进来,网卡把它变成一串比特流,然后链路层负责把这些比特流组装成帧,再交给上层处理。我刚开始接触这部分时,总觉得链路层很简单,不就是个帧格式吗?后来调一个网卡驱动bug,折腾了三天才发现是帧校验字段没对齐……嗯,从那以后我再也不敢小看这一层了。
3.1 以太网帧格式解析
以太网帧是链路层最基础的数据单元。它的结构其实很清晰,但细节里藏着不少坑。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 目的MAC地址 | 6 | 接收方网卡地址 |
| 源MAC地址 | 6 | 发送方网卡地址 |
| EtherType / 长度 | 2 | 标识上层协议(如0x0800为IP) |
| 载荷 | 46-1500 | 上层数据(如IP包) |
| FCS | 4 | 帧校验序列(CRC32) |
这里有个关键点:EtherType字段。如果值大于等于0x0600,它表示上层协议类型;如果小于这个值,它表示帧长度。这是IEEE 802.3和Ethernet II帧的区别。我个人习惯在抓包时先看这个字段,一眼就能判断出帧格式。
核心要点:内核中接收帧时,会通过eth_type_trans()函数解析EtherType。这个函数会返回一个__be16类型的协议号,然后内核根据这个协议号把帧交给对应的上层处理函数(比如IP、ARP)。
我曾经遇到过一个兼容性问题:某款交换机在发送VLAN帧时,EtherType字段被改写成了0x8100(802.1Q标签)。结果我们的网卡驱动没处理这个标签,直接把帧当成了普通以太网帧,导致上层协议解析失败。后来加了个VLAN剥离逻辑才搞定。
3.2 LLC子层功能
LLC(Logical Link Control)子层,位于MAC子层之上。它的存在是为了让上层协议不直接依赖具体的物理介质。说白了,LLC就是个适配层。
LLC主要有三种服务模式:
- 类型1(无连接):不建立连接,不确认,适合实时性要求高的场景。我见过一些工业控制协议用这个模式,丢包了也不重传,因为重传反而会引入延迟。
- 类型2(面向连接):建立逻辑连接,有确认和重传机制。这个模式在内核中很少直接用,因为上层协议(如TCP)已经做了类似的事情。
- 类型3(半可靠):介于两者之间,有确认但无连接。说实话,我在实际项目中几乎没见过这个模式。
内核中LLC的实现主要在net/llc/目录下。它的核心数据结构是struct llc_sap(服务访问点),每个SAP对应一个上层协议。举个例子,IPX协议就注册在SAP 0xE0上。
小技巧:如果你在内核中看到llc_rcv()函数,它负责接收LLC帧。这个函数会检查帧的DSAP(目的SAP)和SSAP(源SAP),然后根据SAP找到对应的处理函数。调试时可以用print_hex_dump()打印SAP字段,快速定位问题。
3.3 ARP协议在内核中的实现(邻居子系统)
ARP协议,说白了就是「IP地址到MAC地址的翻译官」。内核中实现ARP的地方叫「邻居子系统」(Neighbour Subsystem),它不仅仅处理ARP,还处理NDISC(IPv6邻居发现)等其他地址解析协议。
邻居子系统的核心数据结构是struct neighbour。每个邻居条目包含:
- 目的IP地址(
dst字段) - 对应的MAC地址(
ha字段) - 状态(
nud_state字段,如NUD_REACHABLE、NUD_STALE、NUD_FAILED等) - 定时器(
timer字段,用于超时重试)
邻居条目的状态机是理解ARP的关键。我画了一张图,帮你理清这个流程:
这个状态机看起来复杂,其实核心逻辑很简单:
- 一开始条目是NUD_NONE,需要发送ARP请求时变成NUD_INCOMPLETE
- 收到ARP应答后变成NUD_REACHABLE,表示地址解析成功
- 如果一段时间没通信,变成NUD_STALE,表示条目可能过期了
- 有数据要发送时,从NUD_STALE进入NUD_DELAY,等待确认
- 如果超时没确认,进入NUD_PROBE,发送单播ARP探测
- 探测重试耗尽后进入NUD_FAILED,表示解析失败
注意:我曾经在生产环境遇到一个问题:某个网络设备频繁发送ARP请求,导致邻居表被撑爆。原因是该设备的MAC地址频繁变化,每次变化都会创建新的邻居条目,而旧的条目要等到超时才会被清理。解决方案是调整/proc/sys/net/ipv4/neigh/default/gc_thresh*参数,或者缩短base_reachable_time。
内核中邻居子系统的核心函数是neigh_lookup()和neigh_create()。前者用于查找已有的邻居条目,后者用于创建新的条目。我个人习惯在调试时用ip neigh show命令查看邻居表的状态,非常直观。
关键代码路径:
// 发送ARP请求
arp_send() -> arp_create() -> dev_queue_xmit()
// 接收ARP应答
arp_rcv() -> arp_process() -> neigh_update()
// 邻居条目查找
neigh_lookup() -> 哈希表查找 -> 返回struct neighbour *
嗯,这里要注意一个细节:ARP请求是广播发送的,但ARP应答是单播发送的。这意味着如果目标主机不在同一个广播域内,ARP请求就无法到达。这也是为什么跨网段通信需要网关的原因——网关会代理ARP请求。
最后,我建议你在调试链路层问题时,多用tcpdump -e命令,它会显示MAC地址和帧格式。结合内核的dynamic_debug功能,可以打印出邻居子系统的详细日志。我曾经用这个方法定位过一个ARP缓存不一致的问题,发现是某个网卡驱动在接收ARP应答时,没有正确更新邻居表的状态。
公众号:蓝海资料掘金营,微信deep3321