写在前面

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 或开发板实验写成已验证结论。

Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径

阅读前先建立四个问题

阅读任何 skb 代码路径,都建议反复追问:

  1. 当前 skb->data 指向哪一层协议头,linear 与 non-linear 数据分别在哪里?
  2. 哪些字段刚被修改,下一层为什么依赖这些字段?
  3. 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
  4. 成功、失败、重试、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
2
3
4
5
6
7
stmmac_napi_poll_rx()
→ stmmac_rx(priv, budget, channel)
→ 读取 RX descriptor
→ 处理首段/后续段
→ 完成 skb metadata
→ napi_gro_receive()
→ stmmac_rx_refill()

关键入口:

  • 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
2
3
napi_alloc_skb()
→ skb_copy_to_linear_data()
→ skb_put()

首段包含 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
2
3
挂 frag 前:RX ring/page_pool 管理 page
挂 frag 后:skb 持有该 page 引用
skb free:frag unref,5.10 BSP 由相应回收路径处理

驱动必须从 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_NONEskb_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:

  1. 从 page_pool/分配器获取 page;
  2. 写入 descriptor buffer address;
  3. 清理软件状态;
  4. 使用正确屏障;
  5. 设置 OWN 交给 DMA;
  6. 更新 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
2
3
4
5
stmmac RX descriptor 完成
→ page_pool/DMA 同步
→ build_skb/napi_build_skb
→ 填写长度和 offload 元数据
→ napi_gro_receive()

2. NAPI、GRO、RPS 与 RX core

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
2
3
4
5
6
7
8
9
napi_gro_receive()
→ dev_gro_receive()
→ napi_skb_finish()
├─ GRO_HELD / GRO_MERGED:当前 skb 被 GRO 持有或消费
└─ GRO_NORMAL:gro_normal_one()/netif_receive_skb_internal()
→ __netif_receive_skb()
→ __netif_receive_skb_core()
→ packet_type handler
→ ip_rcv()/ipv6_rcv()/ARP/bridge/...

关键位置:

  • 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->protocolptype_base 中找到 IPv4、IPv6、ARP 等 handler。IPv4 通常进入 ip_rcv()

五、pt_prev 延迟投递

core 遍历多个 packet_type handler 时,用 pt_prev 暂存前一个 handler:

  • 发现下一个匹配者时,才把 skb 引用交给前一个;
  • 最后一个 handler 可直接获得原 skb;
  • 减少不必要 clone/ref 操作。

这是一种热路径所有权优化。阅读代码时需同时追踪 skbpt_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 仍有效。

八、常见误区

  1. netif_receive_skb() 不保证在当前 CPU 立即执行协议栈,RPS 可排队。
  2. generic XDP 不是 native XDP,它已经有 skb。
  3. skb->dev 在 core 内可能变化,orig_dev 用于保留原输入设备。
  4. ptype tap 可能让一个包有多个观察者,需要引用/clone。
  5. hook 返回后原 skb 可能已被消费或替换。
  6. 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、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
2
3
4
5
6
7
8
9
10
11
ip_rcv()
→ ip_rcv_core()
→ NF_INET_PRE_ROUTING
→ ip_rcv_finish()
→ route input
→ ip_local_deliver()
→ NF_INET_LOCAL_IN
→ ip_local_deliver_finish()
→ ip protocol handler
├─ udp_rcv()
└─ tcp_v4_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
2
3
4
5
6
7
udp_rcv()
→ __udp4_lib_rcv()
→ socket lookup
→ checksum/filter/policy
→ udp_queue_rcv_skb()
→ sock_queue_rcv_skb()
→ sk_receive_queue

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
2
3
4
5
tcp_v4_rcv()
→ 校验 header/checksum
→ socket lookup
→ 未锁:tcp_v4_do_rcv()
→ 已锁:sk_add_backlog()

关键位置:

  • 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

十一、常见误区

  1. socket lookup 成功不等于立即进入 receive queue,TCP 状态机和 socket lock 会改变路径。
  2. UDP datagram 与 TCP byte stream 对 skb 队列语义不同。
  3. socket 内存按 truesize 计量,不按 skb->len
  4. backlog 不是 netdev backlog;前者是 socket 锁竞争路径。
  5. IP 分片重组发生在 L4 分发之前。
  6. skb->sk 不是从驱动开始始终有效,通常在 socket ownership 阶段建立。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
network/transport header IPv4 与 TCP/UDP L3/L4 解析前保证可访问
sk/destructor socket queue 入队后 socket/队列持有 skb
dst/nfct 路由和 Netfilter 引用随接收、转发或丢弃释放

最小调用链

1
2
3
4
5
6
ip_rcv()
→ Netfilter PRE_ROUTING
→ 路由/local delivery
→ tcp_v4_rcv()/udp_rcv()
→ socket receive/backlog queue
→ recvmsg()/free

源码阅读路线

建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:

  1. 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
  2. 沿成功主线记录 data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化;
  3. 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
  4. 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。

本篇总结

  1. 驱动不只交付字节,还要把长度、协议、checksum、hash、VLAN 等硬件结果编码进 skb。
  2. NAPI/GRO/RPS 决定包何时批量处理、是否聚合以及在哪个 CPU 继续执行。
  3. L3/L4 逐层消费 header 并把 skb 交给 socket receive/backlog queue。
  4. 每个返回码都隐含所有权契约:继续传递、排队、克隆、丢弃或已经消费。

sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。

系列目录

  1. Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
  2. Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
  3. Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径(本文)
  4. Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
  5. Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
  6. Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
  7. Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
  8. Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进