DPDK内存管理——mempool和mbuf的理解

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;
}

  • 8
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值