rte_mem_config 结构
dpdk 多进程共享内存机制依赖 rte_mem_config 文件共享内存描述信息,其中有如下结构定义:
struct rte_tailq_head tailq_head[RTE_MAX_TAILQ]; /**< Tailqs for objects */
此结构共享 tailq_head,用以支持在多进程之间查找特定类型的数据结构地址,本文以 rte_ring 为例描述下其中的原理。
rte_tailq_head 结构
rte_tailq_head 是 dpdk 中双向链表的表头,dpdk 中为多个功能不同的内存结构注册不同的 tailq 链表用于多进程间共享。此结构定义如下:
struct rte_tailq_head {
struct rte_tailq_entry_head tailq_head; /**< NOTE: must be first element */
char name[RTE_TAILQ_NAMESIZE];
};
rte_mem_config 中保存所有的 rte_tailq_head 信息,每一个 rte_tailq_head 的 name 唯一标识一个 tailq。 dpdk 通过共享 rte_tailq_head 头并在大页上分配 rte_tailq_head 链表中链入的每一个 rte_tailq_entry 来实现多进程间基于 tailq 获取共享内存信息功能。
核心数据结构为 rte_tailq_elem 与 rte_tailq_entry,其定义如下:
struct rte_tailq_elem {
/**
* Reference to head in shared mem, updated at init time by
* rte_eal_tailqs_init()
*/
struct rte_tailq_head *head;
TAILQ_ENTRY(rte_tailq_elem) next;
const char name[RTE_TAILQ_NAMESIZE];
};
struct rte_tailq_entry {
TAILQ_ENTRY(rte_tailq_entry) next; /**< Pointer entries for a tailq list */
void *data; /**< Pointer to the data referenced by this tailq entry */
};
核心过程如下:
-
不同模块通过调用 EAL_REGISTER_TAILQ 宏来注册不同的本地 rte_tailq_elem 结构到本地 rte_tailq_elem_head 表头中,此宏会定义一个构造函数在 main 函数之前执行,完成注册过程,注册时 rte_tailq_elem 的 head 字段为 NULL。
-
在内存初始化完成后,dpdk 为每一个注册的 rte_tailq_elem 结构在 rte_mem_config 分配一个 rte_tailq_head 并更新相关数据结构
primary 进程执行的操作如下:在 rte_mem_config 的 tailq_head 数组中为每一个注册的 rte_tailq_elem 结构分配一个 rte_tailq_head 链表头,并将 rte_tailq_elem 中的 name 字段拷贝到分配出的 rte_tailq_head 结构的 name 字段中,然后更新 rte_tailq_elem 的 head 字段为分配的 rte_tailq_head 地址。
secondary 进程执行的操作如下:
遍历每个注册的 rte_tailq_elem 结构,使用此结构的 name 字段在 rte_mem_config 共享内存的 tailq_head 数组中匹配,找到匹配到的 tailq_head 结构并更新 rte_tailq_elem 的 head 字段.
-
每种类型共享数据在创建时,在大页内存上分配一个 rte_tailq_entry 结构,此结构以 name 字符串标识每个分配的结构,将此结构的 data 字段指向创建的共享数据起始地址,然后链入到链表中。
-
需要在其它进程中获取基于 tailq 描述的内存结构时,获取到本地 rte_tailq_elem 中的 rte_tailq_head 地址并强转为一个 rte_tailq_entry_head 结构,然后使用名称遍历此链表的每个结构进行匹配,匹配到后返回 rte_tailq_entry 的 data 字段中存储的数据地址即可。
以 rte_ring 结构为例描述实现细节
rte_ring.c 中定义了如下 tailq 相关信息:
TAILQ_HEAD(rte_ring_list, rte_tailq_entry);
static struct rte_tailq_elem rte_ring_tailq = {
.name = RTE_TAILQ_RING_NAME,
};
EAL_REGISTER_TAILQ(rte_ring_tailq)
TAILQ_HEAD 定义了一个 rte_ring_list 类型链表头,链表的每一个成员数据类型为 rte_tailq_entry。rte_ring_tailq 为本地定义的一个 rte_tailq_elem, name 填充 rte_ring 的特定名称——“RTE_RING”,调用 RTE_REGISTER_TAILQ 创建一个构造函数将 rte_tailq_elem 注册到 rte_tailq_elem_head 链表中。
在 rte_eal_init 函数调用完成后,本地 rte_ring_tailq 变量的 head 字段指向 rte_mem_config 的 tailq_head 数组中的一项。
rte_ring_create 函数主要逻辑如下:
struct rte_ring *
rte_ring_create(const char *name, unsigned count, int socket_id,
unsigned flags)
{
char mz_name[RTE_MEMZONE_NAMESIZE];
struct rte_ring *r;
struct rte_tailq_entry *te;
const struct rte_memzone *mz;
ssize_t ring_size;
int mz_flags = 0;
struct rte_ring_list* ring_list = NULL;
..............................................................
ring_list = RTE_TAILQ_CAST(rte_ring_tailq.head, rte_ring_list);
.............................................................
ret = snprintf(mz_name, sizeof(mz_name), "%s%s",
RTE_RING_MZ_PREFIX, name);
...............................................................
te = rte_zmalloc("RING_TAILQ_ENTRY", sizeof(*te), 0);
..............................................................
mz = rte_memzone_reserve_aligned(mz_name, ring_size, socket_id,
mz_flags, __alignof__(*r));
if (mz != NULL) {
r = mz->addr;
/* no need to check return value here, we already checked the
* arguments above */
rte_ring_init(r, name, requested_count, flags);
te->data = (void *) r;
r->memzone = mz;
TAILQ_INSERT_TAIL(ring_list, te, next);
}
.............................................................
}
name 参数标识此 rte_ring 的名称,rte_ring_create 函数的主要逻辑如下:
- 访问 rte_ring_elem 的 head 字段并强转为一个 rte_ring_list head 类型地址,保存为 ring_list
- 格式化 name 并保存到 mz_name 中,然后在大页内存中申请一个 rte_tailq_entry 结构,结构地址保存在 te 变量中
- 为 rte_ring 预留空间,得到一个 memzone 区域,调用 rte_ring_init 初始化此区域
- 将 te 变量的 data 字段赋值为 memzone 区域的地址以保存 rte_ring 的地址
- 将 te 插入到 ring_list 指向的链表中
rte_ring_lookup 函数的主要代码如下:
struct rte_ring *
rte_ring_lookup(const char *name)
{
struct rte_tailq_entry *te;
struct rte_ring *r = NULL;
struct rte_ring_list *ring_list;
ring_list = RTE_TAILQ_CAST(rte_ring_tailq.head, rte_ring_list);
.............................................................
TAILQ_FOREACH(te, ring_list, next) {
r = (struct rte_ring *) te->data;
if (strncmp(name, r->name, RTE_RING_NAMESIZE) == 0)
break;
}
.............................................................
}
主要逻辑如下:
- 获取 rte_ring_list head 地址
- 遍历 rte_ring_list 中的所有 rte_tailq_entry,获取每个 rte_tailq_entry 的 data 数据,此数据指向一个 rte_ring 结构,使用传入的 name 参数与 rte_ring 结构的 name 字段进行匹配,命中则返回 rte_ring 结构
为什么需要使用 rte_tailq_head 共享内存?
在 dpdk 多进程共享内存描述信息的机制 这篇文章中,我描述了 dpdk 中用于多进程间共享内存的方法,主要包含如下两种:
- memzone
- rte_tailq_head
memzone 这种实现中,在共享内存描述文件 rte_mem_config 中分配 memzone 结构,能够分配的数量在编译时就确定了,不能动态的扩展,当使用这种机制分配大量特定类型的数据结构时,很容易耗尽全局 memzone 结构导致分配失败,这种机制适合分配数量有限特定数据类型的场景。
而 rte_tailq_head 这种机制却没有这种限制,在单个 rte_tailq_head 维持的链表中,在内存充足时可以持续分配特定类型的数据结构,适用于需要大量分配某种特定类型共享数据结构的场景。