chaowen.← Back to archive

Linux networking / packet memory

Linux page_pool: the lifetime of a recycled RX page

A high-speed NIC should not allocate, map, unmap, and free a physical page for every received packet. Linux page_pool turns that expensive lifecycle into a controlled cache—but only if ownership, DMA visibility, and references line up.

Linux deep dive · Networking and packet memory

The concrete problem

Receiving a packet starts before an sk_buff exists. A driver must provide a DMA destination, hand it to the NIC, reclaim it after completion, and eventually make the memory available again. The naïve loop looks like this:

allocate page → DMA-map page → post RX descriptor → NIC writes frame → unmap/sync → build skb → stack releases skb → free page

At millions of packets per second, the fixed costs dominate: page allocation touches allocator and page-state machinery; an IOMMU mapping updates device page tables and may invalidate IOTLB entries; unmapping repeats that work. The payload is also often only a few hundred bytes, while the memory operations around it are comparatively large.

page_pool is a per-RX-queue allocator and recycler. It keeps a bounded supply of pages near the NAPI context, and commonly keeps them DMA-mapped while they circulate between the NIC and the networking stack.

The optimisation is not “reuse memory blindly.” It is “reuse a page only after every consumer has released it and the device can safely own it again.”

One page, several owners

A page can be referenced by multiple representations of the same packet. The driver may attach a fragment of it to an skb; XDP may expose it as an xdp_buff or xdp_frame; a socket or zero-copy consumer may retain it. The page pool cannot recycle it at the moment the driver finishes parsing the descriptor.

CPU / stack ownership ┌──────────────────────┐ NIC DMA ownership ─┤ page_pool page ├─ skb/XDP references └──────────────────────┘ │ last reference released ▼ pool recycle cache → RX ring Device must never DMA-write while a live consumer can still read.

The essential state transition is:

pool allocates → driver maps/posts → device writes → driver takes page back → stack holds reference → final release → pool recycles → driver posts again

That “final release” is the important edge. A fast path can put a page into a per-CPU cache; a slower or remote release can return it through a deferred path. Either way, the page must not re-enter the device-owned ring prematurely.

The implementation map

The public API is deliberately small. The useful source files are:

LocationWhat to inspect
include/net/page_pool/types.hDefinitions of struct page_pool, parameters, per-CPU caches, DMA direction, and statistics.
include/net/page_pool/helpers.hHot inline helpers used by drivers, including allocation and release decisions.
net/core/page_pool.cPool creation/destruction, slow paths, recycling, deferred releases, and DMA synchronization.
Kernel page-pool documentationContract for drivers: NAPI association, DMA mapping, recycling, and teardown.

The structure is not a general-purpose slab cache. Its design assumes a packet RX workload: allocation and recycling happen close to NAPI, pages can be split into fragments, and DMA mapping is part of the lifetime.

Creation: make the policy explicit

A driver fills a struct page_pool_params and calls page_pool_create(). Important fields include:

Field or choiceWhy it matters
pool_sizeBounds the pool’s intended working set; too small causes allocation pressure, too large retains memory.
order / flagsControls page order and behaviours such as fragment use.
nidSteers allocation toward a NUMA node, which matters when the NIC and NAPI CPU are local to one socket.
dma_dir and deviceTell the pool how mappings will be used and which device owns the DMA address space.
napiAssociates recycling with the NAPI context and enables the intended fast path.

Modern drivers often use page_pool_dev_alloc_pages() or its fragment-oriented helpers in the RX refill path. The helper first tries the local cache, then the pool’s slower allocation path. The driver converts the page and offset into the DMA address expected by its descriptor format.

Fast recycling and the slow path

When a packet consumer releases the page, page_pool_put_full_page() is the conceptual hand-off back to the pool. If the release occurs in the right NAPI/CPU context, the page can return directly to the local cache. If not, it may enter a deferred ring and be drained later by the owning NAPI context. This preserves locality without pretending that arbitrary CPUs can safely manipulate every fast-path cache.

Pool statistics separate allocation from recycling. Current kernels expose fast and slow allocation counts, plus recycle outcomes such as cached, ring, cache_full, ring_full, and released_refcnt. A rising slow-allocation count or a page repeatedly reaching the ring instead of the local cache can explain allocator pressure and poorer cache locality; neither is automatically a bug.

DMA mapping is part of the contract

With a mapped page, the device sees a DMA address, not the CPU virtual address. The pool may keep that mapping across multiple RX cycles, but mapping persistence is conditional: the device, direction, IOMMU state, and teardown rules must all match.

CPU virtual page ──CPU page tables──▶ RAM │ └──dma_map_page()──▶ DMA address ──IOMMU──▶ permitted RAM RX: device writes page → CPU syncs/reads → page is returned to device only after the required device sync.

On a coherent x86 server, missing cache maintenance may remain invisible in testing. On a non-coherent architecture, the driver must respect the DMA API’s CPU/device ownership transitions. The page-pool implementation has logic for syncing or unmapping when the pool’s mapping assumptions do not permit a simple reuse.

Do not casually replace a page-pool helper with dma_unmap_page() or free the page yourself. That changes the ownership protocol and can cause a later recycle to use a stale DMA address—or make the IOMMU retain a mapping to memory that has already been reused for another purpose.

RX data flow in a real driver

probe/open:
    params = { .pool_size = ring_size, .napi = &rxq->napi,
               .dma_dir = DMA_FROM_DEVICE, .dev = dev }
    rxq->pool = page_pool_create(&params)

refill:
    page = page_pool_dev_alloc_pages(rxq->pool)
    dma = page_pool_get_dma_addr(page)
    write_dma_address_to_rx_descriptor(dma)
    publish_descriptor_to_nic()

NAPI poll:
    completion = read_completed_descriptor()
    page = completion->page
    build_skb_or_xdp_view(page, offset, length)
    page_pool_release_page_only_when_last_consumer_is_done()
    refill_descriptor_with_a_pool_page()

close/reset:
    stop_new_DMA()
    drain_NAPI_and_deferred_releases()
    page_pool_destroy(rxq->pool)

Details differ by driver, but the ordering does not. Teardown must stop the device, prevent new completions, drain references and deferred work, and destroy the pool only after no path can return another page to it.

Why fragments complicate the picture

A 4 KiB page can hold several small packet buffers. Fragment mode reduces memory overhead and improves cache use, but one page can now represent multiple packet fragments with independent offsets and lengths. The page’s final release is later than any individual packet’s release.

Without fragmentsWith fragments
One packet commonly consumes one page.Several packet buffers may share one page.
Recycling follows that packet’s reference lifetime.Recycling waits for all fragments and references on the page.
More wasted tail room for small packets.More bookkeeping and more subtle refcount/offset bugs.

This is a throughput/complexity trade: fragments can reduce allocation and memory pressure, but a long-lived consumer can pin an entire page even if most of its fragments are idle.

Failure modes that look unrelated

SymptomLikely page-pool explanationEvidence to collect
RX allocation failures or hardware no-buffer countersPool is too small, pages are held by slow consumers, or recycle is not keeping up.Pool stats, RX ring refill failures, socket/zero-copy retention.
High CPU in allocator or IOMMU mapping codePages are falling out of the intended fast reuse path.Fast/slow recycle counters, IOMMU tracing, NUMA locality.
Rare packet corruption after load or resetDevice regained ownership before the last CPU reference ended, or teardown raced in-flight DMA.KASAN, DMA-API debugging, IOMMU faults, driver reset ordering.
Memory remains high after traffic stopsThe pool retains its working set by design, or a consumer/deferred release is still holding pages.Pool destroy/drain path, page refcounts, long-lived skb/XDP users.
Performance worsens after enabling XDP or zero-copyThose consumers extend page lifetime and can shift releases away from the NAPI CPU.Recycle context, XDP redirect completion, application buffer lifetime.

Most dangerous bug: a page is recycled while a consumer still owns a reference. The next NIC write then changes bytes under a reader. That is a data-lifetime violation, not merely a packet drop.

Security implications

Correct recycling is a confidentiality boundary. If a page is returned to a different consumer without the right initialization or ownership checks, stale packet contents can be exposed. If a device can DMA to a page after the kernel reallocates it, the result is a write primitive against unrelated kernel data. An IOMMU limits the device’s address space, but it does not repair a driver that reuses a valid mapping at the wrong time.

Reset and hot-unplug paths deserve the same scrutiny as the receive fast path. Stop DMA before freeing or repurposing pages, handle IOMMU faults as evidence of an ownership bug, and keep device-specific DMA addresses out of untrusted interfaces.

Debugging method

  1. Confirm the driver actually uses page pools: search its RX path for page_pool_create, page_pool_dev_alloc, and release helpers.
  2. Compare RX refill failures, NIC no-buffer counters, and packet loss with page-pool allocation/recycle statistics.
  3. Check whether releases happen on the NAPI CPU or through a deferred path. Unexpected cross-CPU releases often explain a “mysterious” slow path.
  4. For corruption, enable kernel debugging in a test system: KASAN, DMA-API debugging, lockdep, and IOMMU fault reporting. Reproduce with traffic plus resets, not only steady-state traffic.
  5. Inspect NUMA placement. A pool can be logically correct but expensive if the NIC, RX CPU, pool pages, and application are on different nodes.
# On a Linux test host; names and counters vary by driver/kernel
ethtool -S eth0 | grep -Ei 'rx|alloc|no.?buf|miss|drop|page'
grep -R "page_pool_" /usr/src/linux/drivers/net 2>/dev/null | head
cat /sys/class/net/eth0/device/numa_node
cat /proc/interrupts | grep -i eth0

Do not infer a leak from a nonzero pool size: retaining reusable pages is the point. Look for a rising number of pages that cannot be returned, a pool that cannot drain during close, or a mismatch between posted descriptors and pages with live references.

Compact reproducible lab

Goal

Trace the source-level lifecycle and verify where allocation, DMA mapping, fast recycling, deferred recycling, and destruction are implemented.

Prerequisites

Any Unix-like shell with curl, git, and rg. A Linux kernel source checkout is optional; the commands below fetch the authoritative files into a temporary directory.

Commands

LAB_DIR="$(mktemp -d)"
mkdir -p "$LAB_DIR/net"
curl -fsSL https://raw.githubusercontent.com/torvalds/linux/master/include/net/page_pool/types.h -o "$LAB_DIR/types.h"
curl -fsSL https://raw.githubusercontent.com/torvalds/linux/master/include/net/page_pool/helpers.h -o "$LAB_DIR/helpers.h"
curl -fsSL https://raw.githubusercontent.com/torvalds/linux/master/net/core/page_pool.c -o "$LAB_DIR/page_pool.c"

rg -n "struct page_pool|struct page_pool_params|page_pool_(create|destroy|put|recycle|alloc)|dma_sync|deferred" "$LAB_DIR"
printf '\n--- allocation and release call sites ---\n'
rg -n -C 3 "page_pool_(alloc|put|recycle|release)" "$LAB_DIR/helpers.h" "$LAB_DIR/page_pool.c"
printf '\n--- rough control-flow landmarks ---\n'
rg -n "page_pool_create|page_pool_destroy|page_pool_drain|page_pool_release_page" "$LAB_DIR/page_pool.c"

Observations

You should see the public types in types.h, hot helpers in helpers.h, and the slow-path/deferred logic in page_pool.c. The exact line numbers change as the kernel evolves; the important observation is that recycling is a two-speed design: a short fast path plus correctness-oriented draining and synchronization paths.

Revealing variations

First, remove helpers.h from the rg command and inspect only page_pool.c. This hides the inline hot path and makes the implementation look more allocator-like than it is. Second, repeat with a tagged kernel source tree instead of master; driver APIs and statistics have changed over time, so always match the source to the kernel you are debugging.

Live-kernel limitation: this lesson was prepared on macOS, so a Linux-only traffic experiment with ethtool, page-pool tracepoints, and a real NIC was not run here. On a Linux VM, use the same source inspection first, then run the diagnostic commands against a test interface; do not enable DMA debugging or force resets on production traffic.

Production example: a busy virtual machine host

Suppose a virtio or physical NIC shows intermittent RX no-buffer counters while average CPU is low. The misleading explanation is “the host has spare CPU.” A better investigation asks whether one queue is pinned to a busy CPU, whether pages are allocated on the wrong NUMA node, and whether XDP or socket consumers retain pages long enough to starve the pool. Increasing the RX ring may mask the burst, but it does not fix a page-lifetime or locality problem.

The practical decision is usually among three levers: improve ownership locality and release timing; size the pool/ring for the workload’s burst and retention window; or accept a copy/alternate buffer path for consumers that cannot release pages promptly. The fastest path is valuable only while its lifetime contract remains understandable.

Primary sources

Mental model: page_pool is a queue-local lifetime manager for DMA-capable packet pages. Its performance comes from reuse; its correctness comes from proving that the last reader is gone before the NIC becomes the next writer.