dpdk-5. 内存池 流管理

76 阅读9分钟

前言

DPDK 内存池 会话管理

内存池

应用场景说明对象大小
数据包接收存储接收到的网络数据包2048 字节(mbuf)
会话表管理TCP/UDP 连接跟踪自定义结构体
流表OpenFlow 流表项自定义结构体
消息队列进程间通信消息结构体
对象缓存任何需要频繁分配/释放的对象任意大小

Mempool的基本概念

Mempool是固定大小的对象分配器。 在DPDK中,它由名称唯一标识,并且使用mempool操作来存储空闲对象。Mempool的组织是通过三个部分实现的:

  • mempool对象节点:mempool的对象挂接在 static struct rte_tailq_elem rte_mempool_tailq 全局队列中,可以通过名字进行唯一标识符;此队列只是mempool的一个对象指示结构,并不是实际的内存区;
  • mempool实际内存区: struct rte_memzone 是实际分配的连续内存空间,存储所创建的mempool对象;
  • ring无锁队列:作为一个无锁环形队列 struct rte_ring ,存储着mempool对象的指针,提供了方便存取使用mempool的空间的办法。

image.png

1 创建内存池

struct rte_mempool * rte_mempool_create(
    const char *name,              // 内存池名称(全局唯一)
    unsigned n,                    // 对象数量
    unsigned elt_size,             // 每个对象的大小(字节)
    unsigned cache_size,           // 每个核心的本地缓存大小
    unsigned private_data_size,    // 私有数据大小
    rte_mempool_ctor_t *mp_init,   // 内存池初始化回调
    void *mp_init_arg,             // 初始化回调参数
    rte_mempool_obj_cb_t *obj_init,// 对象初始化回调
    void *obj_init_arg,            // 对象初始化回调参数
    int socket_id,                 // NUMA 节点 ID
    unsigned flags                 // 标志位
);
参数详解
参数类型说明推荐值
nameconst char *内存池名称,必须全局唯一"my_pool"
nunsigned内存池中对象的数量1024, 2048, 4096
elt_sizeunsigned每个对象的大小(字节)256, 512, 2048
cache_sizeunsigned每个 CPU 核心的本地缓存大小0-512(推荐 0 或 256)
private_data_sizeunsigned每个内存池的私有数据大小通常为 0
mp_init回调函数内存池创建时的初始化回调通常为 NULL
mp_init_argvoid *初始化回调的参数通常为 NULL
obj_init回调函数每个对象的初始化回调通常为 NULL
obj_init_argvoid *对象初始化回调的参数通常为 NULL
socket_idintNUMA 节点 IDSOCKET_ID_ANYrte_socket_id()
flagsunsigned标志位(见下表)0(默认多生产者多消费者)
标志位详解
标志位说明使用场景性能影响
0(默认)多生产者多消费者(MP-MC)多线程环境标准性能
RTE_MEMPOOL_F_SP_PUT单生产者(Single Producer)只有一个线程 put 对象提升 put 性能 20-30%
RTE_MEMPOOL_F_SC_GET单消费者(Single Consumer)只有一个线程 get 对象提升 get 性能 20-30%
RTE_MEMPOOL_F_NO_CACHE_ALIGN不按缓存行对齐节省内存,但可能假共享可能降低性能
RTE_MEMPOOL_F_NO_SPREAD不在内存通道间分散不需要 NUMA 优化NUMA 系统性能下降

标志位组合示例:

// 单生产者单消费者(最高性能)
flags = RTE_MEMPOOL_F_SP_PUT | RTE_MEMPOOL_F_SC_GET;

// 单生产者多消费者
flags = RTE_MEMPOOL_F_SP_PUT;

// 多生产者单消费者
flags = RTE_MEMPOOL_F_SC_GET;

// 默认多生产者多消费者
flags = 0;
代码示例
#define MEMPOOL_SIZE (1024)       // 内存池大小
#define MEMPOOL_ELT_SIZE (256)    // 对象大小

// 创建基本内存池
struct rte_mempool *mp = rte_mempool_create(
    "test_mempool_basic",        // 内存池名称
    MEMPOOL_SIZE,                 // 对象数量:1024
    MEMPOOL_ELT_SIZE,             // 对象大小:256 字节
    0,                            // cache_size:0(无本地缓存)
    0,                            // private_data_size:0
    NULL, NULL,                   // 内存池初始化回调(不需要)
    NULL, NULL,                   // 对象初始化回调(不需要)
    SOCKET_ID_ANY,                // 任意 NUMA 节点
    0);                           // 标志位:默认(MP-MC)

if (mp == NULL) {
    printf("ERROR: create mempool failed\n");
    return -1;
}

printf("内存池创建成功!\n");

2 从内存池获取对象(Get)

DPDK 提供了三个获取对象的 API:

2.1 rte_mempool_get - 获取单个对象
// 获取单个对象(常用)
static inline int rte_mempool_get(
    struct rte_mempool *mp,    // 内存池指针
    void **obj_p               // 输出参数:返回对象指针
);

使用示例:

void *obj;
if (rte_mempool_get(mp, &obj) < 0) {
    printf("ERROR: 获取对象失败\n");
    return -1;
}
printf("成功获取对象:%p\n", obj);
2.2 rte_mempool_get_bulk - 批量获取对象
// 批量获取对象(高性能)
static inline int rte_mempool_get_bulk(
    struct rte_mempool *mp,    // 内存池指针
    void **obj_table,          // 对象指针数组
    unsigned int n             // 要获取的对象数量
);

使用示例:

// 批量获取 2 个对象
void *objects[2];
if (rte_mempool_get_bulk(mp, objects, 2) < 0) {
    printf("ERROR: bulk get objects failed\n");
    return -1;
}

void *obj1 = objects[0];
void *obj2 = objects[1];
printf("成功获取 2 个对象:%p, %p\n", obj1, obj2);
2.3 rte_mempool_generic_get - 通用获取函数
// 通用获取函数(最灵活)
int rte_mempool_generic_get(
    struct rte_mempool *mp,         // 内存池指针
    void **obj_table,               // 对象指针数组
    unsigned int n,                 // 要获取的对象数量
    struct rte_mempool_cache *cache // 缓存指针(通常为 NULL)
);

使用示例:

void *obj;
if (rte_mempool_generic_get(mp, &obj, 1, NULL) < 0) {
    printf("ERROR: get object failed\n");
    return -1;
}
printf("对象地址: %p\n", obj);

三个函数的对比:

函数使用场景性能灵活性
rte_mempool_get获取单个对象(最常用)
rte_mempool_get_bulk批量获取(推荐)最高
rte_mempool_generic_get需要自定义缓存最高

3 归还对象到内存池(Put)

3.1 rte_mempool_put - 归还单个对象
// 归还单个对象(常用)
static inline void rte_mempool_put(
    struct rte_mempool *mp,    // 内存池指针
    void *obj                  // 要归还的对象指针
);
3.2 rte_mempool_put_bulk - 批量归还对象
// 批量归还对象(高性能)
static inline void rte_mempool_put_bulk(
    struct rte_mempool *mp,    // 内存池指针
    void * const *obj_table,   // 对象指针数组
    unsigned int n             // 要归还的对象数量
);

使用示例:

// 批量归还 2 个对象
void *objects[2] = {obj1, obj2};
rte_mempool_put_bulk(mp, objects, 2);
printf("成功归还 2 个对象\n");
3.3.3 rte_mempool_generic_put - 通用归还函数
// 通用归还函数(最灵活)
void rte_mempool_generic_put(
    struct rte_mempool *mp,         // 内存池指针
    void * const *obj_table,        // 对象指针数组
    unsigned int n,                 // 要归还的对象数量
    struct rte_mempool_cache *cache // 缓存指针(通常为 NULL)
);

使用示例:

rte_mempool_generic_put(mp, &obj, 1, NULL);

4 查询内存池状态

4.1 获取可用对象数量
// 获取内存池中可用(空闲)对象的数量
unsigned int rte_mempool_avail_count(const struct rte_mempool *mp);
4.2 获取正在使用的对象数量
// 获取内存池中正在使用的对象数量
unsigned int rte_mempool_in_use_count(const struct rte_mempool *mp);

使用示例:

printf("可用对象数量: %d, 使用中对象数量: %d\n",
       rte_mempool_avail_count(mp),
       rte_mempool_in_use_count(mp));

5 释放内存池

// 释放内存池及其所有资源
void rte_mempool_free(struct rte_mempool *mp);

重要提示:

  • 释放内存池前,确保所有对象都已归还
  • 释放后不能再使用该内存池
  • 内存池名称可以被重新使用

使用示例:

rte_mempool_free(mp);
printf("内存池已释放\n");

流管理

// 流键:五元组
struct flow_key {
    uint32_t ip_src;      // 源 IP 地址
    uint32_t ip_dst;      // 目的 IP 地址
    uint16_t port_src;    // 源端口
    uint16_t port_dst;    // 目的端口
    uint8_t  proto;       // 协议类型
} __rte_packed;

// 流值:统计信息
struct flow_value {
    uint64_t packets;     // 数据包计数
    uint64_t bytes;       // 字节计数
};

hash api

  • rte_hash_create 创建
  • rte_hash_lookup_data 查询
  • rte_hash_add_key_data 添加
  • rte_hash_iterate 遍历hash

一、核心数据结构

DPDK hash 的核心数据结构是哈希表(struct rte_hash ,其底层由以下关键组件构成:

1. 桶数组(Bucket Array)

  • 哈希表的主体是一个连续的桶(bucket)数组,每个桶包含固定数量的槽位(slot) (默认 8 个,可配置)。
  • 桶和槽位均按缓存行(cache line)  对齐(通常 64 字节),避免跨缓存行访问导致的性能损耗。

2. 槽位(Slot)

每个槽位存储单个键值对的关键信息,结构精简以适配缓存:

struct rte_hash_slot {
    uint32_t hash;       // 键的哈希值高位(用于快速冲突检测)
    uint32_t key_idx;    // 键在全局键数组中的索引(变长键场景)
    uint64_t data;       // 存储值(通常是指针或整数)
    uint8_t key[0];      // 内嵌的固定长度键(短键优化,变长键不使用)
};
  • 哈希高位:仅存储哈希值的高位(如 24 位),用于快速排除不匹配的键,减少完整键比较的开销。
  • 键存储:短键(≤16 字节)直接内嵌在槽位中,长键则通过key_idx索引到全局键数组,平衡内存占用与访问速度。

3. 全局键数组(可选)

对于变长或长键(>16 字节),哈希表会额外分配一个连续的全局键数组,集中存储所有键,槽位通过key_idx指向对应位置,避免槽位因存储长键导致的内存碎片化。

二、哈希函数与哈希值计算

DPDK hash 的性能依赖高效的哈希函数,核心目标是计算速度快、哈希值分布均匀(减少冲突)。

1. 默认哈希函数:Jenkins hash(jhash)

  • 采用优化后的 Jenkins 哈希算法(rte_jhash),支持对多段数据(如网络五元组的源 IP、目的 IP、端口等)直接计算哈希,无需拼接成连续内存,减少数据拷贝。
  • 示例:对 TCP 五元组(src_ip, dst_ip, src_port, dst_port, protocol)计算哈希时,可直接传入各字段指针和长度,函数内部高效合并计算。

2. 自定义哈希函数

支持用户注册自定义哈希函数(通过rte_hash_parameters结构体配置),满足特殊场景(如硬件加速哈希、特定分布需求)。

三、冲突解决机制

哈希冲突(不同键映射到同一桶)是不可避免的,DPDK 采用桶内线性探测解决冲突,兼顾性能与实现复杂度:

  1. 映射到桶:键的哈希值通过取模运算(hash_val % bucket_num)映射到对应的桶索引。

  2. 桶内查找

    • 首先在目标桶的第一个槽位匹配哈希高位和键,若匹配则命中。
    • 若不匹配,在同一桶内按顺序探测下一个槽位(线性探测),直到找到匹配项或遍历完所有槽位。
  3. 冲突溢出处理

    • 若目标桶所有槽位均被占用(冲突满),则哈希表会预分配一定数量的溢出桶(overflow bucket) ,继续在溢出桶内探测。
    • 溢出桶与主桶共享相同的哈希映射规则,避免全局遍历。

优势:桶内槽位连续存储,探测过程中可充分利用 CPU 缓存(一次缓存行加载可覆盖多个槽位),比链表法(易导致缓存失效)更高效。