页面池 API

页面池分配器针对回收 skb 数据包和 xdp 帧使用的页面或页面片段进行了优化。

基本用法包括将任何 alloc_pages() 调用替换为 page_pool_alloc(),它会根据请求的内存大小分配带有或不带页面拆分的内存。

如果驱动程序知道它始终需要完整页面,或者其分配始终小于半个页面,则可以使用更具体的 API 调用之一

1. page_pool_alloc_pages(): 当驱动程序知道它需要的内存始终大于从页面池分配的页面的一半时,分配不进行页面拆分的内存。当页面被回收回页面池时,'struct page' 不会发生缓存行脏写。

2. page_pool_alloc_frag(): 当驱动程序知道它需要的内存始终小于或等于从页面池分配的页面的一半时,分配进行页面拆分的内存。页面拆分可以节省内存,从而避免数据访问的 TLB/缓存未命中,但实现页面拆分也有一些成本,主要是 'struct page' 的一些缓存行脏写/抖动和 page->pp_ref_count 的原子操作。

该 API 会跟踪正在使用的页面,以便让 API 用户知道何时可以安全地释放 page_pool 对象。API 用户必须调用 page_pool_put_page()page_pool_free_va() 来释放 page_pool 对象,或者将 page_pool 对象附加到 skbs 等页面池感知对象,这些对象使用 skb_mark_for_recycle() 进行标记。

如果一个页面被拆分为多个片段,则可以对同一页面多次调用 page_pool_put_page()。对于最后一个片段,它将回收该页面,或者在 page->_refcount > 1 的情况下,它将释放 DMA 映射和正在使用的状态计数。

仅当使用 PP_FLAG_DMA_SYNC_DEV 标志创建 page_pool 时,才会为最后一个片段调用 dma_sync_single_range_for_device()。因此,它取决于最后释放的片段来对同一页面中的所有片段执行 sync_for_device 操作。API 用户必须正确设置 pool->p.max_len 和 pool->p.offset,并确保为片段 API 调用 page_pool_put_page(),且 dma_sync_size 为 -1。

架构概述

+------------------+
|       Driver     |
+------------------+
        ^
        |
        |
        |
        v
+--------------------------------------------+
|                request memory              |
+--------------------------------------------+
    ^                                  ^
    |                                  |
    | Pool empty                       | Pool has entries
    |                                  |
    v                                  v
+-----------------------+     +------------------------+
| alloc (and map) pages |     |  get page from cache   |
+-----------------------+     +------------------------+
                                ^                    ^
                                |                    |
                                | cache available    | No entries, refill
                                |                    | from ptr-ring
                                |                    |
                                v                    v
                      +-----------------+     +------------------+
                      |   Fast cache    |     |  ptr-ring cache  |
                      +-----------------+     +------------------+

监控

可以通过 netdev genetlink 系列(参见 Documentation/netlink/specs/netdev.yaml)访问系统上有关页面池的信息。

API 接口

创建的池的数量**必须**与硬件队列的数量匹配,除非硬件限制使其无法实现。否则,这将违背页面池的目的,即从缓存快速分配页面而无需锁定。这种无锁保证自然来自于在 NAPI 软中断下运行。保护不一定必须是 NAPI,任何保证分配页面不会导致竞争条件的保证就足够了。

struct page_pool *page_pool_create(const struct page_pool_params *params)

创建一个页面池

参数

const struct page_pool_params *params

参数,请参见 struct page_pool_params

struct page_pool_params

页面池参数

定义:

struct page_pool_params {
    struct page_pool_params_fast  fast;
    unsigned int    order;
    unsigned int    pool_size;
    int nid;
    struct device   *dev;
    struct napi_struct *napi;
    enum dma_data_direction dma_dir;
    unsigned int    max_len;
    unsigned int    offset;
    struct page_pool_params_slow  slow;
    STRUCT_GROUP( struct net_device *netdev;
    unsigned int queue_idx;
    unsigned int    flags;
};

成员

fast

在热路径上频繁访问的参数

order

分配时 2^order 个页面

pool_size

ptr_ring 的大小

nid

从中分配页面的 NUMA 节点 ID

dev

设备,用于 DMA 预映射

napi

唯一使用页面的 NAPI,否则为 NULL

dma_dir

DMA 映射方向

max_len

PP_FLAG_DMA_SYNC_DEV 的最大 DMA 同步内存大小

offset

PP_FLAG_DMA_SYNC_DEV 的 DMA 同步地址偏移量

slow

仅具有慢路径访问的参数(初始化和 Netlink)

netdev

此池将服务的 netdev(如果没有或多个,则保留为 NULL)

queue_idx

为此创建 page_pool 的队列索引。

flags

PP_FLAG_DMA_MAP、PP_FLAG_DMA_SYNC_DEV、PP_FLAG_SYSTEM_POOL、PP_FLAG_ALLOW_UNREADABLE_NETMEM。

struct page *page_pool_dev_alloc_pages(struct page_pool *pool)

分配一个页面。

参数

struct page_pool *pool

从中分配的池

描述

从页面分配器或 page_pool 缓存中获取页面。

struct page *page_pool_dev_alloc_frag(struct page_pool *pool, unsigned int *offset, unsigned int size)

分配一个页面片段。

参数

struct page_pool *pool

从中分配的池

unsigned int *offset

已分配页面的偏移量

unsigned int size

请求的大小

描述

从页面分配器或 page_pool 缓存中获取页面片段。

返回

返回已分配的页面片段,否则返回 NULL。

struct page *page_pool_dev_alloc(struct page_pool *pool, unsigned int *offset, unsigned int *size)

分配一个页面或一个页面片段。

参数

struct page_pool *pool

从中分配的池

unsigned int *offset

已分配页面的偏移量

unsigned int *size

输入时为请求的大小,输出时为已分配的大小

描述

根据请求的大小从页面分配器或 page_pool 缓存中获取页面或页面片段,以便以最小的内存利用率和性能损失来分配内存。

返回

返回已分配的页面或页面片段,否则返回 NULL。

void *page_pool_dev_alloc_va(struct page_pool *pool, unsigned int *size)

分配一个页面或页面片段并返回其 va。

参数

struct page_pool *pool

从中分配的池

unsigned int *size

输入时为请求的大小,输出时为已分配的大小

描述

这只是 page_pool_alloc() API 的一个精简包装器,它返回已分配页面或页面片段的 va。

返回

返回已分配页面或页面片段的 va,否则返回 NULL。

enum dma_data_direction page_pool_get_dma_dir(const struct page_pool *pool)

检索存储的 DMA 方向。

参数

const struct page_pool *pool

从中分配页面的池

描述

获取存储的 DMA 方向。驱动程序可能会决定在本地存储此信息,并避免从 page_pool 中获取额外缓存行以确定方向。

void page_pool_put_page(struct page_pool *pool, struct page *page, unsigned int dma_sync_size, bool allow_direct)

释放对页面池页面的引用。

参数

struct page_pool *pool

从中分配页面的池

struct page *page

要释放引用的页面。

unsigned int dma_sync_size

设备可能已访问的页面大小。

bool allow_direct

由消费者释放,允许无锁缓存。

描述

结果取决于页面的引用计数 (refcnt)。如果驱动程序将引用计数增加到 > 1,则会取消映射该页面。如果页面引用计数为 1,则分配器拥有该页面,并将尝试在其中一个池缓存中回收它。如果设置了 PP_FLAG_DMA_SYNC_DEV,则将使用 dma_sync_single_range_for_device() 为设备同步页面。

void page_pool_put_full_page(struct page_pool *pool, struct page *page, bool allow_direct)

释放对页面池页面的引用。

参数

struct page_pool *pool

从中分配页面的池

struct page *page

要释放引用的页面。

bool allow_direct

由消费者释放,允许无锁缓存。

描述

类似于 page_pool_put_page(),但会按照 page_pool_params.max_len 中的配置,对整个内存区域进行 DMA 同步。

void page_pool_recycle_direct(struct page_pool *pool, struct page *page)

释放对页面池页面的引用。

参数

struct page_pool *pool

从中分配页面的池

struct page *page

要释放引用的页面。

描述

类似于 page_pool_put_full_page(),但调用者必须保证安全上下文(例如 NAPI),因为它会将页面直接回收进池的快速缓存中。

void page_pool_free_va(struct page_pool *pool, void *va, bool allow_direct)

将 va 释放到页面池中。

参数

struct page_pool *pool

va 从哪个池分配。

void *va

要释放的 va。

bool allow_direct

由消费者释放,允许无锁缓存。

描述

释放从 page_pool_allo_va() 分配的 va。

dma_addr_t page_pool_get_dma_addr(const struct page *page)

检索存储的 DMA 地址。

参数

const struct page *page

从页面池分配的页面。

描述

获取页面的 DMA 地址。该页面所属的页面池必须已使用 PP_FLAG_DMA_MAP 创建。

bool page_pool_get_stats(const struct page_pool *pool, struct page_pool_stats *stats)

获取页面池统计信息。

参数

const struct page_pool *pool

从中分配页面的池

struct page_pool_stats *stats

要填充的 struct page_pool_stats

描述

检索有关 page_pool 的统计信息。此 API 仅在内核已配置 CONFIG_PAGE_POOL_STATS=y 时可用。将一个指向调用者分配的 struct page_pool_stats 结构的指针传递给此 API,该结构将被填充。然后,调用者可以将这些统计信息报告给用户(可能通过 ethtool、debugfs 等)。

void page_pool_put_page_bulk(struct page_pool *pool, void **data, int count)

释放对多个页面的引用。

参数

struct page_pool *pool

页面从中分配的池。

void **data

保存页面指针的数组。

int count

data 中的页面数。

描述

尝试在持有 ptr_ring 生产者锁的 ptr_ring 缓存中重新填充多个页面。如果 ptr_ring 已满,则 page_pool_put_page_bulk() 会将剩余的页面释放到页面分配器。 page_pool_put_page_bulk() 适合在 XDP_REDIRECT 用例的驱动程序 NAPI tx 完成循环内运行。

请注意,在运行 page_pool_put_page_bulk() 后,调用者不得使用数据区域,因为此函数会覆盖它。

DMA 同步

驱动程序始终负责同步 CPU 的页面。驱动程序可以选择自行负责同步设备,也可以设置 PP_FLAG_DMA_SYNC_DEV 标志,以请求从页面池分配的页面已针对设备同步。

如果设置了 PP_FLAG_DMA_SYNC_DEV,则驱动程序必须告知核心缓冲区中必须同步的部分。这允许核心在驱动程序知道设备仅访问了页面的部分时避免同步整个页面。

大多数驱动程序会在帧前保留页眉空间。这部分缓冲区不会被设备访问,因此,为了避免同步它,驱动程序可以适当地设置 struct page_pool_params 中的 offset 字段。

对于在 XDP xmit 和 skb 路径上回收的页面,页面池将使用 struct page_pool_paramsmax_len 成员来决定需要同步多少页面(从 offset 开始)。当直接在驱动程序中释放页面 (page_pool_put_page()) 时,dma_sync_size 参数指定需要同步多少缓冲区。

如有疑问,请将 offset 设置为 0,将 max_len 设置为 PAGE_SIZE,并将 -1 作为 dma_sync_size 传递。这种参数组合始终正确。

请注意,同步参数适用于整个页面。当使用片段 (PP_FLAG_PAGE_FRAG) 时,请务必记住这一点,其中分配的缓冲区可能小于整页。除非驱动程序作者真正了解页面池的内部结构,否则建议始终在分段页面池中使用 offset = 0max_len = PAGE_SIZE

统计信息 API 和结构

如果内核配置了 CONFIG_PAGE_POOL_STATS=y,则可以使用 API page_pool_get_stats() 和下面描述的结构体。它接受一个指向 struct page_pool 的指针和一个指向调用者分配的 struct page_pool_stats 的指针。

较旧的驱动程序通过 ethtool 或 debugfs 公开页面池统计信息。相同的统计信息可以通过 netlink netdev 系列以独立于驱动程序的方式访问。

struct page_pool_alloc_stats

分配统计信息

定义:

struct page_pool_alloc_stats {
    u64 fast;
    u64 slow;
    u64 slow_high_order;
    u64 empty;
    u64 refill;
    u64 waive;
};

成员

fast

成功的快速路径分配

slow

慢速路径 0 阶分配

slow_high_order

慢速路径高阶分配

empty

指针环为空,因此强制执行慢速路径分配

refill

触发缓存重新填充的分配

waive

由于 NUMA 不匹配,从指针环获取的无法添加到缓存的页面

struct page_pool_recycle_stats

回收(释放)统计信息

定义:

struct page_pool_recycle_stats {
    u64 cached;
    u64 cache_full;
    u64 ring;
    u64 ring_full;
    u64 released_refcnt;
};

成员

cached

回收将页面放置到页面池缓存中

cache_full

页面池缓存已满

ring

页面放置到指针环中

ring_full

由于指针环已满,页面从页面池释放

released_refcnt

由于 refcnt > 1,页面被释放(而不是回收)

struct page_pool_stats

组合的页面池使用统计信息

定义:

struct page_pool_stats {
    struct page_pool_alloc_stats alloc_stats;
    struct page_pool_recycle_stats recycle_stats;
};

成员

alloc_stats

请参阅 struct page_pool_alloc_stats

recycle_stats

请参阅 struct page_pool_recycle_stats

描述

用于将页面池统计信息与不同存储需求相结合的包装结构。

代码示例

注册

/* Page pool registration */
struct page_pool_params pp_params = { 0 };
struct xdp_rxq_info xdp_rxq;
int err;

pp_params.order = 0;
/* internal DMA mapping in page_pool */
pp_params.flags = PP_FLAG_DMA_MAP;
pp_params.pool_size = DESC_NUM;
pp_params.nid = NUMA_NO_NODE;
pp_params.dev = priv->dev;
pp_params.napi = napi; /* only if locking is tied to NAPI */
pp_params.dma_dir = xdp_prog ? DMA_BIDIRECTIONAL : DMA_FROM_DEVICE;
page_pool = page_pool_create(&pp_params);

err = xdp_rxq_info_reg(&xdp_rxq, ndev, 0);
if (err)
    goto err_out;

err = xdp_rxq_info_reg_mem_model(&xdp_rxq, MEM_TYPE_PAGE_POOL, page_pool);
if (err)
    goto err_out;

NAPI轮询器

/* NAPI Rx poller */
enum dma_data_direction dma_dir;

dma_dir = page_pool_get_dma_dir(dring->page_pool);
while (done < budget) {
    if (some error)
        page_pool_recycle_direct(page_pool, page);
    if (packet_is_xdp) {
        if XDP_DROP:
            page_pool_recycle_direct(page_pool, page);
    } else (packet_is_skb) {
        skb_mark_for_recycle(skb);
        new_page = page_pool_dev_alloc_pages(page_pool);
    }
}

统计信息

#ifdef CONFIG_PAGE_POOL_STATS
/* retrieve stats */
struct page_pool_stats stats = { 0 };
if (page_pool_get_stats(page_pool, &stats)) {
        /* perhaps the driver reports statistics with ethool */
        ethtool_print_allocation_stats(&stats.alloc_stats);
        ethtool_print_recycle_stats(&stats.recycle_stats);
}
#endif

驱动程序卸载

/* Driver unload */
page_pool_put_full_page(page_pool, page, false);
xdp_rxq_info_unreg(&xdp_rxq);