土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】网络子系统内存管理:sk_buff 分配、page pool 与 NUMA

文章导航

分类入口
linuxnetworking
标签入口
#kernel#memory#sk_buff#page_pool#NUMA#slab#socket#backpressure#bpftrace#truesize

目录

在第一篇我们拆解了 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;
}

关键点:

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 缓存的优势:

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

关键优化:

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%,slowring_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_alloc

4.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_allocsock_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 字段单位是页

压力模式下的行为:

  1. 收缩接收窗口:减少通告窗口大小,减缓发送端速率
  2. 减少发送缓冲sk->sk_sndbuf 动态下调
  3. 丢弃 OFO 包:乱序队列中的包可能被丢弃以释放内存
  4. 拒绝新连接: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_skbconsume_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

每一层都围绕两个核心原则设计:

  1. 减少分配/释放:通过回收(page pool)、预分配(forward_alloc)、缓存(slab/per-CPU)减少到伙伴系统的往返
  2. 减少竞争:通过 per-CPU 结构(NAPI、page pool cache、fragment cache)和批量操作消除锁竞争

理解这些机制,才能正确诊断网络性能问题的根因——是 page pool 容量不足导致 DMA 重映射?是 socket 缓冲区过小导致应用饥饿?还是 TCP 全局内存压力导致窗口收缩?

参考文献

  1. Linux 内核源码 include/linux/skbuff.h(sk_buff 分配与释放)
  2. Linux 内核源码 include/net/page_pool/types.h(page pool 数据结构)
  3. Linux 内核源码 include/net/page_pool/helpers.h(page pool API)
  4. Linux 内核源码 include/net/sock.h(socket 内存记账)
  5. Jesper Dangaard Brouer, “Page Pool API for network drivers”, Netdev Conference, 2018
  6. Jakub Kicinski, “Recent page pool improvements”, Netdev 0x17, 2023

上一篇分段卸载:GRO/GSO/TSO 的内核实现与陷阱

下一篇XDP 内核实现:在驱动层重编程网络栈

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-22 · linux / networking

【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 可观测实战。


By .