DPDK-内存管理分析一

前言

《DPDK-大页内存使用分析》中粗略分析了DPDK获取hugepage配置和内存映射的流程,并提到保存了相关信息在全局的memseg数组中。到此为止,DPDK相关进程就可以像使用普通内存一样使用这些hugepage。但是具体如何使用,还需要进一步的分析DPDK的内存管理结构。

另:落笔之前,并未仔细研读过DPDK Programming Guide中关于内存管理的描述。曾粗粗浏览过,印象并不深刻,此文全凭个人对DPDK源码的理解及猜想写成,个中缺漏及谬误望指正。

DPDK Version: 17.11.2

Date: 2018-06-18, Created by HRG

正文

首先,猜测DPDK的内存管理基础模块就是memzone模块。

在eal.c rte_eal_init()中调用了memzone的初始化函数rte_eal_memzone_init() 。

	if (rte_eal_memzone_init() < 0) {
		rte_eal_init_alert("Cannot init memzone\n");
		rte_errno = ENODEV;
		return -1;
	}

rte_eal_memzone_init()前把memzone clear掉了,最后返回rte_eal_malloc_heap_init()的调用结果。

int
rte_eal_memzone_init(void)
{
	rte_rwlock_write_lock(&mcfg->mlock);
	/* delete all zones */
	mcfg->memzone_cnt = 0;
	memset(mcfg->memzone, 0, sizeof(mcfg->memzone));
	rte_rwlock_write_unlock(&mcfg->mlock);

	return rte_eal_malloc_heap_init();
}

我们首先着重看下这个函数最后调用的rte_eal_malloc_heap_init(),这里针对已经整理排序好的所有memseg循环调用malloc_heap_add_memseg,将memseg添加到全局的malloc_heaps数组中去。对于NUMA系统来说,有多少个NUMA节点这个malloc_heaps数组就有多少个成员,每个成员都管理着对应NUMA节点的memseg。

int
rte_eal_malloc_heap_init(void)
{
	for (ms = &mcfg->memseg[0], ms_cnt = 0;
			(ms_cnt < RTE_MAX_MEMSEG) && (ms->len > 0);
			ms_cnt++, ms++) {
		malloc_heap_add_memseg(&mcfg->malloc_heaps[ms->socket_id], ms);
	}
	return 0;
};

我们来看看这个malloc_heaps结构体细节,其中free_head是一个有13个成员的数组,此处使用了Linux下的系统库sys/queue.h进行定义,heap中的free_head对应每一个queue的起点,元素的结构体是malloc_elem。即malloc_heaps下总共有RTE_HEAP_NUM_FREELISTS=13个成员是malloc_elem的队列。

struct malloc_heap {
	rte_spinlock_t lock;
	LIST_HEAD(, malloc_elem) free_head[RTE_HEAP_NUM_FREELISTS];
	unsigned alloc_count;
	size_t total_size;
} __rte_cache_aligned;

对于heap来说,上面每次调用malloc_heap_add_memseg时,就会将每个memseg整理成malloc_elem的形式并放到合适的队列中。每个队列对应于一定范围大小的memseg,比如下面这样的布局:

 * Example element size ranges for a heap with five free lists:
 *   heap->free_head[0] - (0   , 2^8]
 *   heap->free_head[1] - (2^8 , 2^10]
 *   heap->free_head[2] - (2^10 ,2^12]
 *   heap->free_head[3] - (2^12, 2^14]
 *   heap->free_head[4] - (2^14, MAX_SIZE]

接下来看malloc_heap_add_memseg()怎么将memseg整理成malloc_elem。首先取memseg指向的大页共享内存的起始地址和cache line对齐后的结束地址,计算对齐后的memseg长度。然后调用malloc_elem_init和malloc_elem_mkend进行初始化,在memseg头部和尾部填写相关的指针信息和cookie,至此,这个memseg就可以用start_elem指针代替了,对应的这块内存就归属于这个malloc_elem。最后调用malloc_elem_free_list_insert将这个elem插入到合适的heap下的13个队列之一,具体哪个队列就要看memseg的长度了。

static void
malloc_heap_add_memseg(struct malloc_heap *heap, struct rte_memseg *ms)
{
	/* allocate the memory block headers, one at end, one at start */
	struct malloc_elem *start_elem = (struct malloc_elem *)ms->addr;
	struct malloc_elem *end_elem = RTE_PTR_ADD(ms->addr,
			ms->len - MALLOC_ELEM_OVERHEAD);
	end_elem = RTE_PTR_ALIGN_FLOOR(end_elem, RTE_CACHE_LINE_SIZE);
	const size_t elem_size = (uintptr_t)end_elem - (uintptr_t)start_elem;

	malloc_elem_init(start_elem, heap, ms, elem_size);
	malloc_elem_mkend(end_elem, start_elem);
	malloc_elem_free_list_insert(start_elem);

	heap->total_size += elem_size;
}

我们看看malloc_elem结构体的内容就知道memseg头部和尾部大概填写的是什么内容了。

struct malloc_elem {
	struct malloc_heap *heap;
	struct malloc_elem *volatile prev;      /* points to prev elem in memseg */
	LIST_ENTRY(malloc_elem) free_list;      /* list of free elements in heap */
	const struct rte_memseg *ms;
	volatile enum elem_state state;
	uint32_t pad;
	size_t size;
#ifdef RTE_MALLOC_DEBUG
	uint64_t header_cookie;         /* Cookie marking start of data */
	                                /* trailer cookie at start + size */
#endif
} __rte_cache_aligned;

通过以上分析,可以得到大概的DPDK内存管理结构了:

1、在NUMA系统中,每一个NUMA节点都有对应的heap,当然,非NUMA系统就只有一个heap了。(这个名字取得很好,程序在动态申请内存时一般是在进程内存结构中的heap中去取的,现在,DPDK自己去管理这些大页内存以供后续程序动态申请,所以也叫做heap)。

2、一个heap对应13个队列,每个队列管理着一定大小范围内的连续的内存块。

3、猜想:程序动态申请内存时,将会根据申请的内存大小,到对应的heap下的内存队列中寻找相应的空闲态内存块返回给程序;程序free时,将内存块整理好继续放在队列中。

为印证以上第3点猜想,继续研究malloc_heap.c/.h文件中的其他函数。

首先调用find_suitable_element()从heap中寻找合适的queue并返回合适的element。最后调用malloc_elem_alloc初始化相关的element。

void *
malloc_heap_alloc(struct malloc_heap *heap,
		const char *type __attribute__((unused)), size_t size, unsigned flags,
		size_t align, size_t bound)
{
	rte_spinlock_lock(&heap->lock);

	elem = find_suitable_element(heap, size, flags, align, bound);
	if (elem != NULL) {
		elem = malloc_elem_alloc(elem, size, align, bound);
		heap->alloc_count++;
	}
	rte_spinlock_unlock(&heap->lock);

	return elem == NULL ? NULL : (void *)(&elem[1]);
}

调用malloc_elem_free_list_index()确定要在heap的哪个queue中去取elem,这里注意for循环,queue本身idx递增就表明了queue所管理的内存块长度范围是递增的。for循环一开始,先找个能容纳用户malloc需求的最小的queue,当这个queue内存用尽或者剩下的内存不足以满足时,便往下一个更大的queue找去。

static struct malloc_elem *
find_suitable_element(struct malloc_heap *heap, size_t size,
		unsigned flags, size_t align, size_t bound)
{
	for (idx = malloc_elem_free_list_index(size);
			idx < RTE_HEAP_NUM_FREELISTS; idx++) {
		for (elem = LIST_FIRST(&heap->free_head[idx]);
				!!elem; elem = LIST_NEXT(elem, free_list)) {
			if (malloc_elem_can_hold(elem, size, align, bound)) {
				if (check_hugepage_sz(flags, elem->ms->hugepage_sz))
					return elem;
			}
		}
	}
}

找到能满足size和align需求之后,还要怎样操作elem?因为elem可能是一块连续的很大的内存,远远超出用户需求的size的,因此后面肯定还会继续处理elem以便更高效地使用elem所代表的内存块。继续看malloc_elem_alloc这个函数针对这个疑问做了什么处理。

首先,根据size和align在elem中取对齐后的new elem(这里可能返回NULL,怎么没看到指针安全检查?),然后计算头部和尾部剩余的长度,接着把old elem从heap的queue中remove掉。

	struct malloc_elem *new_elem = elem_start_pt(elem, size, align, bound);
	const size_t old_elem_size = (uintptr_t)new_elem - (uintptr_t)elem;
	const size_t trailer_size = elem->size - old_elem_size - size -
		MALLOC_ELEM_OVERHEAD;

	elem_free_list_remove(elem);

检查尾部的长度是否能够容得下一个elem(包括elem的头部信息和最小data长度),足够大的话就将尾部空间分离出来,并插入到heap合适的queue中。

	if (trailer_size > MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
		/* split it, too much free space after elem */
		struct malloc_elem *new_free_elem =
				RTE_PTR_ADD(new_elem, size + MALLOC_ELEM_OVERHEAD);

		split_elem(elem, new_free_elem);
		malloc_elem_free_list_insert(new_free_elem);
	}

检查头部长度也是否能够容纳一个elem,如果太小的话就将old elem的状态置为ELEM_BUSY,并更新new elem的信息。

	if (old_elem_size < MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
		/* don't split it, pad the element instead */
		elem->state = ELEM_BUSY;
		elem->pad = old_elem_size;

		/* put a dummy header in padding, to point to real element header */
		if (elem->pad > 0) { /* pad will be at least 64-bytes, as everything
		                     * is cache-line aligned */
			new_elem->pad = elem->pad; //赋这个长度是为了后面找回原elem的起始地址吗?
			new_elem->state = ELEM_PAD;
			new_elem->size = elem->size - elem->pad; //减去尾部后的old elem长度减去pad长度,剩下的就是新elem的长度了
			set_header(new_elem);
		}

		return new_elem;
	}

如果头部的长度依然满足一个elem的长度要求,那不能浪费内存,把头部分离出来并插入到合适的heap的queue中去。至此,new elem已经准备好了,将状态置为busy,返回。

	split_elem(elem, new_elem);
	new_elem->state = ELEM_BUSY;
	malloc_elem_free_list_insert(elem);

	return new_elem;







猜你喜欢

转载自blog.csdn.net/u011272715/article/details/80727426
今日推荐