Linux内核分析之网络协议栈-00
This language version is unavailable; showing the other language.
44.1 网络协议栈分层与 sk_buff
sk_buff(skb)是网络栈的通用语:从驱动到 TCP,报文以 skb 传递,各层只做"加头/减头/改偏移",数据本体几乎不拷贝。这套设计的核心是三个决定:描述符与数据区分离、数据区内的四指针移动、协议头偏移的缓存。本节逐段解剖这个"网络栈的通用语",它是读一切网络代码的前提。
44.1.1 skb 布局:描述符与数据区分离
// include/linux/skbuff.h:885-1100(节选)
struct sk_buff {
union {
struct { /* These two members must be first
to match sk_buff_head. */
struct sk_buff *next; /* :889 */
struct sk_buff *prev; /* :890 队列节点 */
...
struct net_device *dev; /* :893 所属设备 */
};
...
};
...
ktime_t tstamp; /* 时间戳(硬件/软件) */
...
unsigned int len, /* :934 逻辑总长(含分片) */
data_len; /* 不在线性区的部分(frag 页) */
__u16 mac_len,
hdr_len;
...
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail; /* :1092 */
sk_buff_data_t end; /* :1093 */
unsigned char *head, /* :1094 */
*data; /* :1094 */
__u16 inner_transport_header; /* :1076 隧道内层 */
__u16 inner_network_header; /* :1077 */
__u16 transport_header; /* :1081 L4 偏移 */
__u16 network_header; /* :1082 L3 偏移 */
__u16 mac_header; /* :1083 L2 偏移 */
};
描述符(sk_buff 结构体)与数据区分离是第一个决定:结构体常驻专用 slab 缓存(skbuff_head_cache,__alloc_skb(),net/core/skbuff.c:672 分配),数据区独立成块——两者可分别操作(clone 只复制描述符、数据引用计数共享,见 44.1.2)。tail/end 用 32 位相对偏移而 head/data 用指针(sk_buff_data_t,skbuff.h:723 在 64 位上是 unsigned int)——省 16 字节/skb,百万 skb 的机器上就是 16MB。
44.1.2 数据区四指针:加剥头只动指针
skb 数据区布局 (44.1.1 节四指针的图形化):
head end
│ │
▼ ▼
├─────────┬──────────┬──────────────┬──────┤
│ headroom│ (保留空隙)│ 有效数据 │tailroom│
└─────────┴────▲─────┴──────▲───────┴──────┘
│ │
data tail
headroom: 给各层"加头"的空间 (收到时从 NIC 预留)
tailroom: skb_pad / 未来扩展空间
四个移动原语 (include/linux/skbuff.h):
__skb_push (:2811) data -= len; len += len ← 加头 (L4→L3→L2)
__skb_pull (:2822) data += len; len -= len ← 剥头 (L2→L3→L4)
__skb_put (:2755) tail += len; len += len ← 尾部填数据
__skb_trim tail -= len; len -= len ← 截尾
各层穿越的指针舞 (收包方向):
驱动: data 指向以太网帧头
mac_header = data - head (L2 起点)
eth_type_trans: data += 14 (pull) → 指向 IP 头
network_header = data - head (L3 起点)
ip_rcv 后: data += 20 (pull)
transport_header = data - head (L4 起点)
发方向完全对称: TCP 加头 = skb_push(tcp头);
IP 加头 = skb_push(ip头) — 数据一个字节不动!
三层偏移缓存的意义: tcp_hdr(skb)/ip_hdr(skb)/
eth_hdr(skb) 全是 (head + 偏移) 的换算宏 —
每层直接定位自己的头, 无解析扫描
44.1.3 len、data_len 与 truesize:三种"大小"
三个大小字段的语义分工:
skb->len 逻辑总长 = 线性区数据 + 全部 frags
skb->data_len 其中在 frag 页里的部分 (len - data_len
= 线性区大小) — 0 表示完全线性
skb->truesize 内核为此 skb 实际付出的内存
= sizeof(sk_buff) + 数据区总分配(含对齐)
— 45.2 节 socket 配额记账的口径!
paged frags (非线性区): 大包的页链挂在
skb_shared_info (skbuff.h:593):
struct skb_shared_info {
__u8 nr_frags; /* frag 页数 */
unsigned short gso_size; /* TSO/GRO 分段尺寸 */
unsigned short gso_segs; /* 分段数 */
struct sk_buff *frag_list; /* 子 skb 链 */
atomic_t dataref; /* clone 共享计数 */
...
};
位于数据区尾部 (end 之前) — 与数据同块内存,
零额外分配; GRO 合成的大包/零拷贝 sendfile 靠它
44.1.4 克隆族:从零拷贝到深拷贝
// net/core/skbuff.c:2098
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
// :672
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, ...)
复制谱系 (成本递增):
skb_clone 只复制描述符; 数据区共享
(shared_info->dataref++)
消费者: tcpdump 抓包 / TCP 重传队列 /
桥多口转发 / netlink 广播 (48.1)
skb_share_check clone 后检查共享态
skb_copy 描述符+数据全拷 (真复制)
pskb_expand_head 重开更大的线性区 (需要写数据
但 headroom 不足时 — 唯一"搬数据"
的常见路径, 慢!)
clone 的纪律: 共享数据期间"任何一方不得写数据区"
— TCP 重传队列持有 clone, 待 ACK 后才 kfree_skb
(dataref 归零才真释放) — 引用计数的又一次应用
(17/18 章同族)
44.1.5 分发:ptype 链与协议注册
// net/core/dev.c:172 与 :624
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
void dev_add_pack(struct packet_type *pt)
// net/core/dev.c:5965 区(收包分发点, 46.1 节的入口)
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, ...)
协议分发的骨架:
__netif_receive_skb_core (:5965):
[1] taps: ptype_all 上的抓包者先收到 (clone)
(tcpdump/PACKET socket — 45 章 packet 家族)
[2] tc ingress / rx_handler (桥/bond 接管,
44.2.3 节) — rx_handler 可改写 skb->dev
[3] deliver_ptype_list: 按 skb->protocol 查
ptype_base 哈希 (eth_type_trans 填的
ETH_P_IP/IPV6/ARP) → 调各协议的 func
→ ip_rcv (46.1) / ipv6_rcv / arp_rcv
协议注册 (启动期):
dev_add_pack(&ip_packet_type) L3 层注册
inet_add_protocol(&tcp_protocol, IPPROTO_TCP) L4 层注册
(每层有自己的 handler 链 — "分发链"而非
调用栈, 46 章各函数是这条链的节点)
软中断上下文(42.2 节)是全部收包代码的运行环境:NET_RX_SOFTIRQ 里跑 NAPI poll 与协议栈入口——这也是收包路径"不可睡眠"纪律(所有锁是自旋锁/RCU)的根源。
44.1.6 观测:skb 在哪可以看见
观测 skb 的窗口:
/proc/net/softnet_net 每 CPU: dropped/time_squeeze/
received_rps (42.2/44.3 节)
/proc/net/sockstat 系统级 skb/内存占用
dropwatch / bpftrace kfree_skb tracepoint +
丢弃点符号 — "包在哪被丢"的
标准排查工具
ethtool -S 驱动级 rx_dropped 计数
perf probe skb_consume_text ...
小结
skb 以"描述符/数据区分离 + 四指针移动(push/pull/put/trim 只动指针)+ 三层头偏移缓存"实现网络栈的零拷贝穿越:len/data_len/truesize 三种大小分别服务逻辑长度、frags 承载与内存记账;clone 族谱系(clone 共享数据 → copy 全拷)支撑抓包、重传、广播;ptype 分发链把"分层"实现为逐层剥头 + 按协议号查表路由。理解 skb 的布局与指针舞,是读 44-48 章全部网络代码的门票。下一节看报文两端的设备对象——net_device。
44.2 网络设备注册与 net_device
net_device 是网络栈中最大的结构体之一——但驱动真正要填的只有:设备私有数据、net_device_ops(操作表)、能力位(offload features)与收发回调。注册后它进入全局设备表,收发两端随之接通。本节解剖 net_device 的字段分区、注册流程、操作表与收发入口。
44.2.1 net_device:字段分区
// include/linux/netdevice.h:2109(结构巨大, 按功能分区摘录)
struct net_device {
char name[IFNAMSIZ]; /* "eth0" 的名字源 */
...
const struct net_device_ops *netdev_ops; /* 驱动操作表 */
...
unsigned int mtu; /* 1500 默认 */
unsigned short type; /* ARPHRD_ETHER */
unsigned char *dev_addr; /* MAC 地址 */
unsigned int num_tx_queues; /* 发送多队列 */
unsigned int real_num_tx_queues; /* 已使能数 */
...
rx_handler_func_t __rcu *rx_handler; /* 桥/bond 接管点 */
void __rcu *rx_handler_data;
...
netdev_features_t features; /* 当前 offload 能力 */
netdev_features_t hw_features; /* 可协商能力 */
...
struct kobject kobj; /* 39 章模型设备 */
...
struct netdev_queue *_tx; /* 发队列数组 */
unsigned int num_rx_queues; /* 收队列(配 napi) */
};
字段的分区逻辑与 18.2.3 节 struct zone 同思想——按访问热度与归属分组:驱动在 probe 时设置的身份字段(name/mtu/dev_addr)、core 维护的队列数组、每包读的能力位(features)。rx_handler 是"包进了设备后先给谁"的改写点——桥(br_handle_frame)、bonding、macvlan 全部实现为这个函数指针的替换。
44.2.2 注册流程:alloc → setup → register
// net/ethernet/eth.c:342 与 :376
void ether_setup(struct net_device *dev)
struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs,
unsigned int rxqs)
// net/core/dev.c:11563 与 :11341
int register_netdev(struct net_device *dev)
int register_netdevice(struct net_device *dev)
驱动注册的标准序列:
[1] dev = alloc_etherdev_mqs(sizeof_priv, txqs, rxqs)
一次分配: net_device + 私有区 + 队列数组
netdev_priv(dev) 取回私有区 (紧贴结构尾!)
ether_setup (:342) 预填以太网默认:
mtu=1500 / type=ARPHRD_ETHER / 广播地址 /
默认 netdev_ops=ethmac_ops 等
[2] 驱动覆盖:
dev->netdev_ops = &my_ops;
dev->ethtool_ops = &my_ethtool_ops;
dev->features |= NETIF_F_SG | NETIF_F_TSO ...;
netif_napi_add(dev, &qp->napi, my_poll); (44.3 节)
[3] register_netdev(dev)
└─ register_netdevice (:11341)
├─ 校验 (name 唯一/ops 齐全)
├─ 进全局索引: dev_name_hash/dev_index_hash
│ (ifindex 是容器网络的名字空间, 9 章)
├─ list_netdevice + 链入 bus 视图
└─ call_netdevice_notifiers(NETDEV_REGISTER)
→ uevent (39.1.3 节) → udev/ip link 可见
unregister_netdev 的对称拆除必须等"所有引用排空":在途 skb、NAPI 停轮询、等待 RCU 宽限期——与 22.3.5 节 munmap 的"先失效后释放"同纪律。
44.2.3 net_device_ops:驱动操作表
netdev_ops 的核心成员 (驱动必须/常实现的):
.ndo_open ifconfig up: 使能中断/分配收包环/napi_enable
.ndo_stop down: 对称释放
.ndo_start_xmit(skb, dev) 发送出口: skb → 硬件描述符
(返回 NETDEV_TX_OK/BUSY — 41.2 节同款语义)
.ndo_set_rx_mode 多播/混杂模式更新 (过滤编程进网卡)
.ndo_get_stats64 统计快照 (ip -s 的数据源)
.ndo_set_mac_address / .ndo_change_mtu
.ndo_validate_addr MAC 合法性
.ndo_setup_tc tc 卸载钩子 (47.2.3 节)
.ndo_set_features offload 协商落地
.ndo_bpf / .ndo_xdp_xmit XDP 挂载与转发 (44.3.3)
offload 能力位(dev->features)是现代网卡驱动的第二重心:
| 能力位 | 含义 | 栈与硬件的分工变化 |
|---|---|---|
| NETIF_F_SG | 分散聚合 | skb frags 直填 DMA(43.2 节 sg 映射) |
| NETIF_F_TSO | TCP 大段卸载 | 发侧分段下沉硬件(46.3 节 pacing 配合) |
| NETIF_F_GRO_HW | 收侧聚合卸载 | GRO 下沉(44.3.3 节) |
| NETIF_F_HW_CSUM | 校验和卸载 | 栈不算校验和 |
| NETIF_F_HW_L2FW_DOFFLOAD | L2 转发卸载 | 桥卸载 |
协议栈按协商后的能力位决定自己做还是交给硬件——skbuff.h 的 gso_size/gso_segs(44.1.3 节)就是 TSO 的参数通道。
44.2.4 收发两端的入口
// net/core/dev.c:4766(发送)与 :5718(非驱动收包)
int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
int __netif_rx(struct sk_buff *skb)
发方向 (46 章 TCP 的最终出口):
tcp_write_xmit → ip_queue_xmit → ...
→ dev_queue_xmit (:4766)
├─ 选发送队列: skb_get_tx_queue
│ (多队列: 按流哈希/ XPS CPU 映射)
├─ qdisc 入队或直接 xmit (noqueue, 47.2.3)
├─ validate_xmit_skb: 分段(TSO)/校验和/加密卸载
└─ ndo_start_xmit → 硬件描述符环 → 门铃
收方向 (硬件 → 协议栈):
NIC DMA → 中断 (42.1 只做通知) → NAPI poll (44.3)
→ napi_gro_receive → netif_receive_skb_internal (:6372)
→ __netif_receive_skb_core (44.1.5 节分发)
非驱动路径 (环回/隧道): __netif_rx → enqueue_to_backlog
(dev.c:5359, per-CPU backlog 队列 → process_backlog :6637)
观测面:/proc/net/dev(收发计数)、ip -s link(stats64)、ethtool -S(驱动/硬件计数器)、/proc/net/softnet_net(44.3 节)。ip link 的一切属性(mtu/mac/qlen)都直接映射 net_device 字段——用户态可感知的每个设备参数都有一个内核字段与之对应。
小结
net_device 的驱动视图极简(alloc_etherdev 填私有区 + netdev_ops 操作表 + 能力位 + register_netdev),复杂度隐藏在 core 的全局索引、多队列、rx_handler 接管与 offload 协商里;发送入口 dev_queue_xmit(qdisc→校验→xmit)与接收终点 netif_receive_skb(rx_handler→ptype 分发)是协议栈与驱动的两个焊接点。下一节看收包路径的性能生命线——NAPI。
44.3 NAPI —— 中断与轮询的折中
中断驱动的收包在高 PPS 下崩溃:每包一个中断 = 中断风暴(42.2 节的 ksoftirqd 打满、cache line 反复弹跳)。NAPI(New API)的答案是混合模式:首包中断把设备"登记"进轮询列表,随后关闭 RX 中断、在软中断里批量轮询收包,收干净了才重新开中断——中断次数从 O(包数) 降为 O(批次),且批处理天然利于缓存与 GRO 合成。本节解剖 napi_struct 的状态机、驱动侧收包循环与 NAPI 家族的新形态。
44.3.1 为什么"纯中断"撑不住
纯中断收包的恶性循环 (无 NAPI 的反事实):
高 PPS 场景 (10G 线速 ~14.8Mpps 小包):
每包一个中断 → 14.8M 中断/秒
→ 中断处理本身的 CPU 占用 > 包处理 CPU 占用
→ 硬中断打断了正要处理包的软中断 (42.2 节)
→ 活锁: CPU 全在应答中断, 一个包都收不完
中断 vs 轮询的真实权衡:
低负载: 中断优 (包稀疏, 轮询空转浪费)
高负载: 轮询优 (中断开销摊薄, 批量取缓存友好)
NAPI = 自适应切换: 负载轻→中断驱动;
负载重(队列非空)→持续轮询
44.3.2 napi_struct 与状态机
// include/linux/netdevice.h:381-410(节选)
struct napi_struct {
/* This field should be first or softnet_data.backlog needs tweaks. */
unsigned long state; /* NAPI_STATE_* 位 */
/*
* The poll_list must only be managed by the entity which
* changes the state of the NAPI_STATE_SCHED bit. This means
* whoever atomically sets that bit can add this napi_struct
* to the per-CPU poll_list, and whoever clears that bit
* can remove from the list right before clearing the bit.
*/
struct list_head poll_list; /* 挂 per-CPU poll_list */
int weight; /* 单次 poll 配额基数 */
u32 defer_hard_irqs_count;
int (*poll)(struct napi_struct *, int); /* 驱动轮询函数 */
...
/* CPU on which NAPI has been scheduled for processing */
int list_owner;
...
};
注释原文即协议:poll_list 的进出由 NAPI_STATE_SCHED 位的持有者独占管理——谁原子置位谁入队、谁清位谁出队。一次轮询周期因此天然单线程,无锁轮转。每个 RX 队列配一个 napi_struct(多队列网卡 = 多个独立 NAPI 实例,可分布到多 CPU)。
NAPI 状态机:
IDLE ──包到达──▶ SCHED (CAS 置 NAPI_STATE_SCHED
▲ │ + 挂入 per-CPU poll_list)
│ │ NET_RX_SOFTIRQ 被调度 (42.2 节)
│ ▼
│ POLLING (驱动 poll 执行, 最多 budget 个包)
│ │
│ ┌──────────┼────────────┐
│ │预算内收干净│还有包(budget 用尽│
│ ▼ ▼ │或队列仍非空)
│ 重新使能 napi_schedule │
│ 中断+清位 重排入队 (再来一轮)│
└──(IDLE)──────┘ │
└─▶ 仍 POLLING
// net/core/dev.c:6764
bool napi_complete_done(struct napi_struct *n, int work_done)
// :5359 与 :6637(backlog 路径——非 NAPI 驱动的兼容层)
static int enqueue_to_backlog(struct sk_buff *skb, int cpu, ...)
static int process_backlog(struct napi_struct *napi, int quota)
process_backlog(:6637)揭示 NAPI 的完备性:连"老式 netif_rx"路径也被实现了成 NAPI——per-CPU 的 softnet_data.backlog 队列配一个内建 napi(netdevice.h:3516 的 softnet_data,含 poll_list/output_queue/process_queue/time_squeeze 字段)。整个收包子系统只有一种执行模型。
44.3.3 驱动侧的收包循环
NAPI 驱动的标准写法 (虚代码, 对应真实 NIC 驱动):
/* probe/open 时 */
netif_napi_add(dev, &qp->napi, my_poll, NAPI_POLL_WEIGHT=64);
napi_enable(&qp->napi);
/* 硬中断 (42.1 节: 只做这些!) */
my_isr(irq, dev_id)
{
if (!napi_schedule_prep(&qp->napi))
return IRQ_NONE; /* 已在轮询中 */
my_disable_rx_irq(qp); /* 关中断 */
__napi_schedule(&qp->napi); /* 入 poll_list
+ raise NET_RX */
return IRQ_HANDLED;
}
/* 软中断里被调 (NET_RX_SOFTIRQ) */
int my_poll(struct napi_struct *napi, int budget)
{
int work = 0;
while (work < budget && 环里有已 DMA 的包) {
skb = 构造 skb (复用环描述符页, 43.2 节流式映射);
napi_gro_receive(napi, skb); /* GRO 合成入口 */
work++;
}
if (work < budget) { /* 预算内收干净 */
napi_complete_done(napi, work); /* 清位+重开中断
(:6764, 含中断
聚合 deferral) */
return work;
}
return budget; /* 没收完: 再来一轮 */
}
budget 与 weight 的旋钮语义:budget 限制单次软中断的包数(42.2 节风暴防护在 NAPI 的落实——time_squeeze 计数"没收完被挤出"的次数);weight 是补货节奏;napi_complete_done 的重开中断前还有 defer_hard_irqs_count(中断聚合/ethtool coalescing:硬件再延迟数微秒聚批,把中断次数进一步压到 O(毫秒))。
44.3.4 GRO:轮询点上的合成
GRO (Generic Receive Offload):
poll 里收到的每个小包经 napi_gro_receive:
同流 (同四元组) + 序号衔接 + 标志兼容
→ 合成一个巨型 skb (44.1.3 节 frags 装多个包)
→ 交给协议栈: tcp_v4_rcv 一次处理 N 个 MSS
收益: 上层每包成本 ÷ N (校验和/拷贝/锁)
对偶: 发送侧 TSO (46.3 节) — 大包给硬件分段
GRO_HW: 合成也下沉硬件 (features 协商, 44.2.3)
前提纪律: 同一批合成包必须"同去同往" —
转发场景需要正确分段还原 (GRO 转发的经典 bug 源)
44.3.5 新形态谱系与观测
轮询思想的极致化:
busy polling (SO_BUSY_POLL + sysctl):
进程在 recvmsg 里自己轮询驱动环 —
绕开软中断/唤醒, 延迟到微秒级 (45 章低延迟语境)
XDP (eXpress Data Path):
驱动 poll 里最先执行 BPF 程序 — 包还没构造
skb 就可 DROP/FORWARD/重定向 (DDoS 前置防线)
AF_XDP: XDP 重定向到用户态 UMEM 环 —
零协议栈零拷贝 (DPDK 思想的内核原生版)
io_uring zero-copy recv (37 章家族延伸)
观测:
/proc/net/softnet_net: 每 CPU 的
processed / dropped / time_squeeze / received_rps
(softnet_data 字段, netdevice.h:3516)
ethtool -S: rx_packets/rx_dropped/中断计数
ethtool -c: 中断聚合参数
小结
NAPI 以"SCHED 位独占 poll_list 管理 + 首包中断后关中断批量轮询 + 预算内收干净才重开中断"的混合模式把收包中断次数降为 O(批次),budget/weight/中断聚合提供延迟-吞吐旋钮;process_backlog 证明连老式收包路径也被统一进 NAPI 模型,GRO 在轮询点把小包合成大包与发送侧 TSO 对偶,XDP/AF_XDP/busy-poll 是轮询思想的极致化谱系。高 PPS 下的一切收包设计都是"中断与轮询折中"这句话的变奏。网络架构章至此完成;下一章进入 socket 层——进程与网络栈的接口。