DPDK针对高性能pcie设备,设计了专门的mempool内存管理模型,具体的数据结构形式是rte_mempool和rte_mbuf。
在mempool的实现中,每个elem称为mbuf,驱动从mempool中申请每个mbuf使用。每个mempool在创建的时候指定了mbuf的个数和大小,每个mbuf大小是一样的,所以mempool占用多少空间在申请的时候就是确定的,这种模式不同于heap的动态扩展。在mempool申请的空间里,所有的mbuf依次排列,具体的内存拓扑及软件组织形式见下图。
一、mempool和memzone、memsg
一般从性能的角度考虑,DPDK会启用大页(x64支持4KB\2MB\1GB的页表项),选择IOVA=PA的模式。因为经过iommu进行地址映射必然会引起性能的下降,所以不经过iommu或者iommu=pt避免pcie设备DMA的性能下降。而选择PA模式后,因为设备直接访问物理地址,所以要求每个mbuf的空间都不可以跨越不连续的物理页面,要避开页面的边界,每个页面最后都有一些空间不可以利用,所以每个页面越大越好;同时还要考虑到页面的换入换出,设备DMA会访问的物理页面不允许被换出到硬盘。我们实际使用了1GB的页面,预分配了8*1GB的空间,预分配的空间使用单独的内存管理机制,不参与内核的物理页面换入换出。
可以简单的理解为,mempool是在memzone基础上的。每个memzone针对一片连续的物理空间,比如mempool里的buf需要4GB的空间,那么至少需要4个1GB的大页才可以实现,也就需要至少4个memzone来管理。memzone通过rte_mempool_memhdr的结构,通过链表的形式组织起来挂到mempool的mem_list变量。
memzone就是标志了一块连续的物理空间,每个mempool可以包含多个memzone。关于memzone是如何申请和管理的,可以参照下面的结构图,在dpdk初始化的时候,调用eal_hugepage_info_init/read()接口对获取全部的大页内存并初始化,将物理地址连续的页面组成一个单元,记录到rte_config.mem_config->memseg[]里。申请memzone内存空间的时候就从memseg结构里获取可用的内存使用。关于memseg和memzone的创建、使用后续单独介绍。
二、rte_mbuf初始化
因为mempool中每个rte_mbuf的大小是固定的,所以rte_mbuf的结构在mempool创建的时候就都初始化好了。rte_mbuf结构体的定义如下,省略了很多不需要考虑的部分,重点关注地址相关的内容,其中buf_addr是虚拟地址,buf_iova是给设备用的地址,在iova_mod=PA的情况,buf_iova就是物理地址。
/**
* The generic rte_mbuf, containing a packet mbuf.
*/
struct rte_mbuf {
void *buf_addr; /**< Virtual address of segment buffer. */
/**
* Physical address of segment buffer.
* Force alignment to 8-bytes, so as to ensure we have the exact
* same mbuf cacheline0 layout for 32-bit and 64-bit. This makes
* working on vector drivers easier.
*/
rte_iova_t buf_iova __rte_aligned(sizeof(rte_iova_t));
uint16_t data_off;
uint16_t nb_segs; /**< Number of segments. */
uint32_t pkt_len; /**< Total pkt len: sum of all segments. */
uint16_t data_len; /**< Amount of data in segment buffer. */
uint16_t buf_len; /**< Length of segment buffer. */
/* second cache line - fields only used in slow path or on TX */
RTE_MARKER cacheline1 __rte_cache_min_aligned;
struct rte_mbuf *next; /**< Next segment of scattered packet. */
} __rte_cache_aligned;
具体的rte_mbuf初始化动作是在rte_pktmbuf_init()函数中完成的。首先rte_pktmbuf_init是在rte_mempool_obj_iter()中调用执行的。显然这是一个迭代函数,会依次初始化mempool中所有的mbuf。
看一下rte_pktmbuf_init函数的传入参数,每个元素占用的所有空间如下,_m参数传入的是rte_mbuf的起始地址。
所以代码里直接使用struct rte_mbuf *m = _m,然后对m进行赋值。
m->buf_addr=为m + sizeof(rte_mbuf) + priv_size,就是真正的mbuf存储空间的地址。
m->buf_iova = rte_mempool_virt2iova(m) + sizeof(rte_mbuf) + priv_size。
void rte_pktmbuf_init(struct rte_mempool *mp,
__rte_unused void *opaque_arg,
void *_m,
__rte_unused unsigned i)
{
struct rte_mbuf *m = _m;
uint32_t mbuf_size, buf_len, priv_size;
priv_size = rte_pktmbuf_priv_size(mp);
mbuf_size = sizeof(struct rte_mbuf) + priv_size;
buf_len = rte_pktmbuf_data_room_size(mp);
RTE_ASSERT(RTE_ALIGN(priv_size, RTE_MBUF_PRIV_ALIGN) == priv_size);
RTE_ASSERT(mp->elt_size >= mbuf_size);
RTE_ASSERT(buf_len <= UINT16_MAX);
memset(m, 0, mbuf_size);
/* start of buffer is after mbuf structure and priv data */
m->priv_size = priv_size;
m->buf_addr = (char *)m + mbuf_size;
m->buf_iova = rte_mempool_virt2iova(m) + mbuf_size;
m->buf_len = (uint16_t)buf_len;
/* keep some headroom between start of buffer and data */
m->data_off = RTE_MIN(RTE_PKTMBUF_HEADROOM, (uint16_t)m->buf_len);
/* init some constant fields */
m->pool = mp;
m->nb_segs = 1;
m->port = RTE_MBUF_PORT_INVALID;
rte_mbuf_refcnt_set(m, 1);
m->next = NULL;
}
1、buf_iova如何配置
展开rte_mempool_virt2iova(m)接口,其实它的iova值直接取了相应的rte_mempool_objhdr里的iova。所以每个mbuf的iova是由前面而objhdr头里的iova来的。而最早是在哪里初始化的,往下看。
static inline rte_iova_t rte_mempool_virt2iova(const void *elt)
{
const struct rte_mempool_objhdr *hdr;
hdr = (const struct rte_mempool_objhdr *)RTE_PTR_SUB(elt,
sizeof(*hdr));
return hdr->iova;
}
在第1章讲了rte_memseg和rte_memzone。可以知道,rte_memseg是最底层的内存管理单元。在dpdk初始化的时候,会首先根据huge page及其在物理内存的分布初始化rte_config.mem_config->memseg[]。因为每个memseg记录一块连续的物理内存,所以每个memseg的起始物理地址就是iova。在memseg分配的时候,是通过rte_mem_virt2iova(addr)来赋值iova的。在这个接口里,iova_mode=PA的情况,iova进而通过rte_mem_virt2phy(virtaddr)来获取,也就是直接获取了物理地址。
下面的代码中展开了rte_mem_virt2phy()函数,给我们展示了dpdk作为一个用户态进程是如何获取物理地址的。
在每个进程目录下有一个pagemap文件(/proc/self/pagemap),这个文件可以依次读取所有的虚拟页面对应的物理页帧号。通过fseek(通知内核是哪个虚拟地址)和read接口(获取虚拟地址对应的物理页帧),可以精准的获取到物理地址。显然这是一个很危险的动作,直接暴露出了物理地址,所以必须要求root权限才可以执行。如果不具备root权限,就不能使用iova-mode=PA模式。
iova = rte_mem_virt2iova(addr);
rte_iova_t rte_mem_virt2iova(const void *virtaddr)
{
if (rte_eal_iova_mode() == RTE_IOVA_VA)
return (uintptr_t)virtaddr;
return rte_mem_virt2phy(virtaddr);
}
phys_addr_t rte_mem_virt2phy(const void *virtaddr)
{
int fd, retval;
uint64_t page, physaddr;
unsigned long virt_pfn;
int page_size;
off_t offset;
if (phys_addrs_available == 0)
return RTE_BAD_IOVA;
/* standard page size */
page_size = getpagesize();
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
RTE_LOG(INFO, EAL, "%s(): cannot open /proc/self/pagemap: %s\n",
__func__, strerror(errno));
return RTE_BAD_IOVA;
}
virt_pfn = (unsigned long)virtaddr / page_size;
offset = sizeof(uint64_t) * virt_pfn;
if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
RTE_LOG(INFO, EAL, "%s(): seek error in /proc/self/pagemap: %s\n",
__func__, strerror(errno));
close(fd);
return RTE_BAD_IOVA;
}
retval = read(fd, &page, PFN_MASK_SIZE);
close(fd);
if (retval < 0) {
RTE_LOG(INFO, EAL, "%s(): cannot read /proc/self/pagemap: %s\n",
__func__, strerror(errno));
return RTE_BAD_IOVA;
} else if (retval != PFN_MASK_SIZE) {
RTE_LOG(INFO, EAL, "%s(): read %d bytes from /proc/self/pagemap "
"but expected %d:\n",
__func__, retval, PFN_MASK_SIZE);
return RTE_BAD_IOVA;
}
/*
* the pfn (page frame number) are bits 0-54 (see
* pagemap.txt in linux Documentation)
*/
if ((page & 0x7fffffffffffffULL) == 0)
return RTE_BAD_IOVA;
physaddr = ((page & 0x7fffffffffffffULL) * page_size)
+ ((unsigned long)virtaddr % page_size);
return physaddr;
}
然后从memsg中分配memzone的时候,根据当前memsg的iova以及memzone内存的相对便宜,来配置memzone的iova.
在create_mempool()会调用rte_mempool_populate_default()接口,这个接口做了哪些工作。
1)循环申请memzone,直到可以容纳n个mbuf空间,申请memzone的时候配置了iova。
2)每个memzone执行rte_mempool_populate_iova()接口,这个接口对memzone里的mbuf进行了populate,初始化每个mbuf头部的rte_mempool_objhdr结构体,根据每个mbuf相对于memzone的偏移+(memzone->addr/iova)赋值虚拟地址和iova。 具体的实现在memory_add_elem()接口中。
上面的流程比较复杂,用一张图来说明。
三、申请mbuf
rte_mempool_calc_obj_size这个函数,是计算每个rte_mbuf需要的空间,这个函数返回的是一个rte_mempool_objsz结构体,其中elt_size是每个元素需要的内存空间(包括rte_mbuf、priv_size、提供给用户使用的内存空间size),header_size是elt之前的空间大小,rte_mempool为了管理每个元素,在每个元素之前都安排了一个rte_mempool_objhdr结构体;trailer_size是elt之后的空间,在设置了cache对齐的flag时,用于将(header_size+elt_size+trailer_size)补齐cache size的倍数大小。这三个size的和记录在total_size,就是在mempool管理机制里,每个元素需要的实际的内存size。
图一中,第二列就说明了这个内存拓扑结构,可以看到每个单元包括rte_mempool_objhdr\mbuf和aligned space。
所以,mempool需要为mbuf申请的内存空间是total_size*mp->size,(mp->size是mbuf的个数)。rte_mempool_objsz的值都记录到了mp的相应字段中。
struct rte_mempool_objsz {
uint32_t elt_size; /**< Size of an element. */
uint32_t header_size; /**< Size of header (before elt). */
uint32_t trailer_size; /**< Size of trailer (after elt). */
uint32_t total_size;
/**< Total size of an object (header + elt + trailer). */
};
struct rte_mempool {
uint32_t size; /**< Max size of the mempool. */
uint32_t elt_size; /**< Size of an element. */
uint32_t header_size; /**< Size of header (before elt). */
uint32_t trailer_size; /**< Size of trailer (after elt). */
} __rte_cache_aligned;
所有的空闲单元通过rte_mempool_objhdr里的next指针链接起来,挂接到rte_mempool的elt_list结构里进行组织管理。
当驱动从mempool中申请mbuf空间时,使用rte_pktmbuf_alloc接口。该接口最终调用到了rte_mempool_get_bulk,其中obj_p返回的是rte_mbuf的指针。
static __rte_always_inline int
rte_mempool_get_bulk(struct rte_mempool *mp, void **obj_table, unsigned int n)
{
struct rte_mempool_cache *cache;
cache = rte_mempool_default_cache(mp, rte_lcore_id());
rte_mempool_trace_get_bulk(mp, obj_table, n, cache);
return rte_mempool_generic_get(mp, obj_table, n, cache);
}
//rte_mempool_default_cache这个接口返回的是 mp->local_cache[lcore_id]
rte_mempool_default_cache()接口返回了mp->local_cache[lcore_id],也就是mempool在每个cpu上都有一个cache,这个cache里暂存刚刚释放的mbuf元素,申请的时候从cache里申请。这是从cache的利用率和性能角度考虑,类似内核的内存管理机制中的hot页面。rte_mempool_generic_get()又调用了__mempool_generic_get()接口,这个接口里给obj_table赋值。首先判断cache里的有效obj个数如果大于待申请的mbuf个数,则直接从cache->objs获取并赋值给obj_table。
static __rte_always_inline int
__mempool_generic_get(struct rte_mempool *mp, void **obj_table,
unsigned int n, struct rte_mempool_cache *cache)
{
int ret;
uint32_t index, len;
void **cache_objs;
/* No cache provided or cannot be satisfied from cache */
if (unlikely(cache == NULL || n >= cache->size))
goto ring_dequeue;
cache_objs = cache->objs;
/* Can this be satisfied from the cache? */
if (cache->len < n) {
/* No. Backfill the cache first, and then fill from it */
uint32_t req = n + (cache->size - cache->len);
/* How many do we require i.e. number to fill the cache + the request */
ret = rte_mempool_ops_dequeue_bulk(mp,
&cache->objs[cache->len], req);
if (unlikely(ret < 0)) {
/*
* In the off chance that we are buffer constrained,
* where we are not able to allocate cache + n, go to
* the ring directly. If that fails, we are truly out of
* buffers.
*/
goto ring_dequeue;
}
cache->len += req;
}
/* Now fill in the response ... */
for (index = 0, len = cache->len - 1; index < n; ++index, len--, obj_table++)
*obj_table = cache_objs[len];
cache->len -= n;
__MEMPOOL_STAT_ADD(mp, get_success, n);
return 0;
ring_dequeue:
/* get remaining objects from ring */
ret = rte_mempool_ops_dequeue_bulk(mp, obj_table, n);
if (ret < 0)
__MEMPOOL_STAT_ADD(mp, get_fail, n);
else
__MEMPOOL_STAT_ADD(mp, get_success, n);
return ret;
}