网络协议栈概述

本页目录

应用 send 成功、网卡发送完成、对端 ACK、业务处理结束,是不同的时刻。沿收发路径要分别跟踪线上报文、skb 和应用字节流⁠,它们不必一一对应。

查阅目标阅读位置
skb 怎样引用数据第 1 节
接收、NAPI 与发送完成第 2~4 节
卸载、队列分配与计数第 5~7 节

1. skb 是描述符,不是固定连续数据包

struct sk_buff 存放元数据,并引用实际数据缓冲区。线性区域由 head/data/tail/end 的逻辑位置描述;其中部分成员在实现中保存偏移,并非全部都是可直接解引用的 C 指针。

skb 元数据与数据存储分离 headroom 线性有效数据 tailroom shared info head data tail end 外部 page fragments 计入 skb 的总长度 len = 线性长度 + data_len;clone 可以共享数据,修改前仍要确保可写。

skb->len 是总数据长度,data_len 是非线性部分长度,skb_headlen() 得到线性有效长度。header 成员主要表示相对位置,访问应使用 ip_hdr()、tcp_hdr() 等合适接口;GSO 信息和 frags 等位于 skb_shared_info 等结构中,不能把它们随意平铺进伪造的 skb 定义。

收包解析时,skb_pull() 可以推进 data;封装时,skb_push() 利用 headroom 前置头部。但只有空间、线性范围和共享状态满足条件才能这样做,并非调用它们就自动扩容或建立写时复制。

操作新增的是什么写入前仍要确认什么
skb_clone新元数据,共享底层数据数据可写性与共享关系
skb_get同一 skb 的引用并发访问协议

引用计数维持寿命,不授予任意并发修改权限。见 skb 文档。

2. 接收数据怎样到应用

本机接收:先处理报文,再交给 socket 驱动处理接收完成 NAPI 获取缓冲区;native XDP 可在 skb 前运行 1 skb 与链路层处理 GRO、VLAN、packet tap、bridge 等按路径参与 2 IP 处理与路由 PREROUTING 后决定本机接收或转发 3 本机协议交付 INPUT 后查找 TCP/UDP socket 并处理协议 4 应用获得可读数据 唤醒只是通知,数据仍由 recv 等接口读取 5 图为常见本机 IP 路径;generic XDP、隧道、旁路与卸载可能改变经过的节点。

DMA 将帧写入驱动管理的缓冲区,完成描述符告诉驱动有哪些数据可处理。驱动还必须遵守 DMA 缓存、顺序和 buffer 回收规则;发现收包完成不意味着可以立即把该 page 重新交给网卡覆盖。

常见 native XDP 在构造常规 skb 之前运行。packet socket 的抓包入口、bridge RX handler、VLAN 处理属于不同机制,不能都画成一个 RX handler 列表;generic XDP 的位置也不与 native XDP 相同。

IP PREROUTING 在路由决策之前。本机交付继续经过 INPUT,转发则经过 FORWARD 和出口处理。把 PREROUTING 放到 ip_local_deliver() 后面,会直接误导 DNAT 和路由分析。

TCP 接收还需检查连接、序列、窗口、校验和及状态,数据排入相应接收结构后可通知等待者。sk_data_ready() 等唤醒不等于应用已调度运行,更不等于应用已经复制或处理所有数据。MSG_DONTWAIT 只改变等待行为,不会让 softirq 获得一条自动直接复制到当前用户缓冲区的特殊路径。

3. NAPI:通过预算分配处理时间

NAPI 用 poll 方法处理接收和发送完成,常在 softirq 中运行,也支持 threaded NAPI、busy polling 等方式。传统中断驱动模式中,中断触发调度后保持相关设备中断屏蔽,处理完适当工作再完成轮询并恢复通知。

预算:单个 poll 与整轮 NET_RX 分开看

poll 回调的 budget 限制本轮 RX 工作量,不能与整个 NET_RX 轮次的 netdev_budget 混成“每个网卡每次固定 300 包”。TX completion 清理又不简单消耗相同的 RX 预算。budget 为 0 的调用有专门限制,不能执行 RX/page-pool/XDP 操作或调用完成接口。

耗尽预算通常意味着还要继续 poll;如果恰好处理完且工作数等于 budget,也要按 API 约定处理,避免误报完成或漏掉后续处理。napi_complete_done() 的成功返回参与是否恢复设备中断的决定。

NAPI instance、IRQ 和 RX/TX queue pair 常有接近一一对应的组织,但协议并不强制这种映射。一个 instance 可以服务多队列,收发也可分开。见 NAPI 文档。

调整 poll 预算,再向队列加入报文。重点看“恰好用满预算又恰好清空”的边界:队列为零不一定代表这一轮已释放 NAPI 调度所有权。

正在呈现知识画面
队列空了,为什么还可能继续 poll?

budget 是这次 poll 的 RX 工作上限。采用耗尽预算后继续轮询的策略时,队列即使刚好空了,也要在下一次返回不足预算时完成并恢复中断。

吞吐与时延的取舍

增加批量、硬件中断合并或轮询,可摊薄通知成本;但也可能让第一包等待更多时间,或让一个繁忙队列占据 CPU 更久。预算耗尽应结合软中断时间、队列 backlog、丢包和应用时延判断,不能只看到中断少就断言更快。

4. 发送完成的两层含义

应用 send 成功通常表示数据被 socket 接收或排入发送路径。TCP 还要受窗口、拥塞控制和 pacing 约束,然后构建 skb、经过路由、Netfilter、qdisc 和驱动发送。

完成点能据此确认什么
本机 TX completion通常可回收对应传输资源
TCP ACK对端协议栈确认序列范围
业务回复依应用协议判断处理结果

网卡完成没有跨过后两条边界。排查延迟时,应明确正在等待哪种完成。

网络驱动通过 net_device_ops 等接口提供设备操作,统计常经 rtnl_link_stats64 等接口导出。虚拟设备也实现 net_device,所以某个 skb 可能经过多项 net_device,不能把“到 net_device 了”理解成已经到物理网卡。

5. GRO、GSO、TSO 为什么会改变抓包观感

GRO 在接收侧合并可兼容报文,减少协议处理次数;GSO 描述后续如何把较大 skb 分段,软件可完成分段;TSO 是 TCP 分段的硬件卸载。GSO 不等于“只能硬件分段”,GRO 也不是把任意同连接数据无条件合成一个包。

抓包位置:分段前的对象不等于线上帧

因此本机抓到的“大 TCP 包”可能只是分段前的 skb,线上仍按路径 MTU 发送。发送侧 checksum 尚待硬件补齐时,抓包显示校验和未完成也未必是线路错误。需要在正确观测点核实,不能仅凭本机抓包就调整 MTU 或关闭所有卸载。见分段卸载。

6. RSS、RPS、RFS、XPS 分别选择什么

机制选择位置目标
RSS网卡 RX 队列选择硬件分散接收负载
RPSskb 进入后续处理的 CPU软件分散处理,可产生跨 CPU 通知
RFS结合 flow 与应用运行位置提高协议处理和应用的局部性
XPSTX 队列选择按 CPU/RX 队列等映射减少发送竞争

RSS 常通过 hash 与 indirection table 映射,不能简单规定为四元组 hash 对队列数取模。配置、封装、分片及网卡能力影响参与 hash 的字段;修改 indirection 或映射也可能使流量迁移。

同一 flow 的稳定映射有利于保序和缓存命中,但不替代 TCP 自己的序列处理。增加队列数还会增加中断、内存和 CPU 迁移成本。参见网络扩展机制。

7. 只读观察队列与计数

from pathlib import Path

for dev in sorted(Path('/sys/class/net').glob('*')):
    names = sorted(p.name for p in (dev / 'queues').glob('*'))
    counters = {}
    for key in ('rx_packets', 'tx_packets', 'rx_dropped', 'tx_dropped'):
        try:
            counters[key] = int((dev / 'statistics' / key).read_text())
        except OSError:
            pass
    print(dev.name, names, counters)

这些是设备视角累计计数;虚拟设备和物理设备可能计数同一份流量,GRO/GSO 及驱动定义又影响观测。/proc/softirqs 统计处理次数,不是包数;ethtool -S 的私有字段也不是统一 NAPI 统计接口。

按两个时间点取差值,再与实际负载窗口对应。需要更细粒度原因时,查驱动计数、socket 统计或受控追踪,而不是把所有 rx_dropped 都解释成网卡 RX ring 满。