Linux 网络内核协议栈深度剖析(九):网络性能优化与高性能技术

在前几篇的基础上,本文聚焦于 Linux 网络栈中性能极限的挖掘:从硬件卸载、多队列扩展,到内核旁路(Kernel Bypass)技术,再到发送路径的零拷贝优化,最后落地到实际的诊断工具链。所有代码片段均取自 Linux 6.4-rc1 源码树,并附有源文件路径与行号,供读者对照阅读。


一、硬件卸载特性(Hardware Offload)

1.1 卸载特性的表示层:netdev_features_t

Linux 用一个 64 位整型 netdev_features_t 描述网卡能力,定义在 include/linux/netdev_features.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
/* include/linux/netdev_features.h */
typedef u64 netdev_features_t;

enum {
NETIF_F_SG_BIT, /* Scatter/gather IO. */
NETIF_F_IP_CSUM_BIT, /* Can checksum TCP/UDP over IPv4. */
NETIF_F_HW_CSUM_BIT, /* Can checksum all the packets. */
NETIF_F_IPV6_CSUM_BIT, /* Can checksum TCP/UDP over IPV6 */
NETIF_F_GSO_BIT, /* Enable software GSO. */
NETIF_F_GRO_BIT, /* Generic receive offload */
NETIF_F_LRO_BIT, /* large receive offload */
NETIF_F_TSO_BIT, /* ... TCPv4 segmentation */
NETIF_F_TSO6_BIT, /* ... TCPv6 segmentation */
NETIF_F_GSO_UDP_L4_BIT, /* ... UDP payload GSO (not UFO) */
NETIF_F_RXHASH_BIT, /* Receive hashing offload */
NETIF_F_RXCSUM_BIT, /* Receive checksumming offload */
/* ... */
};

#define NETIF_F_TSO __NETIF_F(TSO)
#define NETIF_F_GRO __NETIF_F(GRO)
#define NETIF_F_GSO __NETIF_F(GSO)
#define NETIF_F_IP_CSUM __NETIF_F(IP_CSUM)
#define NETIF_F_HW_CSUM __NETIF_F(HW_CSUM)

每一位对应一项卸载能力。驱动在 probe 阶段通过 dev->features |= NETIF_F_TSO | NETIF_F_GRO | ... 声明支持的特性,内核在发送/接收路径中检查这些标志,决定由硬件还是软件完成分段、校验和等工作。

1.2 TSO:TCP 分段卸载(Transmit Side)

TSO(TCP Segmentation Offload) 让内核把一个超大 skb(最大可达 64 KB,受 gso_size 字段控制)直接交给网卡,由网卡硬件完成 TCP/IP 分段,而不是内核在 validate_xmit_skb 阶段用 GSO 软件分段。

工作流程:

  1. TCP 发送路径(tcp_sendmsg)构建一个大 skb,skb_shinfo(skb)->gso_size 设置为 MSS,gso_segs 设置为段数。
  2. __dev_queue_xmitvalidate_xmit_skb 检查 netif_needs_gso(skb, features):若网卡声明了 NETIF_F_TSO,且协议匹配,则直接透传给驱动,不做 CPU 分段
  3. 网卡 DMA 引擎读取整块数据,自行按 MSS 切割,填写各段的 TCP 序号、IP 长度和校验和,最终发送多个帧。

收益:每次系统调用可减少 CPU 对 IP/TCP 头的重复处理。一个 64 KB 的 GSO skb 可以省去约 45 次独立的 IP 头写入和校验和计算。

1.3 GSO:通用分段卸载(软件兜底)

当网卡不支持 TSO,或 skb 通过隧道(如 VXLAN)封装导致硬件无法识别时,内核在 CPU 上完成分段——这就是 GSO(Generic Segmentation Offload)。实现入口在 net/core/dev.c

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
/* net/core/dev.c,第 3352 行 */
/**
* __skb_gso_segment - Perform segmentation on skb.
* @skb: buffer to segment
* @features: features for the output path (see dev->features)
* @tx_path: whether it is called in TX path
*
* This function segments the given skb and returns a list of segments.
*/
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
netdev_features_t features, bool tx_path)
{
struct sk_buff *segs;

if (unlikely(skb_needs_check(skb, tx_path))) {
int err;
err = skb_cow_head(skb, 0);
if (err < 0)
return ERR_PTR(err);
}

if (features & NETIF_F_GSO_PARTIAL) {
netdev_features_t partial_features = NETIF_F_GSO_ROBUST;
struct net_device *dev = skb->dev;
partial_features |= dev->features & dev->gso_partial_features;
if (!skb_gso_ok(skb, features | partial_features))
features &= ~NETIF_F_GSO_PARTIAL;
}

SKB_GSO_CB(skb)->mac_offset = skb_headroom(skb);
SKB_GSO_CB(skb)->encap_level = 0;
skb_reset_mac_header(skb);
skb_reset_mac_len(skb);

segs = skb_mac_gso_segment(skb, features);
/* ... */
return segs;
}
EXPORT_SYMBOL(__skb_gso_segment);

validate_xmit_skb(同文件第 3643 行)在检测到 netif_needs_gso 时调用 skb_gso_segment,将大 skb 切割为链表,再逐一送入驱动 ndo_start_xmit

GSO 与 TSO 的关键区别:TSO 由网卡硬件分段(零 CPU 开销),GSO 由内核 CPU 分段(有 CPU 开销,但仍比每次 sendmsg 单独发送效率高,因为协议栈的路由查找、防火墙规则等只需执行一次)。

1.4 GRO:通用接收卸载

GRO(Generic Receive Offload) 是接收方向的对称技术,在 NAPI 轮询期间将属于同一 TCP 流的多个小 skb 合并成一个大 skb,再交给上层协议栈,减少协议栈的处理次数。

核心入口在 net/core/gro.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* net/core/gro.c,第 648 行 */
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
gro_result_t ret;

skb_mark_napi_id(skb, napi);
trace_napi_gro_receive_entry(skb);

skb_gro_reset_offset(skb, 0);

ret = napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
trace_napi_gro_receive_exit(ret);

return ret;
}
EXPORT_SYMBOL(napi_gro_receive);

驱动在 NAPI 回调中调用 napi_gro_receive,内核进入 dev_gro_receive(同文件第 482 行):

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
/* net/core/gro.c,第 482 行(精简) */
static enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
u32 bucket = skb_get_hash_raw(skb) & (GRO_HASH_BUCKETS - 1);
struct gro_list *gro_list = &napi->gro_hash[bucket];
/* ... */
/* 设置 GRO CB,记录校验和状态 */
switch (skb->ip_summed) {
case CHECKSUM_COMPLETE:
NAPI_GRO_CB(skb)->csum = skb->csum;
NAPI_GRO_CB(skb)->csum_valid = 1;
break;
case CHECKSUM_UNNECESSARY:
NAPI_GRO_CB(skb)->csum_cnt = skb->csum_level + 1;
break;
}

/* 调用协议特定的 gro_receive(如 inet_gro_receive)*/
pp = INDIRECT_CALL_INET(ptype->callbacks.gro_receive,
ipv6_gro_receive, inet_gro_receive,
&gro_list->list, skb);
/* ... */
same_flow = NAPI_GRO_CB(skb)->same_flow;
ret = NAPI_GRO_CB(skb)->free ? GRO_MERGED_FREE : GRO_MERGED;
/* ... */
}
  • 哈希桶:每个 NAPI 实例维护 GRO_HASH_BUCKETS(默认 8)个哈希桶,按 skb 的流哈希分桶,避免跨流错误合并。
  • MAX_GRO_SKBS = 8:每个哈希桶最多缓存 8 个待合并 skb,超出后强制 flush。
  • 合并结果:GRO_MERGED 表示成功合并、GRO_MERGED_FREE 表示合并后原 skb 被释放、GRO_NORMAL 表示未合并直接上送。

1.5 Checksum Offload

ip_summed 字段(位于 struct sk_buff)描述当前 skb 的校验和状态:

含义
CHECKSUM_NONE 校验和未经验证,软件需完整计算
CHECKSUM_UNNECESSARY 网卡已验证(NETIF_F_RXCSUM),软件无需重算
CHECKSUM_COMPLETE 网卡提供了完整的硬件校验和(skb->csum),软件可用于增量验证
CHECKSUM_PARTIAL 发送方向:软件已填写伪头校验和,网卡负责补全(NETIF_F_IP_CSUM / NETIF_F_HW_CSUM

NETIF_F_IP_CSUM 仅卸载 IPv4 上的 TCP/UDP,NETIF_F_HW_CSUM 可卸载所有协议。

1.6 LRO vs GRO

LRO(Large Receive Offload) 由网卡硬件合并数据包(对应 NETIF_F_LRO),粒度更粗、效率更高,但存在两个主要缺陷:

  1. 转发场景破坏:LRO 合并后的超大包无法直接转发(转发需要原始 MSS 大小的帧)。
  2. IP 头丢失:合并后中间包的 IP 选项、TTL 等信息丢失,导致防火墙/路由规则失效。

GRO 在软件层面实现,保留了完整的协议层控制。内核中 LRO 和 GRO 不能同时开启:若驱动声明 NETIF_F_LRO,内核会在注册时自动清除 NETIF_F_GRO。现代生产系统几乎一律使用 GRO。


二、RSS 与多队列接收

2.1 RSS 原理

RSS(Receive Side Scaling) 是网卡硬件特性:网卡通过对数据包的五元组(源/目 IP、源/目端口、协议)做哈希,将不同流分配到不同的 RX 队列,每个队列绑定一个 CPU 核心,从而实现多核并行收包。

驱动在初始化时通过以下函数告知内核实际使用的队列数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* net/core/dev.c,第 2865 行 */
int netif_set_real_num_tx_queues(struct net_device *dev, unsigned int txq)
{
/* ... 更新 kobject、TC 配置、qdisc ... */
dev->real_num_tx_queues = txq;
/* ... */
}
EXPORT_SYMBOL(netif_set_real_num_tx_queues);

/* net/core/dev.c,第 2917 行 */
int netif_set_real_num_rx_queues(struct net_device *dev, unsigned int rxq)
{
/* ... */
dev->real_num_rx_queues = rxq;
return 0;
}
EXPORT_SYMBOL(netif_set_real_num_rx_queues);

每个 RX 队列独立触发 MSIX 中断,绑定到专属 CPU 核心,彻底消除单 CPU 的收包瓶颈。

2.2 RPS:软件版 RSS

当网卡不支持硬件 RSS 时,RPS(Receive Packet Steering) 在软件层面实现类似效果。核心函数 get_rps_cpunet/core/dev.c 第 4428 行)负责根据流哈希选择目标 CPU:

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
/* net/core/dev.c,第 4427 行 */
/*
* get_rps_cpu is called from netif_receive_skb and returns the target
* CPU from the RPS map of the receiving queue for a given skb.
* rcu_read_lock must be held on entry.
*/
static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
struct rps_dev_flow **rflowp)
{
const struct rps_sock_flow_table *sock_flow_table;
struct netdev_rx_queue *rxqueue = dev->_rx;
struct rps_dev_flow_table *flow_table;
struct rps_map *map;
int cpu = -1;
u32 hash;

/* 读取 per-queue 的 rps_map(用户通过
* /sys/class/net/eth0/queues/rx-0/rps_cpus 设置)*/
flow_table = rcu_dereference(rxqueue->rps_flow_table);
map = rcu_dereference(rxqueue->rps_map);
if (!flow_table && !map)
goto done;

hash = skb_get_hash(skb);
if (!hash)
goto done;

/* 先查 RFS 全局流表,再退回 RPS CPU 位图 */
sock_flow_table = rcu_dereference(rps_sock_flow_table);
if (flow_table && sock_flow_table) {
ident = sock_flow_table->ents[hash & sock_flow_table->mask];
if ((ident ^ hash) & ~rps_cpu_mask)
goto try_rps;
/* 匹配到 RFS 记录,使用应用所在 CPU */
next_cpu = ident & rps_cpu_mask;
/* ... */
}
/* ... */
}

匹配到目标 CPU 后,enqueue_to_backlog 将 skb 放入目标 CPU 的 input_pkt_queue,通过 IPI(Inter-Processor Interrupt)触发软中断处理。

配置 RPS

1
2
# 将 rx-0 队列的包分发到 CPU 0-3(位图 0xf)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

2.3 RFS:接收流引导

RFS(Receive Flow Steering) 在 RPS 基础上更进一步:将数据包引导到应用正在运行的 CPU,利用 CPU L1/L2 缓存热度,避免跨核传递 socket 数据导致的缓存 miss。

记录应用 CPU 位置的函数定义在 include/linux/netdevice.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* include/linux/netdevice.h,第 761 行 */
static inline void rps_record_sock_flow(struct rps_sock_flow_table *table,
u32 hash)
{
if (table && hash) {
unsigned int index = hash & table->mask;
u32 val = hash & ~rps_cpu_mask;

/* We only give a hint, preemption can change CPU under us */
val |= raw_smp_processor_id();

if (table->ents[index] != val)
table->ents[index] = val;
}
}

每当 socket 在某 CPU 上被 recvmsg 系统调用访问时(include/net/sock.h 第 1134 行调用此函数),就将 当前CPU | hash 写入全局流表 rps_sock_flow_tableget_rps_cpu 中优先查此表,找到后数据包走 IPI 被送往应用的 CPU。

配置 RFS

1
2
3
4
# 全局流表大小(必须是 2 的幂)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
# 每个队列的流表大小
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

2.4 XPS:发送包引导

XPS(Transmit Packet Steering) 是发送方向的对称技术:将 socket 发出的数据包绑定到特定 TX 队列,通常配置为与 socket 所在 CPU 对应的队列,减少跨核竞争 txq->lock

1
2
# 将 TX 队列 0 绑定到 CPU 0
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus

三、Kernel Bypass 技术

当数据包在内核网络栈中经历的层次(驱动→协议栈→socket→用户态)成为瓶颈时,内核旁路技术绕过部分或全部内核路径,追求百万级 PPS 乃至线速转发。

3.1 DPDK:完全用户态驱动

DPDK(Data Plane Development Kit) 通过以下机制绕过内核:

  • UIO / VFIO:将网卡的 BAR(Base Address Register,PCIe 配置空间)映射到用户态进程地址空间,用户态代码直接读写网卡寄存器和 DMA 描述符环。
  • Poll Mode Driver(PMD):用户态轮询(busy-poll)收包,完全绕过中断和内核协议栈。
  • Hugepage 内存:使用 2 MB/1 GB 大页减少 TLB miss,DMA 描述符直接指向用户态物理内存。

DPDK 的收包路径:网卡 DMA → 用户态内存 → PMD 驱动轮询 → 用户态应用,全程无系统调用、无内核上下文切换、无 skb 分配开销。代价是独占 CPU 核心(一个核心持续 100% 用于轮询),适合专用转发/处理节点。

3.2 AF_XDP:内核 eBPF 加持的零拷贝接收

AF_XDP 是 Linux 4.18 引入的 socket 类型,结合 XDP eBPF 程序,实现在内核 XDP 钩子处将数据包直接映射到用户态内存(UMEM),无需经过完整协议栈。

核心数据结构定义在 include/net/xdp_sock.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
26
/* include/net/xdp_sock.h,第 45 行 */
struct xdp_sock {
/* struct sock must be the first member of struct xdp_sock */
struct sock sk;
struct xsk_queue *rx ____cacheline_aligned_in_smp;
struct net_device *dev;
struct xdp_umem *umem; /* 用户态共享内存区域 */
struct list_head flush_node;
struct xsk_buff_pool *pool;
u16 queue_id;
bool zc; /* zero-copy 标志 */
enum {
XSK_READY = 0,
XSK_BOUND,
XSK_UNBOUND,
} state;

struct xsk_queue *tx ____cacheline_aligned_in_smp;
struct list_head tx_list;
spinlock_t rx_lock;

/* Statistics */
u64 rx_dropped;
u64 rx_queue_full;
/* ... */
};

收包路径(net/xdp/xsk.c 第 251 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* net/xdp/xsk.c,第 251 行 */
static int xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp)
{
int err;
u32 len;

err = xsk_rcv_check(xs, xdp);
if (err)
return err;

/* 零拷贝路径:xdp_buff 来自 XSK buff pool(用户态 UMEM) */
if (xdp->rxq->mem.type == MEM_TYPE_XSK_BUFF_POOL) {
len = xdp->data_end - xdp->data;
return __xsk_rcv_zc(xs, xdp, len); /* 直接写 RX ring,无内存拷贝 */
}

/* 非零拷贝路径:分配一块 UMEM,memcpy 数据 */
err = __xsk_rcv(xs, xdp);
if (!err)
xdp_return_buff(xdp);
return err;
}

零拷贝的关键在 __xsk_rcv_zc(第 138 行):它将 xdp_buff 的 UMEM 地址(xp_get_handle)直接写入用户态可见的 RX ring 描述符,用户进程通过 poll() 系统调用感知到可读后,直接从 UMEM 读取数据——全程无数据拷贝

3.3 XDP vs DPDK vs 传统内核网络对比

维度 传统内核网络 XDP (AF_XDP) DPDK
数据路径 完整协议栈 XDP 钩子后直达用户态 完全用户态
拷贝次数 ≥1(skb 分配+DMA) 0(零拷贝模式) 0
CPU 开销 中断+软中断+上下文切换 软中断+eBPF JIT 纯轮询,无切换
内核旁路程度 部分(协议栈旁路) 完全
可维护性 优秀 良好(标准 socket API) 较差(需专用框架)
适用场景 通用 高性能收包+eBPF 过滤 专用转发/NFV
PPS 上限(参考) ~5 Mpps/核 ~20 Mpps/核 ~40 Mpps/核

四、发送路径优化

4.1 __dev_queue_xmit 中的 qdisc bypass

发送路径的核心函数是 __dev_queue_xmitnet/core/dev.c 第 4150 行):

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
/* net/core/dev.c,第 4150 行(精简) */
int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
struct net_device *dev = skb->dev;
struct netdev_queue *txq = NULL;
struct Qdisc *q;
/* ... egress hook(TC、netfilter)处理 ... */

txq = netdev_core_pick_tx(dev, skb, sb_dev);
q = rcu_dereference_bh(txq->qdisc);

if (q->enqueue) {
/* 正常路径:入队 qdisc(如 pfifo_fast、fq_codel)*/
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}

/* qdisc bypass:设备无队列(loopback、隧道等) */
if (dev->flags & IFF_UP) {
int cpu = smp_processor_id();
if (READ_ONCE(txq->xmit_lock_owner) != cpu) {
skb = validate_xmit_skb(skb, dev, &again);
if (!skb)
goto out;
HARD_TX_LOCK(dev, txq, cpu);
if (!netif_xmit_stopped(txq)) {
skb = dev_hard_start_xmit(skb, dev, txq, &rc);
}
HARD_TX_UNLOCK(dev, txq);
}
}
/* ... */
}

validate_xmit_skb(第 3643 行)在实际发送前完成所有预处理:VLAN 插入、skb 合规性检查、GSO 分段、checksum 补全。若网卡宣告了 TSO/checksum offload,这些工作会被跳过,直接透传给硬件。

对于启用了 noqueue qdisc 或 MQ qdisc 的高性能场景,HARD_TX_LOCK 是 per-queue 锁,不同队列间完全无竞争,实现了真正的多核并行发送。

4.2 MSG_ZEROCOPY:零拷贝发送

Linux 4.14 引入 MSG_ZEROCOPY 标志,允许内核在 DMA 完成前将用户态内存页钉住(pin),避免 sendmsg 时的内核空间拷贝。核心分配函数在 net/core/skbuff.c

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
/* net/core/skbuff.c,第 1501 行 */
static struct ubuf_info *msg_zerocopy_alloc(struct sock *sk, size_t size)
{
struct ubuf_info_msgzc *uarg;
struct sk_buff *skb;

skb = sock_omalloc(sk, 0, GFP_KERNEL);
if (!skb)
return NULL;

uarg = (void *)skb->cb;

/* 统计 pinned pages,防止 OOM */
if (mm_account_pinned_pages(&uarg->mmp, size)) {
kfree_skb(skb);
return NULL;
}

uarg->ubuf.callback = msg_zerocopy_callback;
uarg->id = ((u32)atomic_inc_return(&sk->sk_zckey)) - 1;
uarg->len = 1;
uarg->bytelen = size;
uarg->zerocopy = 1;
uarg->ubuf.flags = SKBFL_ZEROCOPY_FRAG | SKBFL_DONT_ORPHAN;
refcount_set(&uarg->ubuf.refcnt, 1);
sock_hold(sk);

return &uarg->ubuf;
}

用户态需要通过 MSG_ERRQUEUE 接收完成通知(SO_EE_ORIGIN_ZEROCOPY),确认内核已完成 DMA 并释放页面,才能复用该内存。适合大块数据(>1 KB)的持续发送场景,小包反而因通知开销变慢。

使用示例:

1
2
3
4
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &val, sizeof(val));
send(fd, buf, len, MSG_ZEROCOPY);
/* 轮询 MSG_ERRQUEUE 等待 DMA 完成通知 */

4.3 sendfile 与 splice:内核内零拷贝

sendfile(2) 通过 tcp_sendpagenet/ipv4/tcp.c 第 1147 行)将文件 page cache 中的 page 直接附加到 skb 的 frag 列表,避免内核→用户→内核的数据往返拷贝。配合 NETIF_F_SG(scatter-gather DMA),网卡可直接从 page cache DMA 读取数据发送。


五、内存分配优化

5.1 skb 分配缓存池

sk_buff 是网络栈中分配最频繁的数据结构。内核为其建立了专用 slab 缓存(net/core/skbuff.c 第 4757 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* net/core/skbuff.c,第 4757 行 */
void __init skb_init(void)
{
skbuff_cache = kmem_cache_create_usercopy("skbuff_head_cache",
sizeof(struct sk_buff),
0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC,
offsetof(struct sk_buff, cb),
sizeof_field(struct sk_buff, cb),
NULL);
skbuff_fclone_cache = kmem_cache_create("skbuff_fclone_cache",
sizeof(struct sk_buff_fclones),
0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC,
NULL);
/* skbuff_small_head_cache:小头部专用缓存,减少 kmalloc 碎片 */
skb_small_head_cache = kmem_cache_create_usercopy("skbuff_small_head", ...);
}
  • **skbuff_head_cache**:分配 struct sk_buff 本身(控制块),SLAB_HWCACHE_ALIGN 确保 CPU cache line 对齐。
  • **skbuff_fclone_cache**:分配 struct sk_buff_fclones,包含原始 skb 和一个 clone,用于需要保留原始 skb 的场景(如 TCP 重传)。

usercopy 指定了允许 copy_to/from_user 的字段范围(cb 字段),是 HARDENED_USERCOPY 安全特性的一部分。

5.2 NUMA 感知的网络内存分配

在多路(multi-socket)NUMA 服务器上,内存访问延迟与 CPU 所在 NUMA 节点强相关。现代网卡驱动(如 Intel i40e、mlx5)会调用 dev_alloc_pages / __alloc_pages_node 在与网卡直连的 NUMA 节点分配 RX ring 内存,避免跨节点 DMA 访问。

查看网卡所在 NUMA 节点:

1
cat /sys/class/net/eth0/device/numa_node

将网络处理进程绑定到同一 NUMA 节点:

1
numactl --cpunodebind=0 --membind=0 ./my_server

IRQ 亲和性绑定(推荐手动,irqbalance 在 NUMA 拓扑复杂时决策不够精准):

1
2
3
4
# 查看网卡队列对应的 IRQ 编号
grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'
# 绑定 IRQ 1234 到 CPU 0
echo 1 > /proc/irq/1234/smp_affinity

六、TCP 性能调优参数

6.1 连接积压队列

1
2
3
4
5
6
# SYN 半连接队列上限(内核处理 SYN 包后进入 SYN_RECV 状态的连接数)
sysctl -w net.ipv4.tcp_max_syn_backlog=65536

# accept 全连接队列上限(三次握手完成后等待 accept() 的连接数)
# 同时需在 listen(fd, backlog) 中传入足够大的 backlog 值
sysctl -w net.core.somaxconn=65536

net.ipv4.tcp_max_syn_backlog 防止 SYN flood 打满半连接队列;somaxconn 限制 accept 队列,两者均需调大以支撑高并发。

6.2 软中断收包队列

1
2
# 每个 CPU 的软中断收包队列长度上限(单帧入队失败时丢弃)
sysctl -w net.core.netdev_max_backlog=250000

当 NIC 驱动向 input_pkt_queue 入队速率超过软中断消费速率时,超出部分被丢弃并计入 netdev_rx_dropped

6.3 TCP 缓冲区

1
2
3
4
5
6
7
8
# tcp_rmem: min / default / max(单位 bytes)
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"

# tcp_wmem: min / default / max
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"

# tcp_mem: min / pressure / max(单位 pages)
sysctl -w net.ipv4.tcp_mem="94500000 915000000 927000000"

tcp_rmem 的 max 值是单个 socket 接收缓冲区的上限;tcp_mem 的 max 值是所有 TCP socket 的内存总量上限(超出则进入内存压力模式,限制新连接)。现代服务器(64 GB 内存)通常将 max 调至 256 MB,配合 net.ipv4.tcp_window_scaling=1 实现高带宽延迟积(BDP)下的满速传输。

6.4 TCP Fast Open

TFO(TCP Fast Open) 允许在 SYN 包中携带数据,省去一个 RTT:

1
2
# 0=关闭, 1=客户端, 2=服务端, 3=客户端+服务端
sysctl -w net.ipv4.tcp_fastopen=3

TFO 通过一个 Cookie 机制防重放:首次建连时服务端在 SYN-ACK 中下发 Cookie,客户端后续可将 Cookie 附在 SYN+DATA 中,服务端验证通过后立即将数据递交给应用,无需等待 ACK。

6.5 内核忙轮询

1
2
3
4
# 软中断收包后,保持轮询网卡的时间(微秒)
# 适合延迟敏感场景(如高频交易),代价是 CPU 利用率上升
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50

busy_poll 开启后,poll()/select() 系统调用在返回前会先进行内核忙轮询(napi_busy_loop),避免中断延迟,可将 P99 延迟从数十微秒降至个位数微秒。


七、性能诊断全流程

7.1 查看与修改卸载特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前卸载特性状态
ethtool -k eth0

# 典型输出片段:
# tcp-segmentation-offload: on
# generic-segmentation-offload: on
# generic-receive-offload: on
# large-receive-offload: off [fixed]
# rx-checksumming: on

# 关闭 TSO(调试用,生产环境不建议)
ethtool -K eth0 tso off
# 开启 GRO
ethtool -K eth0 gro on

7.2 调整队列数

1
2
3
4
5
6
7
8
9
10
# 查看当前队列配置(combined = RX+TX 共享队列)
ethtool -l eth0
# 输出:
# Pre-set maximums:
# Combined: 64
# Current hardware settings:
# Combined: 4

# 将队列数设置为 8(建议与物理 CPU 核心数对齐)
ethtool -L eth0 combined 8

调整后需同步更新 IRQ 亲和性,将每个队列的中断绑定到对应的 CPU 核心。

7.3 IRQ 亲和性绑定

1
2
3
4
5
6
7
8
9
# 方法一:使用 irqbalance(自动,但不够精细)
systemctl start irqbalance

# 方法二:手动绑定(推荐高性能场景)
# 将 eth0 的 8 个队列 IRQ 依次绑定到 CPU 0-7
for i in $(seq 0 7); do
IRQ=$(grep "eth0-$i" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
echo $((1 << i)) > /proc/irq/$IRQ/smp_affinity
done

7.4 实时网络监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 实时查看各网卡的收发带宽(每秒刷新)
sar -n DEV 1

# 查看 qdisc 统计,重点关注 dropped 字段
tc -s qdisc show dev eth0
# 输出示例:
# qdisc mq 0: root
# qdisc fq_codel 0: parent :1 limit 10240p flows 1024 ...
# Sent 1234567 bytes 8901 pkt (dropped 0, overlimits 0 requeues 3)

# TCP 全局统计计数器(关注 RetransSegs、TCPLostRetransmit)
nstat -az | grep -E "TcpRetrans|TCPLost|TcpDropped|TcpExtTCPOFOQueue"

# 协议栈详细计数
netstat -s | grep -E "retransmit|failed|reset|overflow"

7.5 关键计数器解读

计数器 含义 告警阈值
TcpExtTCPBacklogDrop accept 队列满导致丢包 > 0 应扩大 somaxconn
TcpExtTCPReqQFullDrop SYN 队列满 > 0 应扩大 tcp_max_syn_backlog
TcpRetransSegs TCP 重传段数 持续上升说明网络质量差
TcpExtTCPOFOQueue 乱序队列中的包数 持续高位说明网络抖动
netdev_rx_dropped NIC 驱动层丢包 > 0 应扩大 ring buffer 或队列
1
2
3
# 查看网卡 ring buffer 大小及丢包
ethtool -g eth0
ethtool -S eth0 | grep -i drop

7.6 bpftrace 追踪 softirq 处理延迟

1
2
3
4
5
6
7
8
9
10
11
12
13
# 追踪 NET_RX softirq 的处理延迟(单位:纳秒)
bpftrace -e '
tracepoint:irq:softirq_entry /args->vec == 3/ {
@ts[tid] = nsecs;
}
tracepoint:irq:softirq_exit /args->vec == 3 && @ts[tid]/ {
@latency_ns = hist(nsecs - @ts[tid]);
delete(@ts[tid]);
}
END {
print(@latency_ns);
}
'

NET_RX softirq(vec 编号 3,对应 NET_RX_SOFTIRQ)的处理延迟直接影响收包延迟。P99 超过 100 µs 通常意味着 NAPI budget 不够(net.core.netdev_budget)或 GRO 批次过大。

1
2
3
4
5
6
7
8
# 追踪 tcp_sendmsg 的延迟分布
bpftrace -e '
kprobe:tcp_sendmsg { @start[tid] = nsecs; }
kretprobe:tcp_sendmsg /@start[tid]/ {
@us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'

总结

本文系统梳理了 Linux 网络栈的性能优化技术体系:

  1. 硬件卸载(TSO/GSO/GRO/Checksum Offload)将 CPU 密集型工作下推给网卡或延迟到批量处理,是零成本提升吞吐的首选。
  2. 多队列扩展(RSS/RPS/RFS/XPS)将网络处理并行化到多个 CPU 核心,消除单核瓶颈,RFS 还利用 CPU 亲和性提升缓存命中率。
  3. Kernel Bypass(AF_XDP/DPDK)在极限场景下绕过协议栈,以工程复杂度换取数量级的性能提升,AF_XDP 借助 eBPF 提供了更好的灵活性。
  4. 发送路径优化(qdisc bypass/MSG_ZEROCOPY/sendfile)减少数据拷贝和锁竞争,降低发送延迟。
  5. 内存分配(slab 缓存/NUMA 亲和)减少分配开销和跨节点访问延迟。
  6. sysctl 调优针对具体负载模式调整内核行为,配合完善的监控诊断(ethtool/bpftrace/nstat)形成闭环。

这些技术并非孤立存在——真实的高性能网络系统往往是硬件卸载 + RSS + GRO + 大缓冲区的组合,辅以 bpftrace 定位热点,逐层剥离瓶颈。


源码参考位置(Linux 6.4-rc1):

  • include/linux/netdev_features.h — 卸载特性标志定义
  • net/core/gro.c — GRO 实现(napi_gro_receivedev_gro_receive
  • net/core/dev.c — GSO 分段(__skb_gso_segment)、TX 路径(__dev_queue_xmitvalidate_xmit_skb)、RPS(get_rps_cpu)、队列管理(netif_set_real_num_rx/tx_queues
  • net/core/skbuff.c — skb 缓存池(skb_init)、零拷贝分配(msg_zerocopy_alloc
  • include/linux/netdevice.h — RFS 流记录(rps_record_sock_flow
  • include/net/xdp_sock.h / net/xdp/xsk.c — AF_XDP 结构体与收包路径