Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
写在前面
sk_buff 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”,而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的控制对象和所有权载体。
以 RK3588 stmmac 为驱动实例,追踪 DMA page 构造 skb、NAPI/GRO/RPS、IPv4、TCP/UDP 到 Socket 接收队列的完整所有权变化。
本文以本地 Linux 5.10.209 源码为主线;涉及演进时对照 Linux 6.1.99,涉及网卡驱动时结合 Firefly RK3588 SDK Linux 5.10.198。本文是源码分析,不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。
阅读前先建立四个问题
阅读任何 skb 代码路径,都建议反复追问:
- 当前
skb->data指向哪一层协议头,linear 与 non-linear 数据分别在哪里? - 哪些字段刚被修改,下一层为什么依赖这些字段?
- 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
- 成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb?
本篇核心结论
- 驱动不只交付字节,还要把长度、协议、checksum、hash、VLAN 等硬件结果编码进 skb。
- NAPI/GRO/RPS 决定包何时批量处理、是否聚合以及在哪个 CPU 继续执行。
- L3/L4 逐层消费 header 并把 skb 交给 socket receive/backlog queue。
- 每个返回码都隐含所有权契约:继续传递、排队、克隆、丢弃或已经消费。
1. RK3588 stmmac:从 DMA page 构造 skb
结论摘要
RK3588 BSP 的 stmmac RX 路径把 DMA descriptor 指向的 page 转换为一个 skb:首段通常复制到新分配 skb 的 linear head,后续段通过 skb_add_rx_frag() 零拷贝挂载。copy 路径中的 RX page 可立即返回 page_pool;frag 路径把 page 所有权转交给 skb,必须等 skb 释放。完整包补充 checksum、hash、RX queue、protocol 后交给 napi_gro_receive()。
一、调用位置
1 | stmmac_napi_poll_rx() |
关键入口:
stmmac_rx():firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3862;stmmac_rx_refill():firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:3740;- NAPI 调用点:
firefly_rk3588_SDK/kernel/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c:4081。
二、descriptor 与 buffer 所有权
RX descriptor 的 OWN 位区分 DMA 和 CPU 所有权:
- OWN 属于 DMA:CPU 不能读取正在被硬件写入的 buffer;
- OWN 被硬件清除:驱动可以读取状态和 packet data;
- refill 完成并设置 OWN:buffer 再次交给 DMA。
驱动推进 cur_rx 只是软件消费进度,真正把 descriptor 重新交给硬件需要 refill、地址写入和内存屏障。
三、为什么首段复制到 linear head
典型逻辑:
1 | napi_alloc_skb() |
首段包含 Ethernet/IP/TCP 等协议头。将小的前部数据放入 linear 区有利于:
- 协议解析直接访问;
- 避免每层都调用
pskb_may_pull(); - 改写 header 时更容易 COW;
- RX page 复制后可立即返回 page_pool;
- copybreak 在小包场景避免长期占用整个 page。
代价是一次 CPU copy,因此不是所有驱动和包长都采用完全相同策略。
四、后续段为什么挂 frags
多 descriptor 包的后续 page 通常不包含热协议头。驱动调用:
1 | skb_add_rx_frag(skb, index, page, offset, size, truesize); |
它把 page 描述加入 shared info,并更新 nr_frags/len/data_len/truesize。优点是避免复制大 payload,便于 GRO、协议栈和 TX scatter-gather 继续使用 page。
所有权变化:
1 | 挂 frag 前:RX ring/page_pool 管理 page |
驱动必须从 RX buffer bookkeeping 中移除或标记已转交的 page,再为 descriptor 分配/获取替代 page。
五、copy 与 frag 回收差异
| 路径 | 包数据去向 | 原 RX page | 释放时机 |
|---|---|---|---|
| 首段 copy | skb linear head | 数据已复制,可直接 recycle | 当前 poll/refill 周期 |
| 后续 frag | skb frags[] |
所有权转交 skb | skb 数据引用归零时 |
如果驱动把已经挂入 frag 的 page 又直接 recycle,会造成 use-after-free/DMA corruption;如果忘记转交后的回收路径,会泄漏 page。
六、半包状态
一个 frame 可能跨多个 descriptor,而 NAPI budget 或 ring 当前可用 descriptor 可能让处理暂时停止。驱动需要保存:
- 当前正在构造的 skb;
- 已消费长度;
- 下一个 descriptor;
- 是否已看到 last segment。
恢复后继续向同一个 skb 添加 frag。错误 descriptor、超长 frag 数、分配失败等路径必须释放已构造 skb,并正确处理尚未转交/已经转交的 page。
七、完成包的 metadata
checksum
若硬件确认 L3/L4 checksum 有效,驱动可设置 CHECKSUM_UNNECESSARY;否则保持 CHECKSUM_NONE。skb_checksum_none_assert() 只是断言当前状态,不等于硬件校验成功。
hash
硬件 RSS/hash 可通过 skb_set_hash() 写入 skb->hash 并设置 hash 类型。后续 GRO、RPS/RFS、reuseport 等可以复用,避免再次解析。
RX queue
skb_record_rx_queue() 把来源硬件队列记录到 queue_mapping,用于统计、socket RX queue 查询和流调度。
protocol
1 | skb->protocol = eth_type_trans(skb, dev); |
eth_type_trans() 不只是读取 EtherType,还保存/调整 MAC header、推进 data 到 L3 位置,并处理 packet type 等二层语义。因此不应在驱动中只手工赋一个常量替代。
八、交给 GRO
1 | napi_gro_receive(&rx_q->napi, skb); |
调用后 skb 所有权交给 GRO/网络栈。驱动不能继续访问或释放它。GRO 可能:
- hold;
- merge 并消费当前 skb;
- flush 旧 skb;
- normal receive。
返回值用于统计,不代表驱动重新获得 skb。
九、RX refill
stmmac_rx_refill() 为已经消费的 descriptor 准备新 buffer:
- 从 page_pool/分配器获取 page;
- 写入 descriptor buffer address;
- 清理软件状态;
- 使用正确屏障;
- 设置 OWN 交给 DMA;
- 更新 dirty/clean index。
refill 与构造 skb 是同一生命周期闭环:取 page → DMA 写入 → CPU 构造 skb/回收 → 补充新 page。
十、字段与所有权变化表
| 时点 | linear/frags | len/data_len |
page owner | skb owner |
|---|---|---|---|---|
| descriptor 完成 | 无 skb | - | RX ring/driver | - |
| 首段 copy | linear 增长 | len 增、data_len 0 | 原 page 可 recycle | driver |
| 后续 frag | nr_frags 增 | len/data_len 增 | skb | driver |
| metadata 完成 | header/hash/csum/queue 有效 | 不变 | skb | driver |
napi_gro_receive |
可能被 GRO 重组 | 可能变化 | network stack | GRO/stack |
十一、BSP 适用边界
这是 Firefly RK3588 SDK 的 stmmac 具体实现,不应推广为所有网卡的唯一方式。其他驱动可能:
- 用
build_skb()直接包装整页; - 全部使用 frags;
- 使用 XDP 后再决定是否构造 skb;
- 使用不同 copybreak;
- 在更新版本中通过
pp_recycle自动 page_pool 回收。
通用不变量仍然是:明确 descriptor、page 和 skb 三者所有权,metadata 完成后再把 skb 交给上层。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
| RX page | page_pool/DMA | 驱动与 page_pool 在 descriptor 和 skb 间转移 |
len/data_len |
stmmac RX 构包 | 决定 linear 与 frag 数据边界 |
| checksum/hash/VLAN | descriptor 状态解析 | 驱动写元数据,协议栈消费 |
最小调用链
1 | stmmac RX descriptor 完成 |
2. NAPI、GRO、RPS 与 RX core
结论摘要
驱动通过 napi_gro_receive() 交出 skb 后,GRO 决定 hold/merge/flush/normal。进入 __netif_receive_skb_core() 后,skb 依次经过时间戳/设备与输入接口初始化、generic XDP、VLAN、rx handler、ingress tc、Netfilter ingress、ptype tap 和协议 handler。每个 hook 都可能替换、clone、redirect、queue 或消费 skb,因此主路径使用 pt_prev 延迟投递以减少 clone,并严格维护所有权。
一、主调用链
1 | napi_gro_receive() |
关键位置:
dev_gro_receive():kernel-5.10/net/core/dev.c:5991;napi_gro_receive():kernel-5.10/net/core/dev.c:6166;__netif_receive_skb_core():kernel-5.10/net/core/dev.c:5168;netif_receive_skb():kernel-5.10/net/core/dev.c:5652。
二、GRO 交接语义
GRO result 不是简单成功/失败:
GRO_MERGED:当前 skb 数据已并入已有 skb,当前 skb 通常被消费;GRO_MERGED_FREE:合并后还需特定释放;GRO_HELD:skb 留在 NAPI GRO list;GRO_NORMAL:不能合并,进入普通协议栈;GRO_DROP:丢弃。
驱动调用后无论返回哪个结果,都不再拥有 skb。
三、进入 core 前的上下文
netif_receive_skb() 适用于进程/软中断等通用入口,内部可根据 RPS 决定本 CPU 直接处理或 enqueue 到目标 CPU backlog。__netif_receive_skb() 是更内部的直接版本。
RPS 可能让 skb 先进入另一个 CPU 的 softnet_data.input_pkt_queue,之后由 backlog NAPI 再调用 core。于是“驱动 poll CPU”不必等于“协议栈执行 CPU”。
四、__netif_receive_skb_core 的关键阶段
1. 初始化输入语义
函数保存 orig_dev,设置/维护 skb_iif,处理 pfmemalloc 和时间戳等状态。skb->dev 在 bridge、bond、VLAN、redirect 等路径中可能变化,所以原始设备和当前设备需要区分。
2. generic XDP
若设备挂载 generic XDP,do_xdp_generic() 在 kernel-5.10/net/core/dev.c:4784,调用点约 5201。此时 skb 已经存在,与 native XDP 的“构造 skb 之前”不同。
XDP verdict 可能 pass、drop、tx、redirect;非 pass 时 skb 可能已消费,core 不能继续使用原指针。
3. VLAN untag/metadata
skb_vlan_untag() 可把包内 VLAN header 转成 skb VLAN metadata,并调整 data/header。硬件已经剥离的 VLAN tag 则可能直接存在 vlan_tci/vlan_proto。
4. ptype_all tap
AF_PACKET/tcpdump 等 tap 通过 ptype_all 观察包。一个 skb 需要交给多个 handler 时不能让第一个 handler 独占原 skb,因此核心通过 clone 或 deliver_skb() 增加引用。
5. ingress tc
sch_handle_ingress() 位于 kernel-5.10/net/core/dev.c:5000,调用点约 5241。tc action 可修改、drop、redirect、mirror 或 consume skb。
6. Netfilter ingress
若配置 ingress hook,Netfilter verdict 决定继续、queue、stolen 或 drop。所有权取决于 verdict,不能按普通函数返回理解。
7. rx_handler
bridge、bond、macvlan 等设备可注册 rx_handler。它可能:
- 消费 skb;
- 修改
skb->dev后重新开始; - 精确传递;
- 让 skb 继续普通协议分发。
8. protocol handler
根据 skb->protocol 在 ptype_base 中找到 IPv4、IPv6、ARP 等 handler。IPv4 通常进入 ip_rcv()。
五、pt_prev 延迟投递
core 遍历多个 packet_type handler 时,用 pt_prev 暂存前一个 handler:
- 发现下一个匹配者时,才把 skb 引用交给前一个;
- 最后一个 handler 可直接获得原 skb;
- 减少不必要 clone/ref 操作。
这是一种热路径所有权优化。阅读代码时需同时追踪 skb 与 pt_prev,否则容易误判一个 handler 是否拿到原 skb。
六、header 与字段变化
| 阶段 | 典型变化 |
|---|---|
| 驱动完成 | dev/protocol/hash/ip_summed/queue_mapping 已设置 |
| GRO | len/data_len/frags/frag_list/GRO cb 可能变化 |
| generic XDP | data/head/len/redirect 可能变化 |
| VLAN | VLAN metadata、protocol、data/header 变化 |
| rx_handler | dev、protocol 或所有权变化 |
| ingress tc/NF | mark、priority、data、redirect、nfct 等变化 |
| ptype/L3 | network header、dst、cb overlay 变化 |
任何可能重分配 head 的 hook 之后,旧 header 指针都不能继续使用。
七、所有权退出点
- GRO merge/drop;
- XDP drop/redirect/tx;
- tc shot/stolen/redirect;
- Netfilter drop/queue/stolen;
- rx_handler consumed;
- protocol handler 接管;
- 无 handler 时 core 释放。
core 的返回码通常用于统计,调用者不能因为返回“成功”就认为 skb 仍有效。
八、常见误区
netif_receive_skb()不保证在当前 CPU 立即执行协议栈,RPS 可排队。- generic XDP 不是 native XDP,它已经有 skb。
skb->dev在 core 内可能变化,orig_dev用于保留原输入设备。- ptype tap 可能让一个包有多个观察者,需要引用/clone。
- hook 返回后原 skb 可能已被消费或替换。
napi_gro_receive()返回并不把所有权还给驱动。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
dev/protocol |
驱动与 eth_type_trans() |
RX core 接管后逐层转交 |
hash/napi_id |
GRO/RPS | 决定聚合与 CPU 调度 |
| skb shell | NAPI/GRO/backlog | 返回码和 enqueue 结果决定消费方 |
3. IPv4、TCP/UDP 与 Socket 接收队列
结论摘要
IPv4 RX 先验证并规范化 skb,再经 PRE_ROUTING、路由输入和 LOCAL_IN 到达 L4 handler。UDP 查找 socket 后把 skb 作为完整 datagram 排入 receive queue;TCP 把 skb 交给连接状态机,可能进入 backlog、out-of-order queue 或 receive queue。socket 通过 truesize 而非包长进行内存计量,owner/destructor 负责在 skb 最终释放时归还计数。
一、IPv4 主链
1 | ip_rcv() |
关键位置:
ip_rcv_core():kernel-5.10/net/ipv4/ip_input.c:442;ip_rcv():kernel-5.10/net/ipv4/ip_input.c:537;ip_local_deliver():kernel-5.10/net/ipv4/ip_input.c:240;ip_local_deliver_finish():kernel-5.10/net/ipv4/ip_input.c:226。
二、ip_rcv_core 对 skb 的规范化
典型工作:
- 确保 IPv4 基本头在线性区;
- 校验 version、IHL、total length;
- trim 掉 L2 padding,使
skb->len与 IP total length 一致; - 校验 IPv4 header checksum;
- 设置/清理 IPv4 控制块
IPCB(skb); - 丢弃 malformed packet。
trim 可能改变 skb 长度和非线性布局。失败路径会释放 skb,后续不能继续访问。
三、路由与 dst
PRE_ROUTING 后通过 route input 决定:
- 本机接收;
- 转发;
- multicast/broadcast;
- unreachable/drop。
路由结果附着为 skb dst,后续通过 dst input/output 回调继续。clone 时 dst 需要增加引用,free 时 skb_dst_drop()。
本地流量进入 ip_local_deliver();分片包可能先调用 ip_defrag(),重组完成后才进入 L4。
四、L4 分发
ip_local_deliver_finish() pull/定位 IP header 后,根据 iph->protocol 查找 net_protocol:
- TCP →
tcp_v4_rcv(); - UDP →
udp_rcv(); - 其它协议对应 handler;
- raw socket 可能同时获得副本。
协议 handler 接管 skb 所有权。IP 层不应在 handler 返回后再次释放同一 skb,除非约定明确。
五、UDP 接收
1 | udp_rcv() |
UDP 保留 datagram 边界,一个 skb 通常对应一个用户可读取的数据报。排队前会处理:
- UDP length;
- checksum;
- multicast/broadcast 多 socket 分发;
- socket filter;
- policy/xfrm;
- rcvbuf 限额。
一个 multicast 包可能需要 clone 给多个 socket。
六、sock_queue_rcv_skb 与 owner
sock_queue_rcv_skb() 位于 kernel-5.10/net/core/sock.c:474。通用逻辑会检查接收内存和 filter,然后把 skb 入 sk_receive_queue。
skb_set_owner_r(skb, sk) 通常:
- 设置
skb->sk = sk; - 设置 destructor 为
sock_rfree; - 按
skb->truesize增加 socket 接收内存计量。
最终 skb free 时 destructor 归还 rmem。不能只在 dequeue 时手工减包长,因为 clone、frag、truesize 和异步释放会让计量失真。
七、socket backlog
如果 socket 当前被用户上下文持锁,软中断不能直接执行完整协议处理,通常通过 sk_add_backlog() 把 skb 放入 sk_backlog。
- backlog 仍属于该 socket;
- socket 解锁时调用
sk_backlog_rcv; - TCP 对应
tcp_v4_do_rcv(); - backlog 同样受内存 limit;
- 入队失败必须释放并计数 drop。
sk_add_backlog() 定义位于 kernel-5.10/include/net/sock.h:1032。
八、TCP 接收
1 | tcp_v4_rcv() |
关键位置:
tcp_v4_rcv():kernel-5.10/net/ipv4/tcp_ipv4.c:1936;tcp_v4_do_rcv():kernel-5.10/net/ipv4/tcp_ipv4.c:1670。
TCP 不一定把原 skb 直接放入用户 receive queue。状态机可能:
- 消费纯 ACK;
- 合并相邻数据;
- 把乱序段放入 out-of-order rbtree/queue;
- 把按序数据进入 receive queue;
- clone/split skb;
- 因窗口、状态、checksum、RST 等释放。
TCP_SKB_CB 使用 cb 保存 seq/end_seq/flags/sacked/timestamp 等 TCP 私有状态。
九、用户读取与最终释放
UDP recvmsg 通常从 receive queue dequeue 一个 datagram,复制或映射数据后 consume skb。TCP recvmsg 按字节流消费,一个 skb 可能部分读取、被切分或继续留队。
最终 users 归零后:
- socket destructor 归还 rmem;
- dst/nfct/extensions 释放;
- frags/frag_list/head 释放;
- shell 释放。
十、字段/所有权变化
| 阶段 | 关键字段 | owner |
|---|---|---|
| IP 入口 | network header、IPCB、len trim | IP stack |
| route | dst、dev 语义 | routing/IP |
| LOCAL_IN | nfct/mark 可能变化 | Netfilter/IP |
| UDP/TCP lookup | sk 候选 |
protocol |
| backlog/receive queue | cb、owner、truesize accounting | socket |
| recv/free | data 指针/len 可能推进 | user syscall/socket |
十一、常见误区
- socket lookup 成功不等于立即进入 receive queue,TCP 状态机和 socket lock 会改变路径。
- UDP datagram 与 TCP byte stream 对 skb 队列语义不同。
- socket 内存按 truesize 计量,不按
skb->len。 - backlog 不是 netdev backlog;前者是 socket 锁竞争路径。
- IP 分片重组发生在 L4 分发之前。
skb->sk不是从驱动开始始终有效,通常在 socket ownership 阶段建立。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
| network/transport header | IPv4 与 TCP/UDP | L3/L4 解析前保证可访问 |
sk/destructor |
socket queue | 入队后 socket/队列持有 skb |
| dst/nfct | 路由和 Netfilter | 引用随接收、转发或丢弃释放 |
最小调用链
1 | ip_rcv() |
源码阅读路线
建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:
- 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
- 沿成功主线记录
data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化; - 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
- 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。
本篇总结
- 驱动不只交付字节,还要把长度、协议、checksum、hash、VLAN 等硬件结果编码进 skb。
- NAPI/GRO/RPS 决定包何时批量处理、是否聚合以及在哪个 CPU 继续执行。
- L3/L4 逐层消费 header 并把 skb 交给 socket receive/backlog queue。
- 每个返回码都隐含所有权契约:继续传递、排队、克隆、丢弃或已经消费。
sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。
系列目录
- Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
- Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
- Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径(本文)
- Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
- Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
- Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
- Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
- Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进
