前言:DPDK的内存管理工做主要分布在几个大的部分:大页初始化与管理,内存管理。使用大页能够减小页表开销,是为了尽可能减小TBL miss致使的性能损失。基于大页,DPDK又进一步细化管理这部份内存,使得分配,回收更加方便。node
linux内存是按照页来划分的,默认的每页为4K大小,对应的就存在页表(TBL)来记录每一个页的地址等该单元的信息。这样就存在一个问题,当访问的内容不在本页时,就会触发 tbl miss,致使页换出换入,很影响性能。而一个解决办法就是使用hugepage,大页的每页大小能够设置,经常使用设置如2M,1G等,好比1G大小的内存,使用4k的页面,须要256K个,而使用1G的大页,只须要一个。这样子就能大大减小tbl miss的几率。 更加详细的大页的相关内容,请参考下面的连接:linux
http://www.tuicool.com/articles/vYZJ3i3cookie
内存的初始化在rte_eal_init()
中完成,因为DPDK的进程分为primary和secondary,内存的初始化工做只能在primory进程中完成。主要的步骤以下:app
eal_hugepage_info_init()
;获取大页的信息,并初始化内部的结构。rte_config_init()
;建立配置文件,并作内存映射。rte_eal_memory_init()
;大页的内存初始化,并链接成连续的内存区。rte_eal_memzone_init()
;初始化memzone子系统。eal_hugepage_info_init()
这一步是获取系统中已配置的大页的信息,以及大页的挂载点(在DPDK的参数中能够指定大页的挂载点,默认应该是/mnt/huge
)。
dir = opendir(sys_dir_path);
先打开"/sys/kernel/mm/hugepages"
目录,读取系统中的大页目录,存储在internal_config.hugepage_info[]
结构中,页面的大小在目录名中。而后获取大页的大小和挂载点:socket
hpi = &internal_config.hugepage_info[num_sizes]; hpi->hugepage_sz = rte_str_to_size(&dirent->d_name[dirent_start_len]); hpi->hugedir = get_hugepage_dir(hpi->hugepage_sz);
最后获取空闲页面数量,而且都先放在第一个核上:函数
hpi->num_pages[0] = get_num_hugepages(dirent->d_name);
能够经过设置MAX_HUGEPAGE_SIZES
宏的值来调整DPDK容许配置的大页的页面值个数。默认是3个。性能
以后,把这些大页按照大小顺序排一下序,大的页面在前面。测试
qsort(&internal_config.hugepage_info[0], num_sizes, sizeof(internal_config.hugepage_info[0]), compare_hpi);
最后作一下检查,这样,对于大页的信息的获取就作完了。ui
rte_config_init()
由于DPDK支持primary进程和secondary进程,他们都须要内存的配置信息,进程间通讯使用了共享内存的方法,把struct rte_mem_config *mem_config
结构作内存映射。this
switch (rte_config.process_type){ case RTE_PROC_PRIMARY: rte_eal_config_create(); break; case RTE_PROC_SECONDARY: rte_eal_config_attach(); rte_eal_mcfg_wait_complete(rte_config.mem_config); rte_eal_config_reattach(); break;
开始就根据进程的类型决定启动顺序问题,若是是primary进程,下面看看他的处理过程:
if (internal_config.base_virtaddr != 0) rte_mem_cfg_addr = (void *) RTE_ALIGN_FLOOR(internal_config.base_virtaddr - sizeof(struct rte_mem_config), sysconf(_SC_PAGE_SIZE)); else rte_mem_cfg_addr = NULL; if (mem_cfg_fd < 0){ mem_cfg_fd = open(pathname, O_RDWR | O_CREAT, 0660); if (mem_cfg_fd < 0) rte_panic("Cannot open '%s' for rte_mem_config\n", pathname); } retval = ftruncate(mem_cfg_fd, sizeof(*rte_config.mem_config)); if (retval < 0){ close(mem_cfg_fd); rte_panic("Cannot resize '%s' for rte_mem_config\n", pathname); }
先根据启动的参数选择内存配置文件共享内存开始的地址,若是配置了base_viraddr
,这个地址应该是能够指定大页开始的地址。在大页开始地址的前面映射内存配置文件。而后打开内存配置文件,裁剪大小。
而后选择地址,映射sizeof(*rte_config.mem_config)
大小的内存到内存配置文件。
rte_config.mem_config = (struct rte_mem_config *) rte_mem_cfg_addr; /* store address of the config in the config itself so that secondary * processes could later map the config into this exact location */ rte_config.mem_config->mem_cfg_addr = (uintptr_t) rte_mem_cfg_addr;
填充映射后的地址,这里最后一句比较有意思,把primary进程中映射的地址保存下来,后面咱们就会看到,是为了让secondary进程也映射一样的逻辑地址。
接下来就看看secondary进程的地址映射状况:
首先,作了一个attach操做,就是先对共享文件作了映射,记录了映射后的地址。
rte_eal_config_attach()
以后,就等待primary进程完整eal层的初始化完成。等初始化完成后,魔数就会填充,rte_eal_mcfg_complete()
。secondary进程会再次进行内存映射,此次映射的目的就是使得secondary进程中对内存配置文件映射后的逻辑地址和primary进程同样,这样作有什么好处咱们后面再仔细说。
rte_mem_cfg_addr = (void *) (uintptr_t) rte_config.mem_config->mem_cfg_addr; munmap(rte_config.mem_config, sizeof(struct rte_mem_config)); mem_config = (struct rte_mem_config *) mmap(rte_mem_cfg_addr, sizeof(*mem_config), PROT_READ | PROT_WRITE, MAP_SHARED, mem_cfg_fd, 0);
最后须要说明的一点是:在DPDK中,建立的mempool,ring等能够在多个进程间访问,也是由于在rte_config.mem_config
中有个成员是struct rte_tailq_head tailq_head[RTE_MAX_TAILQ]
,建立的ring等队列头都是挂在其中,是经过构造函数在main函数以前就挂接上的。
rte_eal_memory_init()
这个函数是初始化内存子系统,任务不少,对于primary进程,则映射大页内存,而对于secondary进程,则把大页attach到primary进程。
rte_eal_hugepage_init()
这就是在primary进程中进行大页的映射。很是有趣,来看看他的主要工做吧!下面直接引用函数原型中的说明:
/* * Prepare physical memory mapping: fill configuration structure with * these infos, return 0 on success. * 1. map N huge pages in separate files in hugetlbfs * 2. find associated physical addr * 3. find associated NUMA socket ID * 4. sort all huge pages by physical address * 5. remap these N huge pages in the correct order * 6. unmap the first mapping * 7. fill memsegs in configuration with contiguous zones */
首先,获取全局的配置信息:
mcfg = rte_eal_get_configuration()->mem_config;
这里比较有意思的地方是,primary进程和secondary进程中配置信息映射的逻辑地址是同样的。
而后获取当前使用的大页的大小和页数。
for (i = 0; i < (int) internal_config.num_hugepage_sizes; i++) { /* meanwhile, also initialize used_hp hugepage sizes in used_hp */ used_hp[i].hugepage_sz = internal_config.hugepage_info[i].hugepage_sz; nr_hugepages += internal_config.hugepage_info[i].num_pages[0]; }
分配大页页表,
tmp_hp = malloc(nr_hugepages * sizeof(struct hugepage_file)); if (tmp_hp == NULL) goto fail; memset(tmp_hp, 0, nr_hugepages * sizeof(struct hugepage_file));
而后就到了很是重要的一步:内存映射大页。主要分为三步
先看第一次映射大页:map_all_hugepages(&tmp_hp[hp_offset], hpi, 1)
,最后一个参数就是指明是第一次映射。因为是第一次映射,因此,先填充大页的文件信息
if (orig) { hugepg_tbl[i].file_id = i; hugepg_tbl[i].size = hugepage_sz; eal_get_hugefile_path(hugepg_tbl[i].filepath, sizeof(hugepg_tbl[i].filepath), hpi->hugedir, hugepg_tbl[i].file_id); hugepg_tbl[i].filepath[sizeof(hugepg_tbl[i].filepath) - 1] = '\0'; }
以后,就在/mnt/huge
目录下建立每一个大页文件,并映射每一个大页到内存中。为何是/mnt/huge目录
?由于这是挂载大页文件系统的位置,挂载大页文件系统,能够经过mount -t hugetlbfs nodev /mnt/huge
来挂载。
fd = open(hugepg_tbl[i].filepath, O_CREAT | O_RDWR, 0600); if (fd < 0) { RTE_LOG(DEBUG, EAL, "%s(): open failed: %s\n", __func__, strerror(errno)); return i; } /* map the segment, and populate page tables, * the kernel fills this segment with zeros */ virtaddr = mmap(vma_addr, hugepage_sz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, 0);
在这里,新建立的大页文件并无大小,可是在映射后,文件大小就变成了映射的大小,貌似只能映射页大小的整数倍。
第一次映射,填充orig_va
地址:
hugepg_tbl[i].orig_va = virtaddr;
而后计算下一个页面映射的地址:
vma_addr = (char *)vma_addr + hugepage_sz;
等把全部的页面都映射完了后,这部分对应的物理内存就不会被换出到磁盘。此时,咱们映射的这部份内存,逻辑地址是连续的,可是物理地址不必定是连续的。
接下来查找已经映射的每一个大页的物理地址,并填充其结构。
find_physaddrs()
具体的虚拟地址到物理地址的查找关系
rte_mem_virt2phy()
而后找到映射的大页内存被放在哪一个NUMA node上。
if (find_numasocket(&tmp_hp[hp_offset], hpi) < 0){ RTE_LOG(DEBUG, EAL, "Failed to find NUMA socket for %u MB pages\n", (unsigned)(hpi->hugepage_sz / 0x100000)); goto fail; }
把映射的大页的物理地址按照从小到大的顺序进行排序。
qsort(&tmp_hp[hp_offset], hpi->num_pages[0], sizeof(struct hugepage_file), cmp_physaddr);
接下来就是第二次对大页进行映射:
if (map_all_hugepages(&tmp_hp[hp_offset], hpi, 0) != hpi->num_pages[0])
这里咱们看到最后一个参数就已是0了。
这样进来函数以后,第一个循环时,vma_len
就是0,而后就去查找物理地址连续的页:
for (j = i+1; j < hpi->num_pages[0] ; j++) { #ifdef RTE_ARCH_PPC_64 /* The physical addresses are sorted in * descending order on PPC64 */ if (hugepg_tbl[j].physaddr != hugepg_tbl[j-1].physaddr - hugepage_sz) break; #else if (hugepg_tbl[j].physaddr != hugepg_tbl[j-1].physaddr + hugepage_sz) break; #endif } num_pages = j - i; vma_len = num_pages * hugepage_sz;
这样,就能肯定连续的物理页有几个,而后,去尝试分配和连续物理页同样大的虚拟地址空间,若是不能,就减少一个页再尝试,直到成功(返回地址)或者失败(返回NULL)。若是能拿到地址,那么就以这个地址开始,依次映射物理地址连续的几个页。若是不能拿到这么大且连续的虚拟地址,那么,就让内核本身去分配地址,而后映射这一页。
第二次映射后,就填充final_va
地址了:hugepg_tbl[i].final_va = virtaddr;
。
既然已经从新映射了大页的虚拟地址,那么就应该撤销原来的映射。
if (unmap_all_hugepages_orig(&tmp_hp[hp_offset], hpi) < 0) goto fail;
这样事后,对于大页内存的映射工做就完成了。
接下来就是分配映射的大页内存咯。
首先,清空配置信息中的每一个socket中大页的数量,等待从新分配。
for (i = 0; i < (int)internal_config.num_hugepage_sizes; i++) for (j = 0; j < RTE_MAX_NUMA_NODES; j++) internal_config.hugepage_info[i].num_pages[j] = 0;
而后获取每一个socket上的大页数量,
for (i = 0; i < nr_hugefiles; i++) { int socket = tmp_hp[i].socket_id; /* find a hugepage info with right size and increment num_pages */ const int nb_hpsizes = RTE_MIN(MAX_HUGEPAGE_SIZES, (int)internal_config.num_hugepage_sizes); for (j = 0; j < nb_hpsizes; j++) { if (tmp_hp[i].size == internal_config.hugepage_info[j].hugepage_sz) { internal_config.hugepage_info[j].num_pages[socket]++; } } }
从新计算调整每一个socket上的大页的分布,最后返回大页个数。
nr_hugepages = calc_num_pages_per_socket(memory, internal_config.hugepage_info, used_hp, internal_config.num_hugepage_sizes);
默认每一个socket上的大页数量是按核心数量的比例分配的。
而后为大页映射信息文件建立共享内存,用于secondary进程来映射地址。
先撤销不用的大页映射,而后把临时大页信息文件拷贝到建立的共享内存中。
if (unmap_unneeded_hugepages(tmp_hp, used_hp, internal_config.num_hugepage_sizes) < 0) { RTE_LOG(ERR, EAL, "Unmapping and locking hugepages failed!\n"); goto fail; } if (copy_hugepages_to_shared_mem(hugepage, nr_hugefiles, tmp_hp, nr_hugefiles) < 0) { RTE_LOG(ERR, EAL, "Copying tables to shared memory failed!\n"); goto fail; }
最后把大页内存切成段保存在内存管理结构中。大页内存切段的条件是:
而后把切好的内存段放入mcfg配置表中:
mcfg->memseg[j].phys_addr = hugepage[i].physaddr; mcfg->memseg[j].addr = hugepage[i].final_va; mcfg->memseg[j].len = hugepage[i].size; mcfg->memseg[j].socket_id = hugepage[i].socket_id; mcfg->memseg[j].hugepage_sz = hugepage[i].size;
这样,大页的初始化就完成了!
rte_eal_hugepage_attach()
对于secondary进程而言,它并不能建立大页的共享内存,而只能attach上去。
开始大页内存attach的前提是先attach内存配置文件,咱们再来看一下attach配置的过程:
rte_eal_config_attach(); rte_eal_mcfg_wait_complete(rte_config.mem_config); rte_eal_config_reattach();
第一个函数中,先映射一下/var/run/.rte_config
文件,拿到内存配置的结构信息,就是为了第二个函数的等待判断用的。第三个函数中,既然主进程已经初始化完成,那么,就先解除第一个函数的映射,以primary进程中映射的内存配置文件地址做为新的映射地址,从新映射,映射完成后,primary进程和secondary进程中,对于/var/run/.rte_config
映射的虚拟地址是同样的。(虽然,对于配置文件映射地址同样,感受并没什么卵用~,但后面的大页映射也是这么作的,映射地址的一致,就有用啦)。
接下来就来看大页内存的attach,首先打开/dev/zero
文件,按照primary的段的虚拟地址依次映射全部的内存段,这一步至关于先测试一下是否能分配这样的连续地址空间。
base_addr = mmap(mcfg->memseg[s].addr, mcfg->memseg[s].len, PROT_READ, MAP_PRIVATE, fd_zero, 0);
而后映射大页信息共享文件/var/run/.rte_hugepage_info
,并计算页个数等。
size = getFileSize(fd_hugepage); hp = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd_hugepage, 0); if (hp == MAP_FAILED) { RTE_LOG(ERR, EAL, "Could not mmap %s\n", eal_hugepage_info_path()); goto error; } num_hp = size / sizeof(struct hugepage_file);
最后解除映射到/dev/zero
,从新映射到各个大页文件中,
for (i = 0; i < num_hp && offset < mcfg->memseg[s].len; i++){ if (hp[i].memseg_id == (int)s){ fd = open(hp[i].filepath, O_RDWR); if (fd < 0) { RTE_LOG(ERR, EAL, "Could not open %s\n", hp[i].filepath); goto error; } mapping_size = hp[i].size; addr = mmap(RTE_PTR_ADD(base_addr, offset), mapping_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); /* close file both on success and on failure */ if (addr == MAP_FAILED || addr != RTE_PTR_ADD(base_addr, offset)) { RTE_LOG(ERR, EAL, "Could not mmap %s\n", hp[i].filepath); goto error; } offset+=mapping_size; } }
到这里咱们仔细看一下,进程中是以primary中的虚拟地址做为映射地址来映射的,也就是说在映射完成后,primary进程和secondary进程中映射的大页地址是同样的。这很关键,这正是实现零拷贝的原理。虚拟地址同样,那么从大页内存中拿到的数据包,就能够不通过拷贝,直接把地址传到secondary进程中。
这些都映射完了后,就完成了attach工做。
rte_eal_memzone_init()
memzone是内存分配器,上一步中,咱们已经把大页内存分段放好了,可是在使用的时候,怎么来分配呢?天然须要内存分配器,就是memzone。而memzone_init
主要就是把内存放到空闲链表中,等须要的时候,可以分配出去。
在看初始化前,先看一个结构,struct malloc_elem
,这个结构表示一个内存对象,
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_LIBRTE_MALLOC_DEBUG uint64_t header_cookie; /* Cookie marking start of data */ /* trailer cookie at start + size */ #endif } __rte_cache_aligned;
而后看初始化
rte_eal_malloc_heap_init()
依次把每一段都添加到heap中,段属于哪一个socket,就添加到哪一个socket的heap中,分配就从这里拿。
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); }
把每一段作初始化,并挂在空闲链表中:
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;
而后就初始化完了!
内存分配有一系列的接口:大多定义在rte_malloc.c
文件中。咱们重点挑两个来看一下。
rte_malloc_socket()
ret = malloc_heap_alloc(&mcfg->malloc_heaps[socket], type, size, 0, align == 0 ? 1 : align, 0); if (ret != NULL || socket_arg != SOCKET_ID_ANY) return ret;
先在指定的socket上分配,若是不能成功,再去尝试其余的socket。咱们接着看函数malloc_heap_alloc()
:
void * malloc_heap_alloc(struct malloc_heap *heap, const char *type __attribute__((unused)), size_t size, unsigned flags, size_t align, size_t bound) { struct malloc_elem *elem; size = RTE_CACHE_LINE_ROUNDUP(size); align = RTE_CACHE_LINE_ROUNDUP(align); 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); /* increase heap's count of allocated elements */ heap->alloc_count++; } rte_spinlock_unlock(&heap->lock); return elem == NULL ? NULL : (void *)(&elem[1]);
先去空闲链表中找是否有知足需求的内存块,若是找到,就进行分配,不然返回失败。进一步的,在函数malloc_elem_alloc()
分配的的时候,若是存在的内存大于须要的内存时,会对内存进行切割,而后把用不完的从新挂在空闲链表上。就不细致的代码分析了。
rte_memzone_reserve_aligned()
struct rte_memzone
型的,这是和上一个分配接口的不一样之处,同时注意分配时的flag的不一样。分配出来的memzone能够直接使用名字索引到。这个函数最终也是会调用到malloc_heap_alloc()
,就很少说了,能够看看代码。除此之外,须要额外提到的内存分配的地方是建立内存池。在建立内存池时,会建立一个ring来存储分配的对象,同时,为了减小多核之间对同一个ring的访问,每个核都维护着一个cache,优先从cache中取。
说完了DPDK的内存分配,最后来讲一下内存回收。跟分配的接口对应,也有多个回收函数。
rte_free()
一样这个函数,在上层封装了多种接口。如rte_memzone_free()
等。主要的过程也是从新把elem放进free list上,若是有可以合并的,还会对其进行合并。
rte_memzone_free()
上面都说过了,这个里面也是对rte_free()
的封装,很少说了,just see the code!
一样,关于回收也有点注意的,对于内存池中的元素的回收,不是释放回空闲链表,而是从新放到ring或者cache中,就这么多了。