应用 send 成功、网卡发送完成、对端 ACK、业务处理结束,是不同的时刻。沿收发路径要分别跟踪线上报文、skb 和应用字节流,它们不必一一对应。
| 查阅目标 | 阅读位置 |
| skb 怎样引用数据 | 第 1 节 |
| 接收、NAPI 与发送完成 | 第 2~4 节 |
| 卸载、队列分配与计数 | 第 5~7 节 |
1. skb 是描述符,不是固定连续数据包
struct sk_buff 存放元数据,并引用实际数据缓冲区。线性区域由 head/data/tail/end 的逻辑位置描述;其中部分成员在实现中保存偏移,并非全部都是可直接解引用的 C 指针。
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. 接收数据怎样到应用
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 工作上限。采用耗尽预算后继续轮询的策略时,队列即使刚好空了,也要在下一次返回不足预算时完成并恢复中断。
{"id":"napi-budget-and-completion","title":"队列空了,为什么还可能继续 poll?","summary":"budget 是这次 poll 的 RX 工作上限。采用耗尽预算后继续轮询的策略时,队列即使刚好空了,也要在下一次返回不足预算时完成并恢复中断。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"budget\" data-i18n=\"budget\"></label><output for=\"budget\" id=\"budget-value\"></output><input id=\"budget\" type=\"range\" min=\"1\" max=\"8\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"arrival\" data-i18n=\"arrival\"></label><output for=\"arrival\" id=\"arrival-value\"></output><input id=\"arrival\" type=\"range\" min=\"1\" max=\"16\" value=\"4\" step=\"1\"></div><div class=\"actions\"><button id=\"inject\" data-i18n=\"inject\"></button><button id=\"poll\" data-i18n=\"poll\"></button></div><p class=\"caption\" data-i18n=\"queue\"></p><div id=\"ring\"></div><div class=\"metrics\"><div><span data-i18n=\"pending\"></span><strong id=\"pending\"></strong></div><div><span data-i18n=\"dropped\"></span><strong id=\"dropped\"></strong></div></div><div class=\"card\"><header data-i18n=\"state\"></header><div id=\"state\" class=\"readout\"></div><code id=\"returned\"></code></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"exact\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"completed\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"arrived\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#ring{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:15px 0}#ring i{height:25px;border:1px dashed var(--rule);border-radius:4px;background:var(--surface)}#ring i.on{border:1px solid var(--accent);background:var(--accent-soft)}.readout{min-height:5.3em}#returned{display:block;min-height:3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let queued=0,dropped=0,scheduled=false,last=null,message='initial';function draw(){const b=+$('#budget').value;set('budget-value',b);set('arrival-value',$('#arrival').value);$('#ring').replaceChildren(...Array.from({length:32},(_,i)=>{const e=document.createElement('i');e.classList.toggle('on',i<queued);return e;}));set('pending',queued);set('dropped',dropped);set('state',t(scheduled?'scheduled':'idle'));set('returned',last===null?'return —':'return '+last.work+' / budget '+last.budget);$('#poll').disabled=!scheduled;set('explanation',t(message));}$('#budget').oninput=$('#arrival').oninput=draw;$('#inject').onclick=()=>{const n=+$('#arrival').value,accepted=Math.min(n,32-queued);queued+=accepted;dropped+=n-accepted;if(queued)scheduled=true;message='arrived';draw();};$('#poll').onclick=()=>{if(!scheduled)return;const b=+$('#budget').value,work=Math.min(queued,b);queued-=work;last={work,budget:b};scheduled=work===b;message=scheduled?'exact':'completed';draw();};$('#reset').onclick=()=>{queued=dropped=0;scheduled=false;last=null;message='initial';draw();};draw();","audio":false,"strings":{"arrival":"一次到达的包数","arrived":"到包保存在 ring,通知只安排处理。通知次数不代替队列里的包数量。","budget":"本次 poll 的 RX budget","completed":"返回值小于 budget,本模型成功完成 NAPI 并恢复中断。","dropped":"因本队列已满丢弃","exact":"本轮返回完整 budget,保持调度。即使剩余包数为 0,也先再 poll 一次来完成。","heading":"队列空了,为什么还可能继续 poll?","idle":"已完成;IRQ 可通知","initial":"队列空,等待设备通知。","inject":"让这一批包到达","intro":"调节本轮 RX budget 和到包数量,直接注入或 poll。特别比较“少于预算处理完”和“恰好耗尽预算”的最后一轮。","pending":"队列中包数","poll":"调用一次 poll","queue":"RX ring:实色为尚待处理的包","reset":"重置","scheduled":"保持调度;IRQ 屏蔽","scope":"32 项单 RX 队列,budget=1…8,传统中断触发后屏蔽模型。采用内核文档允许的“返回完整 budget,下一轮再完成”策略。省略 budget=0、TX completion、完成竞态、多队列与 busy polling;满队列到包丢弃只表示此模型的 ring 容量。","scopeLabel":"模型范围与约定","state":"调度状态 / 中断状态"}}
吞吐与时延的取舍
增加批量、硬件中断合并或轮询,可摊薄通知成本;但也可能让第一包等待更多时间,或让一个繁忙队列占据 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 或关闭所有卸载。见分段卸载。
| 机制 | 选择位置 | 目标 |
| RSS | 网卡 RX 队列选择 | 硬件分散接收负载 |
| RPS | skb 进入后续处理的 CPU | 软件分散处理,可产生跨 CPU 通知 |
| RFS | 结合 flow 与应用运行位置 | 提高协议处理和应用的局部性 |
| XPS | TX 队列选择 | 按 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 满。