在第一篇我们拆解了 sk_buff
的数据结构布局,在收发包路径中我们看到每个网络包都需要分配和释放
sk_buff。10Gbps 网口每秒 812 万包意味着每秒 812
万次 sk_buff 分配和释放——如果走普通的
kmalloc/kfree,仅分配器锁竞争就会把
CPU 打满。
内核为网络子系统设计了一套精密的内存管理体系:slab
缓存加速 sk_buff 头部分配,page pool
管理数据页的零拷贝回收,NAPI 上下文的 per-CPU
缓存消除跨核竞争,socket
层的内存记账与反压防止单连接耗尽系统内存。这些机制环环相扣,共同支撑了高速网络的内存效率。
本文基于 Linux 6.6/6.8 源码,全面拆解网络内存管理的四层架构。
一、sk_buff 分配路径
sk_buff
的分配分为两部分:头部结构体(约 256
字节,从 slab
缓存分配)和数据缓冲区(从页分配器或 page
pool 获取)。不同场景使用不同的分配函数。
1.1 分配函数族
// include/linux/skbuff.h
// 1. 基础分配
struct sk_buff *__alloc_skb(unsigned int size, gfp_t priority,
int flags, int node);
// 2. 驱动收包分配(中断/softirq 上下文)
struct sk_buff *__netdev_alloc_skb(struct net_device *dev,
unsigned int length,
gfp_t gfp_mask);
// 3. NAPI 轮询分配(最高效)
struct sk_buff *__napi_alloc_skb(struct napi_struct *napi,
unsigned int length,
gfp_t gfp_mask);
// 4. 便利宏
#define netdev_alloc_skb(dev, length) \
__netdev_alloc_skb(dev, length, GFP_ATOMIC)
#define napi_alloc_skb(napi, length) \
__napi_alloc_skb(napi, length, GFP_ATOMIC)
// 5. IP 对齐版本(预留 NET_IP_ALIGN 字节 headroom)
static inline struct sk_buff *
netdev_alloc_skb_ip_align(struct net_device *dev, unsigned int length)选择原则:
| 函数 | 上下文 | 特点 |
|---|---|---|
__alloc_skb |
任意 | 通用分配,指定 NUMA 节点 |
netdev_alloc_skb |
硬中断/softirq | 驱动收包,GFP_ATOMIC |
napi_alloc_skb |
NAPI poll | per-CPU 缓存,最快 |
alloc_skb |
进程上下文 | 可睡眠(GFP_KERNEL) |
1.2 __alloc_skb 内部流程
// net/core/skbuff.c (概念性)
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct sk_buff *skb;
u8 *data;
// 步骤 1:从 slab 缓存分配 sk_buff 头部
if (flags & SKB_ALLOC_FCLONE)
skb = kmem_cache_alloc_node(skbuff_fclone_cache, gfp_mask, node);
else
skb = kmem_cache_alloc_node(skbuff_cache, gfp_mask, node);
// 步骤 2:分配数据缓冲区
size = SKB_DATA_ALIGN(size); // 对齐到 cache line
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info)); // 尾部 shinfo
data = kmalloc_reserve(&size, gfp_mask, node, &pfmemalloc);
// 步骤 3:初始化指针
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size - SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
skb->truesize = SKB_TRUESIZE(size); // 记账大小
return skb;
}关键点:
- 两次分配:sk_buff 头和数据缓冲区是独立分配的,允许数据缓冲区复用(clone 时共享)
- slab 缓存:
skbuff_cache和skbuff_fclone_cache是专用 kmem_cache,避免通用 slab 的碎片化 - fclone:
SKB_ALLOC_FCLONE标志预分配一对 sk_buff,优化高频 clone 场景(如 TCP 重传) - truesize:记录该 skb 的真实内存占用,用于 socket 内存记账
1.3 NAPI 分配的 per-CPU 优化
napi_alloc_skb() 利用 NAPI 的单 CPU
执行特性,使用 per-CPU fragment 缓存避免分配器竞争:
// net/core/skbuff.c (概念性)
struct sk_buff *__napi_alloc_skb(struct napi_struct *napi,
unsigned int len, gfp_t gfp_mask)
{
// 对于小包(< PAGE_SIZE)
// 从 per-CPU 的 napi_alloc_cache 分配 page fragment
// 多个小 skb 共享同一个物理页
struct page_frag_cache *nc = this_cpu_ptr(&napi_alloc_cache);
void *data = page_frag_alloc(nc, len, gfp_mask);
// 构建 sk_buff 并指向 fragment 数据
skb->head = data;
skb->head_frag = 1; // 标记使用 page fragment
return skb;
}per-CPU fragment 缓存的优势:
- 无锁:NAPI 在单个 CPU 的 softirq 上下文执行,无需自旋锁
- 页共享:一个 4KB 页可以切分给多个小包(如 128 字节的 ACK 包)
- 缓存友好:连续分配的 skb 数据在同一页,L1/L2 cache 命中率高
1.4 skb_clone 与 skb_copy
// 三种 skb 复制方式
// clone:共享数据缓冲区,独立 sk_buff 头
struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t priority);
// 用途:TCP 重传(保留原始 skb 直到 ACK)
// 开销:仅分配 sk_buff 头(~256 字节)
// pskb_copy:复制头部区域数据,共享 page fragment
struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask);
// 用途:需要修改 L3/L4 头但不改载荷
// 开销:分配 sk_buff 头 + linear 区域
// skb_copy:完全深拷贝
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t priority);
// 用途:需要修改所有数据
// 开销:分配 sk_buff 头 + 全部数据性能对比:
| 操作 | 分配量 | 典型用途 |
|---|---|---|
skb_clone |
~256B | TCP 重传、netfilter |
pskb_copy |
~256B + headlen | NAT(改头不改尾) |
skb_copy |
~256B + 全部数据 | 完全修改(罕见) |
二、page pool:高性能页面回收
page pool 是 Linux 4.18 引入、在 6.x 中大幅增强的驱动级页面管理框架。它解决的核心问题是:DMA 映射/解映射的开销比页分配本身还大,而且 IOMMU 的 TLB 刷新代价极高。page pool 通过页面回收消除重复的 DMA 映射操作。
2.1 核心数据结构
// include/net/page_pool/types.h
struct page_pool_params {
/* 快路径字段(常在栈上) */
unsigned int flags; // PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV
unsigned int order; // 页阶数(2^order 页/次)
unsigned int pool_size; // ptr_ring 大小
int nid; // NUMA 节点
struct device *dev; // DMA 设备
struct napi_struct *napi; // 关联 NAPI(单消费者优化)
enum dma_data_direction dma_dir;
unsigned int max_len; // DMA 同步最大长度
unsigned int offset; // DMA 同步起始偏移
/* 慢路径字段 */
struct net_device *netdev;
void (*init_callback)(struct page *page, void *arg);
void *init_arg;
};// include/net/page_pool/types.h
#define PP_ALLOC_CACHE_SIZE 128 // per-CPU 快缓存容量
#define PP_ALLOC_CACHE_REFILL 64 // 补充水位(= NAPI budget)
struct pp_alloc_cache {
u32 count;
struct page *cache[PP_ALLOC_CACHE_SIZE];
};
struct page_pool {
struct page_pool_params_fast p;
long frag_users; // 当前 fragment 用户数
struct page *frag_page; // 当前切分中的页
unsigned int frag_offset; // 切分偏移
struct pp_alloc_cache alloc; // per-CPU 快缓存
struct ptr_ring ring; // 无锁回收队列
// ...
};2.2 分配路径
// include/net/page_pool/helpers.h
// 分配整页
static inline struct page *
page_pool_dev_alloc_pages(struct page_pool *pool);
// 分配页 fragment(从一个大页切分多个小块)
static inline struct page *
page_pool_dev_alloc_frag(struct page_pool *pool,
unsigned int *offset,
unsigned int size);
// 智能分配(根据大小自动选择整页或 fragment)
static inline struct page *
page_pool_dev_alloc(struct page_pool *pool,
unsigned int *offset,
unsigned int *size);分配流程:
page_pool_alloc_pages()
→ 1. 检查 per-CPU alloc.cache[]
→ cache 非空?直接弹出(最快路径)
→ 2. cache 为空 → 从 ptr_ring 补充
→ ptr_ring 有页?批量转移到 cache
→ 3. ptr_ring 也空 → 走 page allocator
→ __page_pool_alloc_page_order()
→ 如果 PP_FLAG_DMA_MAP → dma_map_page_attrs()
→ 存储 DMA 地址到 page->dma_addr
关键优化:
- 快缓存 128 页:覆盖 2 轮 NAPI 轮询(budget=64)的页需求
- 补充水位 64:一次性从 ring 补充 64 页,摊销 ring 操作开销
- DMA 地址缓存:页回收时不解除 DMA 映射,下次分配直接使用
2.3 回收路径
page pool 的核心价值在于回收。当 skb 被释放时,其数据页可以回到 page pool 而非释放到伙伴系统:
// 驱动在分配时标记页属于 page pool
skb_mark_for_recycle(skb);
// 设置 skb->pp_recycle = 1
// 释放时检查标记
// net/core/skbuff.c
void __kfree_skb(struct sk_buff *skb)
{
// ...
if (skb->pp_recycle)
// 页回到 page pool 而非 free_pages()
page_pool_return_skb_page(page);
}回收流程:
页释放路径
→ 同 CPU?→ 放入 alloc.cache[](最快)
→ cache 满?→ 放入 ptr_ring(无锁 MPSC 队列)
→ ptr_ring 满?→ 真正释放(稀有,说明池太小)
2.4 page pool 统计
// include/net/page_pool/types.h
struct page_pool_alloc_stats {
u64 fast; // 快缓存命中
u64 slow; // 走 page allocator(order-0)
u64 slow_high_order; // 走 page allocator(高阶)
u64 empty; // ring 空,被迫分配
u64 refill; // 缓存补充次数
u64 waive; // NUMA 不匹配页被拒绝
};
struct page_pool_recycle_stats {
u64 cached; // 回到 per-CPU 缓存
u64 cache_full; // 缓存满,走 ring
u64 ring; // 放入 ptr_ring
u64 ring_full; // ring 满,真正释放
u64 released_refcnt; // refcnt>1,无法回收
};# 查看 page pool 统计
cat /sys/kernel/debug/page_pool/page_pool-*
# 或使用 ethtool
ethtool -S eth0 | grep page_pool理想状态:fast 占比 >
95%,slow 和 ring_full 接近
0。如果 waive 非零,说明存在跨 NUMA
回收问题。
2.5 page pool 与 DMA
传统 DMA 映射每包两次操作(map + unmap),在 IOMMU 场景下开销巨大:
传统路径(每包):
alloc_page() → dma_map_page() → 驱动写入 → dma_unmap_page() → free_page()
每包 4 次重量级操作
page pool 路径:
page_pool_alloc() → [DMA 映射已缓存] → dma_sync_single_for_device() → 驱动写入
回收: page_pool_put() → [保持 DMA 映射] → dma_sync_single_for_cpu()
仅需 2 次轻量 sync 操作
PP_FLAG_DMA_MAP 让 page pool
在首次分配时建立 DMA
映射,回收时保持映射不变。PP_FLAG_DMA_SYNC_DEV
在重新分配时只做轻量的 cache 同步(而非完整的
map/unmap)。
三、NUMA 感知分配
在多路服务器上,跨 NUMA 节点的内存访问延迟可达本地的 2-3 倍。网络子系统通过多层机制确保内存分配尽可能在处理 CPU 的本地 NUMA 节点。
3.1 层级式 NUMA 亲和
层级 1:page pool
page_pool_params.nid = 绑定 NUMA 节点
page_pool_params.napi = 绑定 NAPI 实例
→ 分配的页在指定 NUMA 节点
层级 2:NAPI per-CPU 缓存
napi_alloc_skb() 运行在 NAPI 轮询 CPU
per-CPU fragment cache 天然本地分配
层级 3:sk_buff slab 缓存
__alloc_skb(... node) 可指定 NUMA 节点
kmem_cache 支持 NUMA-aware 分配
层级 4:socket 缓冲区
sk->sk_allocation 控制 GFP 标志
通常分配在应用线程运行的 NUMA 节点
3.2 NUMA 不匹配检测
page pool 会检测跨 NUMA 回收的页并拒绝复用:
// page pool 分配时(概念性)
struct page *page_pool_alloc_pages(struct page_pool *pool, gfp_t gfp)
{
struct page *page;
// 从 ring 取出页
page = ptr_ring_consume(&pool->ring);
if (page) {
// 检查 NUMA 节点
if (page_to_nid(page) != pool->p.nid) {
// NUMA 不匹配 → 拒绝,统计 waive++
page_pool_return_page(pool, page);
goto alloc_new;
}
return page;
}
alloc_new:
// 从指定 NUMA 节点分配新页
return __page_pool_alloc_page_order(pool, gfp);
}3.3 中断亲和与内存局部性
要实现真正的 NUMA 感知,需要从中断绑定开始:
# 确保网卡中断绑定到正确的 NUMA 节点
# 查看网卡的 NUMA 节点
cat /sys/class/net/eth0/device/numa_node
# 查看 CPU 的 NUMA 分布
lscpu | grep NUMA
# 绑定中断到同一 NUMA 节点的 CPU
# 例如 NUMA node 0 = CPU 0-15
echo 0000ffff > /proc/irq/XX/smp_affinity
# 绑定 NAPI 到指定 CPU(通过 RSS)
ethtool -X eth0 equal 16完整的 NUMA 对齐链路:
NIC PCIe → NUMA node 0
→ IRQ 绑定 → CPU 0-15(node 0)
→ NAPI 轮询 → CPU 0-15
→ page pool (nid=0) → node 0 页面
→ napi_alloc_skb → node 0 slab
→ 协议栈处理 → node 0 socket 缓冲区
→ 应用线程 → 绑定 node 0 CPU
任何环节的 NUMA 不对齐都会引入跨节点访问延迟。
四、socket 内存记账与反压
4.1 socket 内存字段
// include/net/sock.h
struct sock {
int sk_rcvbuf; // 接收缓冲区上限
int sk_sndbuf; // 发送缓冲区上限
refcount_t sk_wmem_alloc; // 发送路径已分配字节
int sk_wmem_queued; // 发送队列排队字节
int sk_forward_alloc; // 预分配额度
u32 sk_reserved_mem; // 保留不可回收内存
struct {
atomic_t rmem_alloc; // 接收路径已分配字节
int len;
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog;
};
#define sk_rmem_alloc sk_backlog.rmem_alloc4.2 内存记账流程
每个进入 socket 接收队列的 skb,其 truesize
会被计入 sk_rmem_alloc:
// 收包路径记账
void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb->sk = sk;
skb->destructor = sock_rfree; // 释放时回调
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
// 充值到 sk->sk_forward_alloc
sk_mem_charge(sk, skb->truesize);
}
// 释放时回调
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_uncharge(sk, skb->truesize);
// 如果有等待的写者/读者,唤醒
}发包路径类似:skb_set_owner_w() 计入
sk_wmem_alloc,sock_wfree()
回调减少。
4.3 反压机制
当 socket 内存使用达到上限时,内核拒绝接收新包:
// TCP 收包路径
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
// ...
if (!sk_rmem_schedule(sk, skb, skb->truesize)) {
// 内存不足 → 丢弃该包
// TCP 发送端会重传
goto drop;
}
// ...
}
// 内存检查
static inline bool sk_rmem_schedule(struct sock *sk,
struct sk_buff *skb,
int size)
{
// 检查 sk_rmem_alloc + size <= sk_rcvbuf
// 加上全局 TCP 内存压力检查
return __sk_rmem_schedule(sk, size, skb_pfmemalloc(skb));
}4.4 TCP 全局内存压力
除了 per-socket 限制,TCP 还有全局内存限制:
// include/net/tcp.h
extern long sysctl_tcp_mem[3]; // [low, pressure, high]
extern atomic_long_t tcp_memory_allocated;
extern unsigned long tcp_memory_pressure;三级水位:
| 水位 | sysctl | 含义 |
|---|---|---|
| low | tcp_mem[0] |
低于此值,无内存压力 |
| pressure | tcp_mem[1] |
进入压力模式,收缩窗口 |
| high | tcp_mem[2] |
超过此值,拒绝新分配 |
# 查看 TCP 内存水位(单位:页)
cat /proc/sys/net/ipv4/tcp_mem
# 例如:94389 125852 188778
# low=369MB pressure=491MB high=738MB(4KB 页)
# 查看当前 TCP 内存使用
cat /proc/net/sockstat | grep TCP
# TCP: inuse 1234 orphan 12 tw 567 alloc 1500 mem 8901
# mem 字段单位是页压力模式下的行为:
- 收缩接收窗口:减少通告窗口大小,减缓发送端速率
- 减少发送缓冲:
sk->sk_sndbuf动态下调 - 丢弃 OFO 包:乱序队列中的包可能被丢弃以释放内存
- 拒绝新连接:SYN 队列可能拒绝新的握手
4.5 sysctl 参数总览
# socket 默认/最大缓冲区
net.core.rmem_default = 212992 # 接收默认(208KB)
net.core.rmem_max = 212992 # 接收最大
net.core.wmem_default = 212992 # 发送默认
net.core.wmem_max = 212992 # 发送最大
# TCP 自动调优范围 [min default max]
net.ipv4.tcp_rmem = 4096 131072 6291456 # 4KB 128KB 6MB
net.ipv4.tcp_wmem = 4096 16384 4194304 # 4KB 16KB 4MB
# TCP 全局内存限制(页数)
net.ipv4.tcp_mem = 94389 125852 188778
# 应用层设置
# setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
# setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));4.6 sk_forward_alloc:预分配优化
为了减少频繁的全局内存记账操作,内核引入了
sk_forward_alloc 预分配机制:
// 概念性
void sk_mem_charge(struct sock *sk, int size)
{
// 从 forward_alloc 中扣除
sk->sk_forward_alloc -= size;
if (sk->sk_forward_alloc < 0) {
// forward_alloc 耗尽 → 从全局池批量补充
int amt = sk_mem_pages(size);
sk->sk_forward_alloc += amt * PAGE_SIZE;
atomic_long_add(amt, &tcp_memory_allocated);
// 检查是否触发压力模式
}
}这类似于 slab 分配器的 per-CPU cache:先从本地预分配额度扣除,耗尽时才访问全局计数器,减少原子操作竞争。
五、sk_buff 释放与回收
5.1 释放函数选择
// include/linux/skbuff.h
// NAPI 上下文释放(可批量延迟)
void napi_consume_skb(struct sk_buff *skb, int budget);
// 通用释放
void consume_skb(struct sk_buff *skb); // 正常消费
void kfree_skb(struct sk_buff *skb); // 丢包释放(触发 drop 统计)
// 驱动上下文宏
#define dev_kfree_skb(a) consume_skb(a)
#define dev_consume_skb_any(a) consume_skb(a)kfree_skb 与 consume_skb
的区别不在于释放逻辑,而在于追踪:kfree_skb
触发 kfree_skb
tracepoint(丢包监控的关键),consume_skb 触发
consume_skb tracepoint(正常消费)。
5.2 napi_consume_skb 的批量优化
// net/core/skbuff.c (概念性)
void napi_consume_skb(struct sk_buff *skb, int budget)
{
if (budget == 0) {
// budget=0 意味着非 NAPI 上下文
// 走普通释放路径
dev_consume_skb_any(skb);
return;
}
// NAPI 上下文:延迟释放
// 利用 softirq 的 per-CPU 特性
// 批量释放减少锁竞争和 cache 失效
if (skb->pp_recycle) {
// page pool 回收路径
// 页回到 page pool 而非 free_pages()
}
// 释放 sk_buff 头部到 slab
// 释放 frag 页面到 page pool 或伙伴系统
}5.3 skb_mark_for_recycle
驱动在分配 skb 时标记 page pool 回收:
// include/linux/skbuff.h
static inline void skb_mark_for_recycle(struct sk_buff *skb)
{
#ifdef CONFIG_PAGE_POOL
skb->pp_recycle = 1;
#endif
}
// 典型驱动用法:
struct page *page = page_pool_dev_alloc_pages(pool);
// ... 设置 DMA,网卡写入数据 ...
skb = napi_alloc_skb(napi, headlen);
skb_add_rx_frag(skb, 0, page, offset, len, truesize);
skb_mark_for_recycle(skb);
napi_gro_receive(napi, skb);整个生命周期:
page_pool 分配 → DMA 映射(首次)→ 网卡写入数据
→ skb_mark_for_recycle(skb) → 协议栈处理
→ 应用 recv() → skb 释放
→ pp_recycle=1 → 页回到 page_pool
→ DMA 映射保持 → 再次分配给网卡
六、netdev_budget 与 softirq 内存限制
6.1 NAPI budget 机制
NAPI 轮询不能无限处理包,否则其他 softirq 会饥饿:
// net/core/dev.c (概念性)
static void net_rx_action(struct softirq_action *h)
{
unsigned long budget = netdev_budget; // 默认 300
unsigned long time_limit = jiffies +
usecs_to_jiffies(netdev_budget_usecs); // 默认 2ms
while (budget > 0 && time_before(jiffies, time_limit)) {
// 遍历 poll_list 中的 NAPI 实例
struct napi_struct *n = list_first_entry(...);
int work = napi_poll(n, min(budget, n->weight));
budget -= work;
}
}| 参数 | 默认值 | 说明 |
|---|---|---|
net.core.netdev_budget |
300 | 单次 softirq 最大包数 |
net.core.netdev_budget_usecs |
2000 | 单次 softirq 最大微秒 |
| NAPI weight | 64 | 单个 NAPI 实例单次轮询上限 |
6.2 per-CPU backlog 队列
非 NAPI 设备或 RPS 重定向的包进入 per-CPU backlog 队列:
// net/core/dev.c
// 全局参数
int netdev_max_backlog = 1000; // 每 CPU backlog 上限
// 入队时检查
int enqueue_to_backlog(struct sk_buff *skb, int cpu, ...)
{
struct softnet_data *sd = &per_cpu(softnet_data, cpu);
if (skb_queue_len(&sd->input_pkt_queue) > netdev_max_backlog) {
// 队列满 → 丢包
// 计入 /proc/net/softnet_stat 第二列
sd->dropped++;
kfree_skb(skb);
return NET_RX_DROP;
}
// 入队成功
}# 监控 backlog 丢包
cat /proc/net/softnet_stat
# 每行对应一个 CPU
# 第一列:已处理包数
# 第二列:backlog 丢包数(应为 0)
# 第三列:softirq 时间超限次数七、可观测性实战
7.1 追踪 sk_buff 分配热点
# 统计 sk_buff 分配来源
bpftrace -e '
kprobe:__alloc_skb {
@alloc_skb = count();
}
kprobe:__napi_alloc_skb {
@napi_alloc = count();
}
kprobe:__netdev_alloc_skb {
@netdev_alloc = count();
}
interval:s:5 {
print(@alloc_skb);
print(@napi_alloc);
print(@netdev_alloc);
clear(@alloc_skb);
clear(@napi_alloc);
clear(@netdev_alloc);
}
'7.2 追踪 page pool 回收效率
# 统计 page pool 的回收路径
bpftrace -e '
kprobe:page_pool_return_skb_page {
@pp_recycle = count();
}
kprobe:__page_pool_alloc_pages_slow {
@pp_slow_alloc = count();
}
interval:s:5 {
printf("page_pool recycle: %lld, slow_alloc: %lld\n",
@pp_recycle, @pp_slow_alloc);
clear(@pp_recycle);
clear(@pp_slow_alloc);
}
'回收/慢分配比应该 > 100:1。如果
pp_slow_alloc 频繁,说明 page pool
容量不足或存在跨 NUMA 回收。
7.3 socket 内存压力监控
# 追踪 TCP 内存压力事件
bpftrace -e '
kprobe:tcp_enter_memory_pressure {
printf("TCP memory pressure entered! allocated: %ld pages\n",
*kaddr("tcp_memory_allocated"));
@pressure_events = count();
}
kprobe:tcp_leave_memory_pressure {
printf("TCP memory pressure left\n");
}
'
# 持续监控 TCP 内存使用
watch -n 1 'cat /proc/net/sockstat | grep TCP'7.4 truesize 异常检测
# 检测 truesize 异常大的 skb(可能是内存泄漏)
bpftrace -e '
kprobe:skb_set_owner_r {
$skb = (struct sk_buff *)arg0;
if ($skb->truesize > 65536) {
printf("Large truesize: %d bytes, dev=%s\n",
$skb->truesize, str($skb->dev->name));
@large_truesize = hist($skb->truesize);
}
}
interval:s:30 { print(@large_truesize); clear(@large_truesize); }
'7.5 perf 分析内存分配开销
# 分析网络内存分配在总 CPU 中的占比
perf record -g -a -e cycles -- sleep 10
perf report --no-children | grep -E '(alloc_skb|page_pool|kmem_cache)'
# 追踪 DMA 映射开销
perf stat -e 'dma:*' -a -- sleep 10八、调优参数速查
| 参数 | 默认值 | 建议 |
|---|---|---|
net.core.rmem_max |
212992 | 高吞吐:16MB+ |
net.core.wmem_max |
212992 | 高吞吐:16MB+ |
net.ipv4.tcp_rmem |
4096 131072 6291456 | 高吞吐:4096 524288 16777216 |
net.ipv4.tcp_wmem |
4096 16384 4194304 | 高吞吐:4096 262144 16777216 |
net.ipv4.tcp_mem |
自动计算 | 通常无需调整 |
net.core.netdev_budget |
300 | 高吞吐:600 |
net.core.netdev_budget_usecs |
2000 | 高吞吐:4000 |
net.core.netdev_max_backlog |
1000 | 高吞吐:10000 |
| IRQ 亲和 | 自动 | 绑定到 NIC 同 NUMA 节点 |
调优检查清单
# 1. 确认 NUMA 对齐
cat /sys/class/net/eth0/device/numa_node
cat /proc/interrupts | grep eth0
# 2. 检查 page pool 是否启用
ethtool -S eth0 | grep -i page_pool
# 3. 检查内存压力
cat /proc/net/sockstat
# TCP: mem 字段不应接近 tcp_mem[1]
# 4. 检查 backlog 丢包
cat /proc/net/softnet_stat
# 第二列应为 0
# 5. 检查 socket 缓冲区使用
ss -tm # 查看每连接的内存使用九、总结
网络子系统的内存管理是一个四层架构:
| 层级 | 机制 | 核心优化 |
|---|---|---|
| 数据页 | page pool | DMA 映射缓存、per-CPU 快缓存、页面回收 |
| sk_buff 头 | slab 缓存 | fclone 预分配、NAPI per-CPU fragment |
| socket 记账 | truesize + backpressure | per-socket 限额、全局压力模式 |
| 全局限制 | netdev_budget | 时间+包数双重限制、per-CPU backlog |
每一层都围绕两个核心原则设计:
- 减少分配/释放:通过回收(page pool)、预分配(forward_alloc)、缓存(slab/per-CPU)减少到伙伴系统的往返
- 减少竞争:通过 per-CPU 结构(NAPI、page pool cache、fragment cache)和批量操作消除锁竞争
理解这些机制,才能正确诊断网络性能问题的根因——是 page pool 容量不足导致 DMA 重映射?是 socket 缓冲区过小导致应用饥饿?还是 TCP 全局内存压力导致窗口收缩?
参考文献
- Linux 内核源码
include/linux/skbuff.h(sk_buff 分配与释放) - Linux 内核源码
include/net/page_pool/types.h(page pool 数据结构) - Linux 内核源码
include/net/page_pool/helpers.h(page pool API) - Linux 内核源码
include/net/sock.h(socket 内存记账) - Jesper Dangaard Brouer, “Page Pool API for network drivers”, Netdev Conference, 2018
- Jakub Kicinski, “Recent page pool improvements”, Netdev 0x17, 2023
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Linux 网络子系统深度拆解】内核网络调优方法论:从基准测试到生产验证
系统化的 Linux 内核网络调优方法论:从基准测试建立性能基线,到 sysctl 参数与内核数据结构的对应关系,再到中断亲和性、NUMA 拓扑、ring buffer、qdisc 的逐层调优,最终通过 A/B 对比验证生产效果。
【Linux 网络子系统深度拆解】网络丢包定位:从 drop_monitor 到 kfree_skb 追踪
从内核源码拆解 Linux 网络丢包追踪的完整体系:kfree_skb tracepoint 与 80+ 种 drop_reason 枚举、drop_monitor netlink 子系统、dropwatch 工具、perf 丢包记录、bpftrace 丢包聚合脚本,以及生产环境常见丢包点速查表。
【Linux 网络子系统深度拆解】内核网络追踪工具箱:bpftrace/perf/ftrace 实战
从内核 tracepoint 定义出发,系统讲解 bpftrace、perf、ftrace 三大工具在网络诊断中的实战用法:TCP 重传根因分析、softirq 延迟定位、收发包路径延迟剖析、conntrack 表满监控、per-function 火焰图,以及各工具的适用场景与性能开销对比。
【Linux 网络子系统深度拆解】eBPF 网络钩子全景:TC/XDP/socket/cgroup
从内核源码全面拆解 eBPF 在网络子系统中的所有挂载点:TC BPF direct-action 模式与 bpf_mprog 多程序链、XDP 驱动级钩子回顾、socket ops 回调与 TCP 生命周期事件、cgroup BPF 策略控制、sk_msg/sk_skb 的 sockmap 重定向引擎、struct_ops 实现自定义拥塞控制,以及 bpftrace 可观测实战。