前言
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的空间的办法。
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 // 标志位
);
参数详解
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
| name | const char * | 内存池名称,必须全局唯一 | "my_pool" |
| n | unsigned | 内存池中对象的数量 | 1024, 2048, 4096 |
| elt_size | unsigned | 每个对象的大小(字节) | 256, 512, 2048 |
| cache_size | unsigned | 每个 CPU 核心的本地缓存大小 | 0-512(推荐 0 或 256) |
| private_data_size | unsigned | 每个内存池的私有数据大小 | 通常为 0 |
| mp_init | 回调函数 | 内存池创建时的初始化回调 | 通常为 NULL |
| mp_init_arg | void * | 初始化回调的参数 | 通常为 NULL |
| obj_init | 回调函数 | 每个对象的初始化回调 | 通常为 NULL |
| obj_init_arg | void * | 对象初始化回调的参数 | 通常为 NULL |
| socket_id | int | NUMA 节点 ID | SOCKET_ID_ANY 或 rte_socket_id() |
| flags | unsigned | 标志位(见下表) | 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 采用桶内线性探测解决冲突,兼顾性能与实现复杂度:
-
映射到桶:键的哈希值通过取模运算(
hash_val % bucket_num)映射到对应的桶索引。 -
桶内查找:
- 首先在目标桶的第一个槽位匹配哈希高位和键,若匹配则命中。
- 若不匹配,在同一桶内按顺序探测下一个槽位(线性探测),直到找到匹配项或遍历完所有槽位。
-
冲突溢出处理:
- 若目标桶所有槽位均被占用(冲突满),则哈希表会预分配一定数量的溢出桶(overflow bucket) ,继续在溢出桶内探测。
- 溢出桶与主桶共享相同的哈希映射规则,避免全局遍历。
优势:桶内槽位连续存储,探测过程中可充分利用 CPU 缓存(一次缓存行加载可覆盖多个槽位),比链表法(易导致缓存失效)更高效。