本系列基于 Linux 6.4-rc1 源码(commit ac9a78681b92),所有代码片段均从真实源文件读取,路径为 include/linux/skbuff.h 与 net/core/skbuff.c。
一、为什么要读懂 Linux 网络协议栈 网络性能调优、eBPF 程序开发、内核模块编写,乃至排查一个”神秘的丢包”——这些工作的底层都有一个共同的核心:sk_buff (socket buffer)。它是 Linux 内核网络子系统中最关键的数据结构,每一个在内核中流动的网络报文都以 sk_buff 的形式存在。
本系列文章将从零到深,系统拆解 Linux 网络协议栈。第一篇先建立全局视图,再深入 sk_buff 的内存模型与操作原语。
二、网络协议栈完整架构 2.1 接收路径(RX Path) 从网卡收到一帧,到用户态 read() 返回数据,内核经历的完整调用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 硬件中断 └─ napi_schedule() // 将 NAPI poll 加入软中断队列 └─ net_rx_action() // softirq: NET_RX_SOFTIRQ └─ napi->poll() // 驱动的 poll 函数,如 igb_poll() └─ napi_gro_receive() └─ netif_receive_skb() └─ __netif_receive_skb_core() ├─ deliver_skb() → packet socket tap └─ ip_rcv() // IPv4 入口 └─ ip_rcv_finish() └─ ip_local_deliver() └─ ip_local_deliver_finish() └─ tcp_v4_rcv() // TCP 入口 └─ tcp_rcv_established() └─ tcp_queue_rcv() └─ sk->sk_data_ready() └─ 唤醒用户态 read()
关键节点说明:
NAPI(New API) 是现代驱动的标准轮询机制。硬中断只负责触发 napi_schedule(),将 poll 工作推给软中断 NET_RX_SOFTIRQ,从而避免高速网卡的中断风暴。驱动在 poll 函数中用 napi_build_skb() 或 __napi_alloc_skb() 创建 skb,并调用 napi_gro_receive() 进入 GRO(Generic Receive Offload)聚合层。
netif_receive_skb() 是协议层的分发入口。它通过 ptype_base 哈希表查找与 skb->protocol 对应的协议处理函数(如 ip_rcv、ipv6_rcv)。
ip_rcv() 完成 IP 首部校验、路由查找(ip_route_input_noref()),然后决定报文去向:本地投递还是转发。本地投递进入 ip_local_deliver(),这里完成 IP 分片重组,再调用上层协议(TCP/UDP/ICMP)的接收函数。
tcp_v4_rcv() 根据四元组找到 socket,将报文挂入 sk->sk_receive_queue,并通过 sk->sk_data_ready 回调唤醒阻塞在 read() 的用户进程。
2.2 发送路径(TX Path) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 用户态 write() / sendmsg() └─ sock_sendmsg() └─ tcp_sendmsg() └─ tcp_push_one() / __tcp_push_pending_frames() └─ tcp_write_xmit() └─ tcp_transmit_skb() // 封装 TCP 首部 └─ ip_queue_xmit() // 封装 IP 首部 + 路由 └─ ip_output() └─ ip_finish_output() └─ ip_finish_output2() └─ neigh_output() // ARP/邻居子系统 └─ dev_queue_xmit() // 入队 └─ qdisc_run() // 流量控制 └─ dev_hard_start_xmit() └─ ndo_start_xmit() // 驱动发送
发送路径的核心在于 TCP 在 sk_write_queue 中维护待发送的 skb 。TCP 实现了可靠传输,skb 只有在收到 ACK 后才会通过 tcp_clean_rtx_queue() 释放。发送时,TCP 先调用 skb_clone() 产生一个 fclone(fast-clone),将克隆体交给 IP 层,自己保留原始 skb 用于重传。
三、sk_buff 结构体深度分析 3.1 什么是 sk_buff sk_buff 是 Linux 内核网络子系统中报文的载体。它本身不直接存储报文数据 ,而是一个元数据描述符,通过指针指向实际的数据缓冲区。其设计目标是:在协议栈各层之间传递报文时,尽量避免数据拷贝 ,只修改指针和元数据。
3.2 核心内存布局:四个指针 skbuff.h 中的官方注释给出了最清晰的内存模型(源码第 706-728 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
四个核心指针在结构体末尾(源码第 1053-1057 行):
1 2 3 4 5 6 7 sk_buff_data_t tail;sk_buff_data_t end;unsigned char *head, *data; unsigned int truesize;refcount_t users;
各指针含义:
指针
含义
head
分配的缓冲区起始地址,固定不变
data
当前有效数据的起始位置(随 skb_push/pull 移动)
tail
当前有效数据的结束位置(随 skb_put 移动)
end
缓冲区末尾(紧接 skb_shared_info),固定不变
在 64 位系统(NET_SKBUFF_DATA_USES_OFFSET 宏定义时),tail 和 end 存储的是相对于 head 的偏移量 ,而非指针,节省内存并方便原子操作。
Headroom(头部预留空间) 是 data - head 的字节数,发送时用于各层协议依次 skb_push() 写入自己的首部,无需移动数据。Tailroom 是 end - tail 的字节数(不含 skb_shared_info),接收时用于 skb_put() 追加数据。
3.3 len、data_len 与 truesize 的区别 这三个字段是初学者最容易混淆的:
1 2 unsigned int len, data_len;
skb->len:报文的 逻辑总长度 ,等于线性区域长度加上所有分片的长度。skb_headlen(skb) + skb->data_len。
skb->data_len:仅计算 非线性部分 (page frags + frag_list)的字节数。线性 skb 的 data_len 为 0。
**truesize**:内核为这个 skb 实际占用的内存大小 ,包含 sk_buff 结构体本身、数据缓冲区以及 skb_shared_info。用于 socket 发送/接收缓冲区的内存计费(sk_wmem_alloc / sk_rmem_alloc)。
1 2 3 4 #define SKB_TRUESIZE(X) ((X) + \ SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \ SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
truesize 的精确计算在 __finalize_skb_around() 中(net/core/skbuff.c 第 352-375 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static inline void __finalize_skb_around(struct sk_buff *skb, void *data, unsigned int size) { struct skb_shared_info *shinfo ; size -= SKB_DATA_ALIGN(sizeof (struct skb_shared_info)); skb->truesize = SKB_TRUESIZE(size); refcount_set(&skb->users, 1 ); skb->head = data; skb->data = data; skb_reset_tail_pointer(skb); skb_set_end_offset(skb, size); }
3.4 cb[48]:各层私有控制块 1 char cb[48 ] __aligned(8 );
这 48 字节是各协议层的私有暂存区 ,对上下层完全透明。内核使用宏来访问它:
IP 层(include/net/ip.h):
1 2 3 4 5 6 7 8 struct inet_skb_parm { int iif; struct ip_options opt ; u16 flags; }; #define IPCB(skb) ((struct inet_skb_parm*)((skb)->cb))
TCP 层(include/net/tcp.h):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct tcp_skb_cb { __u32 seq; __u32 end_seq; union { __u32 tcp_tw_isn; struct { u16 tcp_gso_segs; u16 tcp_gso_size; }; }; union { struct { u32 is_app_limited:1 ; } tx; union { struct inet_skb_parm h4 ; struct inet6_skb_parm h6 ; } header; }; }; #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
TCP 在发包时用 TCP_SKB_CB(skb)->seq 记录序列号,在收包时用 TCP_SKB_CB(skb)->header.h4 读取 IP 层留下的信息。由于 sizeof(struct tcp_skb_cb) <= 48,两者都合法地复用同一块内存。
3.5 sk 与 dev 字段 1 2 3 4 union { struct sock *sk ; int ip_defrag_offset; };
skb->sk 指向拥有这个报文的 socket。在发送路径中,skb 一直持有对 socket 的引用(用于内存计费);在接收路径进入 TCP/UDP 处理后也会被赋值。ip_defrag_offset 是 IP 分片重组阶段对 sk 字段的临时复用,无需额外内存。
1 2 3 4 5 6 7 8 9 10 11 union { struct { struct sk_buff *next ; struct sk_buff *prev ; union { struct net_device *dev ; unsigned long dev_scratch; }; }; };
skb->dev 指向 skb 当前关联的网络设备 (struct net_device)。在接收路径,它指向收包的网卡;在发送路径,它指向出口网卡。某些协议(如 UDP 接收路径)会将 dev 置 NULL 并用 dev_scratch 存储其他临时信息。
3.6 _skb_refdst:路由缓存 1 2 3 4 5 6 7 union { struct { unsigned long _skb_refdst; void (*destructor)(struct sk_buff *skb); }; };
_skb_refdst 存储指向 struct dst_entry 的指针(路由缓存条目)。最低位被用作标志位 SKB_DST_NOREF,表示是否持有引用计数:
1 2 3 4 5 6 7 8 9 #define SKB_DST_NOREF 1UL #define SKB_DST_PTRMASK ~(SKB_DST_NOREF) static inline struct dst_entry *skb_dst (const struct sk_buff *skb) { WARN_ON((skb->_skb_refdst & SKB_DST_NOREF) && !rcu_read_lock_held() && !rcu_read_lock_bh_held()); return (struct dst_entry *)(skb->_skb_refdst & SKB_DST_PTRMASK); }
在快速转发路径(如 ip_forward())中,内核使用 skb_dst_set_noref() 以 RCU 方式持有路由缓存,避免昂贵的引用计数操作。
3.7 GSO/GRO 相关字段 GSO(Generic Segmentation Offload)和 GRO(Generic Receive Offload)是 Linux 将分段/聚合工作推迟到驱动层或硬件的机制,核心信息存储在 skb_shared_info(见下节)中:
1 2 3 4 5 6 7 struct skb_shared_info { unsigned short gso_size; unsigned short gso_segs; unsigned int gso_type; };
发送路径中,TCP 构造一个大 skb(可包含多个 MSS 的数据),设置 gso_size = tp->mss_cache、gso_segs = 数量,并设置 ip_summed = CHECKSUM_PARTIAL,由驱动或网卡完成实际切割和 checksum。
接收路径中,GRO 将多个小包聚合成一个大 skb,减少协议栈处理次数。skb->slow_gro 标志指示该 skb 在 GRO 合并时携带了额外状态(连接跟踪、路由等),需要在释放时特殊处理。
3.8 Checksum Offload 字段 1 2 3 4 5 6 7 8 __u8 ip_summed:2 ; union { __wsum csum; struct { __u16 csum_start; __u16 csum_offset; }; };
四种状态的含义(源码 include/linux/skbuff.h 第 99-250 行有完整文档):
值
场景
含义
CHECKSUM_NONE
RX
硬件未校验,软件需要验证
CHECKSUM_UNNECESSARY
RX
硬件已验证,无需软件重算
CHECKSUM_COMPLETE
RX
硬件提供了整包 checksum(存于 skb->csum)
CHECKSUM_PARTIAL
TX
软件填写了伪首部,请求硬件完成最终 checksum
TX 路径中,csum_start 是从 skb->head 开始计算 checksum 的起始偏移,csum_offset 是相对于 csum_start 的 checksum 写入位置。
3.9 skb_shinfo() 与 skb_shared_info skb_shared_info 紧跟在数据缓冲区的末尾(end 指针处):
1 #define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
完整结构(源码第 574-599 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct skb_shared_info { __u8 flags; __u8 meta_len; __u8 nr_frags; __u8 tx_flags; unsigned short gso_size; unsigned short gso_segs; struct sk_buff *frag_list ; struct skb_shared_hwtstamps hwtstamps ; unsigned int gso_type; u32 tskey; atomic_t dataref; unsigned int xdp_frags_size; void *destructor_arg; skb_frag_t frags[MAX_SKB_FRAGS]; };
frags[] 是 Scatter-Gather IO 的核心。每个 skb_frag_t(实为 struct bio_vec)描述一个页片段:
1 2 3 4 5 6 7 8 typedef struct bio_vec skb_frag_t ;struct bio_vec { struct page *bv_page ; unsigned int bv_len; unsigned int bv_offset; };
网卡驱动通过 DMA 将数据直接写入这些页,frags[] 只存储(页, 偏移, 长度)三元组,完全避免数据拷贝。
frag_list 是一个 skb 链表,用于两种场景:
IP 分片重组 :各个分片 skb 通过 frag_list 链接在一起,skb->len 是全部分片的总长度。
GSO fraglist :SKB_GSO_FRAGLIST 模式下,将多个小 skb 链成链表交给驱动。
dataref 是 skb_shared_info 中最重要的计数器,分为两个 16 位字段:低 16 位是总引用数,高 16 位是 payload-only 引用数(nohdr clone)。skb_clone() 共享数据区时递增此计数,skb_release_data() 递减,归零后才真正释放数据缓冲区。
四、sk_buff 操作函数详解 4.1 alloc_skb / __alloc_skb:slab 缓存分配 内核为 sk_buff 头部维护了专用的 slab 缓存(net/core/skbuff.c 第 89-90 行):
1 2 struct kmem_cache *skbuff_cache __ro_after_init ;static struct kmem_cache *skbuff_fclone_cache __ro_after_init ;
核心分配函数 __alloc_skb()(源码第 625-687 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 struct sk_buff *__alloc_skb (unsigned int size , gfp_t gfp_mask , int flags , int node ) { struct kmem_cache *cache ; struct sk_buff *skb ; bool pfmemalloc; u8 *data; cache = (flags & SKB_ALLOC_FCLONE) ? skbuff_fclone_cache : skbuff_cache; if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX)) gfp_mask |= __GFP_MEMALLOC; if ((flags & (SKB_ALLOC_FCLONE | SKB_ALLOC_NAPI)) == SKB_ALLOC_NAPI && likely(node == NUMA_NO_NODE || node == numa_mem_id())) skb = napi_skb_cache_get(); else skb = kmem_cache_alloc_node(cache, gfp_mask & ~GFP_DMA, node); if (unlikely(!skb)) return NULL ; prefetchw(skb); data = kmalloc_reserve(&size, gfp_mask, node, &pfmemalloc); if (unlikely(!data)) goto nodata; memset (skb, 0 , offsetof(struct sk_buff, tail)); __build_skb_around(skb, data, size); skb->pfmemalloc = pfmemalloc; if (flags & SKB_ALLOC_FCLONE) { struct sk_buff_fclones *fclones ; fclones = container_of(skb, struct sk_buff_fclones, skb1); skb->fclone = SKB_FCLONE_ORIG; refcount_set(&fclones->fclone_ref, 1 ); } return skb; nodata: kmem_cache_free(cache, skb); return NULL ; }
分配分两步:
从 slab 缓存分配 sk_buff 结构体(仅元数据,约 232 字节)
调用 kmalloc_reserve() 分配数据缓冲区
kmalloc_reserve() 内部会优先尝试 skb_small_head_cache(专为 TCP 头部优化的小块缓存,大小为 SKB_HEAD_ALIGN(MAX_TCP_HEADER)),对于不超过 1024 字节的小报文走快速路径。
NAPI 路径还有额外优化:napi_skb_cache_get() 维护一个 per-CPU 的 64 个 skb 的批量缓存,使用 kmem_cache_alloc_bulk() 批量分配,大幅减少 slab 调用开销。
4.2 skb_reserve:为各层头部预留空间 1 2 3 4 5 static inline void skb_reserve (struct sk_buff *skb, int len) { skb->data += len; skb->tail += len; }
在 skb 刚分配、尚未写入数据时调用,同时移动 data 和 tail,使 head 到 data 之间形成 headroom。后续各层通过 skb_push() 向前扩展,写入自己的首部,无需移动已有数据。
驱动分配接收 skb 时通常调用:
1 2 skb_reserve(skb, NET_SKB_PAD); skb_reserve(skb, NET_SKB_PAD + NET_IP_ALIGN);
发送路径中,sock_alloc_send_skb() 预留的空间要包含所有协议层首部的总长度:
1 NET_SKB_PAD + MAX_HEADER(ETH + IP + TCP 首部之和)
4.3 skb_push / skb_pull / skb_put:指针操作 这三个函数是协议栈各层操作数据的核心原语(源码第 2383-2428 行):
skb_put() — 向 tail 方向扩展,用于接收路径追加数据:
1 2 3 4 5 6 7 8 9 10 void *skb_put (struct sk_buff *skb, unsigned int len) { void *tmp = skb_tail_pointer(skb); SKB_LINEAR_ASSERT(skb); skb->tail += len; skb->len += len; if (unlikely(skb->tail > skb->end)) skb_over_panic(skb, len, __builtin_return_address(0 )); return tmp; }
skb_push() — 向 head 方向扩展,用于发送路径在 headroom 中写入首部:
1 2 3 4 5 6 7 8 void *skb_push (struct sk_buff *skb, unsigned int len) { skb->data -= len; skb->len += len; if (unlikely(skb->data < skb->head)) skb_under_panic(skb, len, __builtin_return_address(0 )); return skb->data; }
skb_pull() — 从 data 头部移除数据,用于接收路径跳过已处理的首部:
1 2 3 4 5 6 7 8 9 10 void *skb_pull (struct sk_buff *skb, unsigned int len) { return skb_pull_inline(skb, len); } static inline void *__skb_pull(struct sk_buff *skb, unsigned int len){ skb->len -= len; return skb->data += len; }
三者关系图示(发送路径,从上层到下层):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 初始状态(alloc + reserve 后): [headroom...................| tailroom | skb_shared_info] ^ ^ ^ head data=tail end tcp_push() → skb_push(TCP_hdr_len): [headroom......|TCP hdr|data| tailroom | skb_shared_info] ^ ^ data tail ip_output() → skb_push(IP_hdr_len): [headroom..|IP hdr|TCP hdr|data| tailroom | skb_shared_info] ^ ^ data tail dev_hard_start_xmit() → 发出后 ip_finish_output2() → skb_push(ETH_hdr_len): [ETH hdr|IP hdr|TCP hdr|data| tailroom | skb_shared_info] ^ ^ data (= head) tail
4.4 skb_clone vs skb_copy:引用计数 vs 深拷贝 **skb_clone()**(源码第 1853-1881 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct sk_buff *skb_clone (struct sk_buff *skb, gfp_t gfp_mask) { struct sk_buff_fclones *fclones = container_of(skb, struct sk_buff_fclones, skb1); struct sk_buff *n ; if (skb->fclone == SKB_FCLONE_ORIG && refcount_read(&fclones->fclone_ref) == 1 ) { n = &fclones->skb2; refcount_set(&fclones->fclone_ref, 2 ); n->fclone = SKB_FCLONE_CLONE; } else { n = kmem_cache_alloc(skbuff_cache, gfp_mask); if (!n) return NULL ; n->fclone = SKB_FCLONE_UNAVAILABLE; } return __skb_clone(n, skb); }
skb_clone() 只复制 sk_buff 元数据结构,共享数据缓冲区 (递增 shinfo->dataref)。克隆体不能修改数据内容,但可以修改自己的首部指针(data 等)。适用于:
TCP 发送时保留原始 skb 用于重传
Netfilter 分支处理
fclone 优化 :TCP 通过 SKB_ALLOC_FCLONE 标志分配 sk_buff_fclones(两个 skb 紧挨着分配),第一次 clone 时直接使用伴生的 skb2,无需额外 slab 分配。
**skb_copy()**(源码第 1933-1953 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct sk_buff *skb_copy (const struct sk_buff *skb, gfp_t gfp_mask) { int headerlen = skb_headroom(skb); unsigned int size = skb_end_offset(skb) + skb->data_len; struct sk_buff *n = __alloc_skb(size, gfp_mask, skb_alloc_rx_flag(skb), NUMA_NO_NODE); if (!n) return NULL ; skb_reserve(n, headerlen); skb_put(n, skb->len); BUG_ON(skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len)); skb_copy_header(n, skb); return n; }
skb_copy() 完全复制数据,包括 headroom、线性数据以及所有 page frags,返回一个完全独立的 skb。代价是需要实际的数据拷贝。适用于需要修改数据内容的场景。
操作
复制元数据
复制数据
共享 shinfo
可修改数据
skb_clone()
是
否
是
否
skb_copy()
是
是
否
是
pskb_copy()
是
仅线性部分
是(frags)
仅线性部分
4.5 skb_linearize:处理分散/聚集 IO 1 2 3 4 5 6 7 8 9 static inline int skb_linearize (struct sk_buff *skb) { return skb_is_nonlinear(skb) ? __skb_linearize(skb) : 0 ; } static inline int __skb_linearize(struct sk_buff *skb){ return __pskb_pull_tail(skb, skb->data_len) ? 0 : -ENOMEM; }
当代码需要顺序访问报文所有字节,但 skb 是非线性的(data_len > 0,存在 page frags 或 frag_list)时,调用 skb_linearize() 将所有 page frags 的数据拷贝到线性区域,整合为线性 skb。
这是一个较重的操作 ,会触发内存分配和拷贝。协议栈中有大量 pskb_may_pull() 调用——它只确保 skb 的首部 位于线性区域(仅 pull 必要的字节),比完整的 skb_linearize() 代价小得多。
4.6 kfree_skb vs consume_skb:释放路径的语义区别 两者都会递减 skb->users 引用计数,归零后调用 __kfree_skb() 释放内存,但语义不同 ,这对 tracing 工具(如 drop_monitor、skb:kfree_skb tracepoint)至关重要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void kfree_skb_reason (struct sk_buff *skb, enum skb_drop_reason reason) { if (__kfree_skb_reason(skb, reason)) __kfree_skb(skb); } void consume_skb (struct sk_buff *skb) { if (!skb_unref(skb)) return ; trace_consume_skb(skb, __builtin_return_address(0 )); __kfree_skb(skb); }
释放链路(__kfree_skb() 路径):
1 2 3 4 5 6 7 8 9 10 11 12 13 __kfree_skb(skb) └─ skb_release_all(skb, reason, false) ├─ skb_release_head_state(skb) │ ├─ skb_dst_drop(skb) // 释放路由缓存引用 │ ├─ skb->destructor(skb) // 如 sock_wfree() 归还 socket 内存配额 │ ├─ nf_conntrack_put() // 释放连接跟踪引用 │ └─ skb_ext_put(skb) // 释放扩展数据 └─ skb_release_data(skb, reason, false) ├─ 递减 shinfo->dataref ├─ napi_frag_unref() // 释放 page frags(可能触发 page_pool 回收) ├─ kfree_skb_list(frag_list) └─ skb_free_head() // 释放数据缓冲区 └─ kfree_skbmem(skb) // 归还 sk_buff 到 slab 缓存
五、零拷贝相关机制 5.1 skb_frag_t 页片段机制 skb_frag_t 等价于 struct bio_vec,每个 frag 持有一个 struct page * 引用:
1 2 3 4 5 6 7 typedef struct bio_vec skb_frag_t ;static inline unsigned int skb_frag_size (const skb_frag_t *frag) { return frag->bv_len; }
驱动在接收时通常这样操作:
预先分配 page(通常通过 page_pool 管理),通过 DMA 映射给网卡
网卡将数据写入 page,触发中断/NAPI
驱动调用 skb_add_rx_frag() 将 page 引用填入 frags[]
1 2 3 4 5 6 7 8 void skb_add_rx_frag (struct sk_buff *skb, int i, struct page *page, int off, int size, unsigned int truesize) { skb_fill_page_desc(skb, i, page, off, size); skb->len += size; skb->data_len += size; skb->truesize += truesize; }
page_pool 是 6.x 内核引入的高性能页缓存机制,通过 skb->pp_recycle 标志和 skb_pp_recycle() 函数,在 skb 释放时直接将 page 归还给 page_pool 而非系统内存池,避免了 page allocator 的开销,实现”原地回收”。
5.2 sendfile/splice 的 skb 复用 sendfile() 和 splice() 系统调用走的是 SKBFL_SHARED_FRAG 路径:文件页缓存中的 page 直接被加入 skb 的 frags[],内核标记该 frag 为可能被外部修改:
1 2 3 4 5 6 SKBFL_SHARED_FRAG = BIT(1 ),
此时 skb 的数据直接来自 page cache,全程无需拷贝到内核网络缓冲区。但如果需要计算 TX checksum(ip_summed == CHECKSUM_PARTIAL 但硬件不支持),内核必须通过 skb_copy_ubufs() 先复制 frags 内容,再计算 checksum,这是 zero-copy sendfile 的一个隐含成本。
用户态零拷贝(MSG_ZEROCOPY,SKBFL_ZEROCOPY_ENABLE)则通过 ubuf_info 回调机制,在 DMA 传输完成后通知用户态释放缓冲区,实现用户态缓冲区到网卡的真正零拷贝。
六、关键结构体尺寸速查(x86-64,Linux 6.4)
结构体
大小
struct sk_buff
~232 字节
struct skb_shared_info
~320 字节(含 17 个 frags)
skb->cb[48]
48 字节
struct tcp_skb_cb
44 字节(含 IPv6 时)
struct inet_skb_parm
~20 字节
skb_frag_t (bio_vec)
12 字节
七、系列文章导航 本系列共计划 10 篇,系统覆盖 Linux 网络内核的核心子系统:
篇次
标题
核心内容
Part 1
总览架构与 sk_buff 生命周期 (本文)
协议栈全路径、sk_buff 内存模型、操作函数、零拷贝
Part 2
网卡驱动与 NAPI 机制
ndo_open/ndo_start_xmit、中断合并、NAPI poll、page_pool、XDP
Part 3
IP 层:路由、分片与转发
fib_lookup、路由缓存、ip_fragment、conntrack hook 点
Part 4
TCP 实现(一):连接管理与状态机
三次握手、TIME_WAIT、tcp_rcv_state_process、SYN cookies
Part 5
TCP 实现(二):拥塞控制与发送缓冲区
CUBIC/BBR、tcp_write_xmit、TSQ、Nagle 算法
Part 6
TCP 实现(三):接收与流量控制
tcp_rcv_established、OOO 队列、接收窗口、tcp_recvmsg
Part 7
UDP 与多播
udp_sendmsg、udp_rcv、GRO for UDP、多播路由
Part 8
Socket 层与 VFS 接口
sock_alloc、inet_stream_ops、poll/epoll 实现、零拷贝 recv
Part 9
Netfilter 与 iptables/nftables
hook 框架、连接跟踪(nf_conntrack)、NAT 实现原理
Part 10
性能调优与 eBPF 网络编程
RSS/RPS/RFS、SO_REUSEPORT、XDP、TC eBPF、sockmap
参考资料
Linux 6.4-rc1 源码:include/linux/skbuff.h、net/core/skbuff.c
Linux 内核文档:Documentation/networking/skbuff.rst(内核源码树内)
Jonathan Corbet 等:Linux Device Drivers, 3rd Edition ,Chapter 17
Jesper Dangaard Brouer:Network stack receive path
Thomas Graf:Kernel Networking Walkthrough (netdev 历届会议资料)