Linux Kernel page_pool: 网络栈页面回收与零拷贝 AI 流量工程
为什么需要 page_pool
在现代数据中心网络中,单台服务器每秒可能要处理数百万个数据包。传统的内核网络栈处理流程是:驱动收到数据包 → 分配 page → 构造 skb → 传给协议栈 → 协议栈处理完毕后释放 page。每次收发包都触发一次 page_alloc/page_free,在高pps(packets per second)场景下,这会带来严重的性能瓶颈。
Björn Topel 在 2018 年引入了 page_pool,其核心思想简单而高效:让网络驱动在完成数据包处理后,将使用的 page 重新放回池中,供下一次同一个 RX 队列复用,避免反复调用 page allocator。
page_pool 与常见的 SLUB/SLAB 分配器不同。它不管理任意大小的缓存,而是专门针对网络页面做了"快速回收"策略:
- 页面始终处于 NUMA 本地节点,不会因跨节点迁移引入延迟
- 当页面使用完毕,驱动可以选择直接回收到 pool,而不管上层是否仍持有引用
- 配合 page_pool_put_page 的 allow_direct 参数,允许在软中断上下文中快速回收
对于 AI 流量场景——比如推理服务的 gRPC 请求、分布式训练中的梯度同步、推理缓存的 KV 传输——网络吞吐量直接决定了端到端延迟。一个被传统网络栈"吃掉"的 5μs 分配延迟,对 LLM 首 Token 延迟(TTFT)的影响可能达到 10% 以上。
page_pool 架构设计
核心数据结构
struct page_pool {
struct page_pool_params_slow params;
struct delayed_work destroy_work;
void *frag_pool; /* 用于 page frag 模式 */
/* 追踪仍在"飞行中"的页面数量 */
u32 pages_state_hold_cnt;
/* DMA 映射管理 */
bool dma_map;
bool dma_sync_dev;
enum dma_data_direction dma_dir;
/* 页面大小策略:完整页 vs fragment */
u32 pages_state_release_cnt;
/* 统计计数器 */
u64 alloc_stats[PP_ALLOC_TYPE_MAX];
};
page_pool 的设计哲学是尽可能使用无锁(lock-free)操作。回收路径通过 per-CPU 的页面栈实现快速入栈,分配时则先从"direct recycle"池(per-CPU)取,不足时才走"release"池或最终 page allocator。
三代回收策略
page_pool 的回收经历了三代演进:
第一代:返回 lruvec per_cpu_pages 早期的 page_pool 将页面释放回 buddy 系统。这实现了基本复用,但分配/释放的冷热路径仍然依赖 zone lock。性能提升有限。
第二代:per-CPU direct recycling
当前主流方案。回收时直接将 push 到 per-CPU 的 direct 栈(类似 mc:struct pcpu_drain)。分配时 LIFO 从 direct 栈取出,热路径无需加锁。
第三代:page pool fragment pool 针对小于 PAGE_SIZE 的数据包。当驱动使用 page_pool_alloc_frag 分配小块内存时,复用机制更精细:即使页面未完成使用(一些 fragment 已释放),只要还有 active reference,就能从 fragment pool 继续分配。
page_pool 的碎片池允许页面在被多个小包共享时逐步分配,而不必等到整个页面完全释放。
DMA map/unmap 优化
每次收发包,传统做法需要做 DMA map/unmap 操作,会触发 IOMMU 映射更新和缓存同步。page_pool 的策略是:只要页面回到 pool,DMA map 状态就保留。
只有当页面从 seq_pool 真正释放(长时间不被使用)时,才会调用 dma_unmap_page。这意味着在正常流量模式下,每个页面在其生命周期内只被 DMA map/unmap 较少次数,显著降低了 TLB shootdown 和 IOMMU 开销。
page_pool 与 XDP 的协同
page_pool 最重要的生产用途之一是作为 XDP (eXpress Data Path) 的数据内存提供者。
在 XDP 模式下,数据包在驱动层还未分配到 sk_buff 之前就已处理。此时传统的 skb 分配路径不适用,而 page_pool 提供的 page_pool_alloc_pages 接口恰好满足 XDP 对高性能页面分配的需求。
典型的 XDP + page_pool 工作流:
/* 1. 初始化 page_pool */
struct page_pool_params pp_params = {
.order = 0,
.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
.pool_size = RX_RING_SIZE * 2,
.nid = dev_to_node(dev),
};
struct page_pool *pool = page_pool_create(&pp_params);
/* 2. RX 路径:驱动从硬件拿到描述符后,从 pool 取页面填充 */
struct page *page = page_pool_alloc_pages(pool, gfp);
if (!page)
return -ENOMEM;
dma_addr = dma_map_page(dev, page, PAGE_SIZE/2, PAGE_SIZE/2, DMA_FROM_DEVICE);
nic_fill_rx_desc(ring, i, dma_addr);
/* 3. 数据包处理完毕(或 XDP_TX 后),回收页面 */
page_pool_put_page(page, true);
这种模式的优势在于: - XDP 程序可以直接操作 page 的物理地址,没有 skb 的额外开销 - 如果 XDP 选择 XDP_TX 转发同一张网卡,页面可以零上下文切换地直接填充入 TX 描述符环 - 配合 DO_ONCE 页面初始化策略,批量预取页面消除分配冷启动延迟
page_pool 与 io_uring uring_cmd 的联合优化
Linux 6.8+ 引入了 uring_cmd 对 NVMe 设备 passthrough,同时 page_pool 也在驱动层加速。在 AI 推理服务场景中,一个值得关注的方向是:通过 uring_cmd 将 page_pool 管理的页面直接填充到 NVMe/rDMA 设备。
传统的 io_uring 零拷贝路径需要预先注册缓冲区。page_pool 管理的页面天然具备以下特性,使其适合作为 uring_cmd 的数据载体:
- 全部处于 DMA 可访问的 low memory 区域
- 在 NUMA 本地且已经完成了 IOMMU mapping
- 页面生命周期驱动程序可控,可以安全地跨子系统传递
推理引擎可以构建这样的管道: 1. 网络驱动使用 page_pool 接收 gRPC 请求的 page 2. 推理前处理在 page 上就地完成(避免拷贝到另外的 buffer) 3. 推理结果也写入 page_pool 管理的 page 4. 通过 uring_cmd 将结果 page 直接推送到 NVMe 存储/网络发送路径
这种"page 全流程复用"的设计,理论上可以将推理服务的拷贝次数从传统的 3-4 次降低到接近 0 次。
性能实测:page_pool vs 零拷贝基线
在一台配备 Intel E810-CQDA2 和 AMD EPYC 9654 的服务器上,使用相同流量规格(IMIX 混合包大小)对比了 page_pool 与标准 skb 路径的性能差异。
测试条件:
- 标准路径:默认 ixgbe/i40e 驱动 + skb alloc/free
- page_pool 路径:启用 ethtool -K eth0 gro on + page_pool RX recycling
结果显示,page_pool 在小包处理(64B)场景下带来了显著提升,pps 接近网卡理论线速。特别是在混合流量场景中,减少内存分配延迟对 CPU 利用率的优化也很明显。
同时需要注意,page_pool 引入了一个可能影响性能的细节:当页面的 refcount 无法降至零时,页面无法回到 pool。这通常发生在某些网络功能:
- 启用了 socket timestamping 导致 skb 被长时间持有
- BPF 程序持有对页面的引用
- 网络有 GRO offload 未完成
生产环境中需要通过监控 /proc/net/page_pool 下的统计来确保页面回收率。
生产部署注意事项
1. 页面回收率监控
page_pool 最重要的健康指标是 recycle 率,可以通过以下方式获取:
- ethtool 的 --show-ring 与 --set-ring 命令(部分驱动支持)
- /proc/net/page_pool 中的统计
- 自实现的 sysfs 接口
如果回收率低于预期,检查网络栈中是否存在隐式的 page holder: - 关闭 netpoll(可能持有 skb) - 检查是否有 BPF 程序未正确 release 页面 - 验证 GRO 是否真正合并了流量
2. NUMA 亲和性
考虑为每个 NUMA 节点创建独立的 page_pool 实例,而不是全局共享:
for_each_online_node(node) {
pp_params.nid = node;
pool_per_node[node] = page_pool_create(&pp_params);
}
然后将每个 RX 队列的中断绑定到对应节点的 CPU,实现全链路 NUMA 本地化。
3. 页面大小与 BPF 程序匹配
如果使用 XDP BPF 程序处理数据包,需要确保 PAGE_SIZE 与 BPF 预期一致(通常 4KB)。page_pool 的 fragment 分配模式可能导致数据包无法完全放入一个 fragment。建议在 BPF 处理路径中关闭 fragment 模式或增大 fragment size。
4. 避免 DMA 映射开销
对于一些不需要 DMA mapping 的场景(如 DPDK 模式),可以关闭 PP_FLAG_DMA_MAP,进一步降低回收延迟。大多数生产环境(使用标准驱动)需要开启该标志。
5. 内存用量预估
page_pool 会占用一定的固定内存: - 池大小 = pool_size × PAGE_SIZE × 通道数 - 驱动可能另外有私有缓存
建议根据 QPS 峰值预估所需页面数,避免 pool 过小导致 fallback 到直接 page_alloc。
内核源码关键流程追踪
分配路径
page_pool_alloc_pages(gfp)
└─ page_pool_alloc_pages_fast() [快速:从 per-CPU direct 栈分配]
└─ page_pool_alloc_pages_slow() [慢速:从 release 池或 buddy alloc]
└─ __page_pool_alloc_pages()
└─ alloc_pages_node(nid, gfp)
关键优化点:快速路径没有锁,只需要一个 preempt_disable/enable 保护。慢速路径涉及 zone 锁竞争,在大流量下会成为瓶颈。
回收路径
page_pool_put_page(page, allow_direct)
└─ page_pool_put_full_page()
├─ if (allow_direct && in_softirq())
│ └─ page_pool_direct_release(page) [入 per-CPU 栈]
└─ else
└─ page_pool_release_page() [入 release 池,延迟释放]
注意:大部分驱动在中断上下文(NAPI poll)中调用 page_pool_put_page 且 allow_direct=true,因此实际路径大概率走快速回收。
DMA 映射保持策略
static void page_pool_dma_sync_for_device(...)
{
/* 仅当 pp_flag_dma_sync_dev 设置时同步 */
if (pp->dma_sync_dev)
dma_sync_single_range_for_device(pp->dev, dma_addr, 0, len, pp->dma_dir);
}
这种设计在多数场景下有效:如果页面内容在 Device → CPU 传输过程中没有被修改,仅需要存档同步一次(第一次使用或 recycle 回 pool 时)。
page_pool 未来方向
随着 AI 工作负载的持续增长,page_pool 在以下方面有进一步发展空间:
与 io_uring zcrx 协同
Linux 6.14+ 引入了 io_uring zcrx(Zero-Copy Receive),允许直接把收到的数据包 page 映射到用户空间。page_pool 可以作为 zcrx 的底层 page provider,实现"从网卡到用户空间零 page 拷贝"的极致路径。这比 DPDK 的 mempool 更通用(DPDK 需要驱动旁路,page_pool 原生集成在内核栈中)。
智能页面回收调度
当前的页面回收是 LIFO 的,没有预测能力。未来可以根据驱动流量特征做智能调度: - 在已知即将收到大包时,直接分配大页(HugeTLB) - 在流量低谷时主动释放页面(避免内存浪费) - 通过 eBPF 驱动的自适应策略(如根据 P99 延迟动态调整 pool 大小)
与 GPU Networking(GPUDirect)的融合
NVIDIA Magnum IO 的 GPUDirect RDMA 需要将页面锁定为 DMA 常驻。page_pool 如果能原生支持"GDR 模式"(页面始终映射到 GPU),将进一步简化 AI 训练中 GPU 数据的网络传输链路。
这要求 page_pool 在创建时预先 map 所有池页面到 GPU 设备(一次性成本),之后网络包直接落入 GPU 显存区域。
总结
page_pool 是 Linux 网络栈中最值得关注的底层性能优化之一。它通过简单而精妙的页面回收机制,将网络 I/O 的内存管理从"按需分配"转变为"预热复用",在 AI 推理、网络功能虚拟化等高性能场景中已经验证了显著收益。
对于追求极致网络性能的工程师来说,理解 page_pool 的三代回收策略、DMA map 保持机制、以及与 XDP 和 io_uring 的协同设计,是构建下一代零拷贝 IO 管道的关键一步。
在未来版本中,page_pool 有潜力成为 Linux 网络栈的核心组件——不仅服务于传统网卡,还将成为跨子系统(GPU、存储、eBPF)的统一页面生命周期管理框架。

发表评论 取消回复