dpdk内存管理——内存初始化
———lvyilong316(转载请注明出处)
说明:本系列博文源代码均来自dpdk17.02
1.1内存初始化
1.1.1 hugepage技术
hugepage(2M/1G..)相对于普通的page(4K)来说有几个特点:
(1) hugepage 这种页面不受虚拟内存管理影响,不会被替换(swap)出内存,而普通的4kpage 如果物理内存不够可能会被虚拟内存管理模块替换到交换区。
(2) 同样的内存大小,hugepage产生的页表项数目远少于4kpage.
举一个例子,用户进程需要使用 4M 大小的内存,如果采用4Kpage, 需要1K的页表项存放虚拟地址到物理地址的映射关系,而采用hugepage
2M 只需要产生2条页表项,这样会带来两个后果,一是使用hugepage的内存产生的页表比较少,这对于数据库系统等动不动就需要映射非常大的数据到进程的应用来说,页表的开销是很可观的,所以很多数据库系统都采用hugepage技术。二是tlb冲突率大大减少,tlb 驻留在cpu的1级cache里,是芯片访问最快的缓存,一般只能容纳100多条页表项,如果采用hugepage,则可以极大减少
tlb miss 导致的开销:tlb命中,立即就获取到物理地址,如果不命中,需要查 rc3->进程页目录表pgd->进程页中间表pmd->进程页框->物理内存,如果这中间pmd或者页框被虚拟内存系统替换到交互区,则还需要交互区load回内存。。总之,tlb miss是性能大杀手,而采用hugepage可以有效降低tlb
miss 。
linux 使用hugepage的方式比较简单,以2M的hugepage为例:
1. /sys/kernel/mm/hugepages/hugepages-2048kB/
通过修改这个目录下的文件可以修改hugepage页面的大小和总数目;
2. mount -t
hugetlbfs nodev /mnt/huge linux将hugepage实现为一种文件系统hugetlbfs,需要将该文件系统mount到某个文件;
3. mmap
/mnt/huge 在用户进程里通过mmap 映射hugetlbfs mount 的目标文件,这个mmap返回的地址就是大页面的了。
1.1.2 多进程共享
mmap 系统调用可以设置为共享的映射,dpdk的内存共享就依赖于此,在这多个进程中,分为两种角色,第一种是主进程(RTE_PROC_PRIMARY),第二种是从进程(RTE_PROC_SECONDARY)。主进程只有一个,必须在从进程之前启动,负责执行DPDK库环境的初始化,从进程attach到主进程初始化的DPDK上,主进程先mmap hugetlbfs 文件,构建内存管理相关结构将这些结构存入hugetlbfs 上的配置文件rte_config,然后其他进程mmap rte_config文件,获取内存管理结构,dpdk采用了一定的技巧,使得最终同样的共享物理内存在不同进程内部对应的虚拟地址是完全一样的,意味着一个进程内部的基于dpdk的共享数据和指向这些共享数据的指针,可以在不同进程间通用。
1.1.3 相关数据结构
l rte_config
内存全局配置结构。
1) rte_config 是每个程序私有的数据结构,这些东西都是每个程序的私有配置。
2) lcore_role:这个DPDK程序使用-c参数设置的它同时跑在哪几个核上。
3) master_lcore:DPDK的架构上,每个程序分配的lcore_role 有一个主核,对使用者来说影响不大。
4) lcore_count:这个程序可以使用的核数。
5) process_type:DPDK多进程:一个程序是主程序,否则初始化DPDK内存表,其他从程序使用这个表。RTE_PROC_PRIMARY/RTE_PROC_SECONDARY
6) mem_config:指向设备各个DPDK程序共享的内存配置结构,这个结构被mmap到文件/var/run/.rte_config,通过这个方式多进程实现对mem_config结构的共享。
l hugepage_file
这个是struct hugepage数组,每个struct hugepage_file 都代表一个hugepage 页面,存储的每个页面的物理地址和程序的虚拟地址的映射关系。然后,把整个数组映射到文件/var/run /. rte_hugepage_info,同样这个文件也是设备共享的,主/从进程都能访问它。
1) file_id: 每个文件在hugepage 文件系统中都有一个编号,就是数组1-N;
2) filepath:%s/%smap_%file_id mount 的hugepage文件系统中的文件路径名;
3) size: 这个hugepage页面的size,2M还是1G;
4) socket_id:这个页面属于那个CPU socket 。
5) Physaddr:这个hugepage 页面的物理地址
6) orig_va:它和final_va一样都是指这个huagepage页面的虚拟地址。这个地址是主程序初始化huagepage用的,后来就没用了。
7) final_va:这个最终这个页面映射到主/从程序中的虚拟地址。
首先因为整个数组都映射到文件里面,所有的程序之间都是共享的。主程序负责初始化这个数组,首先在它内部通过mmap把所有的hugepage物理页面都映射到虚存空间里面,然后把这种映射关系保存到这个文件里面。从程序启动的时候,读取这个文件,然后在它内存也创建和它一模一样的映射,这样的话,整个DPDK管理的内存在所有的程序里面都是可见,而且地址都一样。
在对各个页面的物理地址份配虚拟地址时,DPDK尽可能把物理地址连续的页面分配连续的虚存地址上,这个东西还是比较有用的,因为CPU/cache/内存控制器的等等看到的都是物理内存,我们在访问内存时,如果物理地址连续的话,性能会高一些。至于到底哪些地址是连续的,那些不是连续的,DPDK在这个结构之上又有一个新的结构rte_mem_config.
memseg来管理。因为rte_mem_config也映射到文件里面,所有的程序都可见rte_mem_config. memseg结构。
l rte_mem_config
这个数据结构mmap 到文件/var/run /.rte_config中,主/从进程通过这个文件访问实现对这个数据结构的共享。在每个程序内,使用rte_config .mem_config 访问这个结构。
l rte_memseg
memseg 数组是维护物理地址的,在上面讲到struct hugepage结构对每个hugepage物理页面都存储了它在程序里面的虚存地址。memseg 数组的作用是将物理地址、虚拟地址都连续的hugepage,并且都在同一个socket,pagesize 也相同的hugepage页面集合,把它们都划在一个memseg结构里面,这样做的好处就是优化内存。
rte_memseg这个结构也很简单:
1) phys_addr:这个memseg的包含的所有的hugepage页面的起始物理地址;
2) addr:这些hugepage页面的起始的虚存地址;
3) len:这个memseg的包含的空间size
4) hugepage_sz; 这些页面的size 2M /1G?
这些信息都是从hugepage页表数组里面获得的。
1.2 dpdk 内存初始化源码解析
l rte_eal_init
这个函数是dpdk 运行环境初始化入口函数。
整个内存初始化的代码流程如上图所示,下面我们逐个分析。
l eal_hugepage_info_init
这个函数较为简单,主要是遍历系统的/sys/kernel/mm/hugepages目录建立对应的数据结构。系统支持的每种size的hugepage类型在/sys/kernel/mm/hugepages目录下都对应一个子目录。例如系统支持2M和1G的大页,就会有对应内目录如下图所示:
而其中每个目录就会对应一个struct hugepage_info的结构,其结构如下,记录着对应目录下的信息。那么目录下都有什么信息呢?如下图所示:
所以struct hugepage_info中也是记录的这些信息,包括当前size的hugepage页面总个数(nr_hugepages),已经还没有被分配的个数(free_hugepages)等。所有struct
hugepage_info构成一个数组,保存在struct
internal_config结构中。
源码如下:
-
int
-
eal_hugepage_info_init(void)
-
{
-
const char dirent_start_text[] = "hugepages-";
-
const size_t dirent_start_len = sizeof(dirent_start_text) - 1;
-
unsigned i, num_sizes = 0;
-
DIR *dir;
-
struct dirent *dirent;
-
-
dir = opendir(sys_dir_path); /* /sys/kernel/mm/hugepages */
-
if (dir == NULL)
-
rte_panic("Cannot open directory %s to read system hugepage "
-
"info\n", sys_dir_path);
-
-
for (dirent = readdir(dir); dirent != NULL; dirent = readdir(dir)) {
-
struct hugepage_info *hpi;
-
-
if (strncmp(dirent->d_name, dirent_start_text,
-
dirent_start_len) != 0)
-
continue;
-
-
if (num_sizes >= MAX_HUGEPAGE_SIZES)
-
break;
-
-
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);
-
-
/* first, check if we have a mountpoint */
-
if (hpi->hugedir == NULL) {
-
uint32_t num_pages;
-
-
num_pages = get_num_hugepages(dirent->d_name);
-
if (num_pages > 0)
-
RTE_LOG(NOTICE, EAL,
-
"%" PRIu32 " hugepages of size "
-
"%" PRIu64 " reserved, but no mounted "
-
"hugetlbfs found for that size\n",
-
num_pages, hpi->hugepage_sz);
-
continue;
-
}
-
-
/* try to obtain a writelock */
-
hpi->lock_descriptor = open(hpi->hugedir, O_RDONLY);
-
-
/* if blocking lock failed */
-
if (flock(hpi->lock_descriptor, LOCK_EX) == -1) {
-
RTE_LOG(CRIT, EAL,
-
"Failed to lock hugepage directory!\n");
-
break;
-
}
-
/* clear out the hugepages dir from unused pages */
-
if (clear_hugedir(hpi->hugedir) == -1)
-
break;
-
-
/* for now, put all pages into socket 0,
-
* later they will be sorted */
-
/* 这里还没有按socket统计页数,将内存页数直接记录到hupage_info的num_pages[0]里面了 */
-
hpi->num_pages[0] = get_num_hugepages(dirent->d_name);
-
-
#ifndef RTE_ARCH_64
-
/* for 32-bit systems, limit number of hugepages to
-
* 1GB per page size */
-
hpi->num_pages[0] = RTE_MIN(hpi->num_pages[0],
-
RTE_PGSIZE_1G / hpi->hugepage_sz);
-
#endif
-
-
num_sizes++;
-
}
-
closedir(dir);
-
-
/* something went wrong, and we broke from the for loop above */
-
if (dirent != NULL)
-
return -1;
-
-
internal_config.num_hugepage_sizes = num_sizes;
-
-
/* sort the page directory entries by size, largest to smallest */
-
qsort(&internal_config.hugepage_info[0], num_sizes,
-
sizeof(internal_config.hugepage_info[0]), compare_hpi);
-
-
/* now we have all info, check we have at least one valid size */
-
for (i = 0; i < num_sizes; i++)
-
if (internal_config.hugepage_info[i].hugedir != NULL &&
-
internal_config.hugepage_info[i].num_pages[0] > 0)
-
return 0;
-
-
/* no valid hugepage mounts available, return error */
-
return -1;
-
}
每一类所有内存页,也分处在哪个 socket上(不明白的查看NUMA相关知识补齐)的,hugepage_info中统计内存页数会按属于处在哪个socket上进行统计,但在这一步(eal_hugepage_info_init)中,还区分不了每个页处在哪个socket上,因此这里还没有按socket统计页数,所以就暂时将内存页数直接记录到hupage_info的num_pages[0]里面了。
这里有一个特别注意的点就是get_num_hugepages获取的页面数量是怎么计算来的。这里就不将这个函数展开了,根据其内部实现,其返回的页面个数为“free_hugepages-resv_hugepages”。也就是说,这里获取的是整个系统的可用hugepage页面数。所以后面进行mmap时也是用的这个值,就是会对整个系统的可用页面数进行mmap。这就有个问题,我们知道dpdk进程启动会传入一个指定的内存大小参数,dpdk进程完全只需要分配及mmap这个内存大小就可以了,为什么还要mmap整个系统的的页面呢?这是为了在整个系统层面最大限度找到连续的物理内存,dpdk进程需要尽可能使用连续的内存来提高性能,当然多余的mmap内存会被dpdk unmmap掉,这个见后文分析。
l rte_config_init
-
static void
-
rte_config_init(void)
-
{
-
rte_config.process_type = internal_config.process_type;
-
-
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;
-
case RTE_PROC_AUTO:
-
case RTE_PROC_INVALID:
-
rte_panic("Invalid process type\n");
-
}
-
}
DPDK多进程状态下,分为RTE_PROC_PRIMARY进程及RTE_PROC_SECONDARY进程,RTE_PROC_PRIMARY负责初始化内存,RTE_PROC_SECONDARY获取 RTE_PROC_PRIMARY 内存映射的信息,创建与RTE_PROC_PRIMARY一样的内存映射。这是DPDK多进程共享内存的方式。此处先不展开描述。随着流程的展开,自然会明白。
l rte_eal_config_create
创建struct rte_mem_config结构,并mmap 到文件/var/run /.rte_config中。
-
static void
-
rte_eal_config_create(void)
-
{
-
void *rte_mem_cfg_addr;
-
int retval;
-
-
const char *pathname = eal_runtime_config_path(); /*/var/run*/
-
-
if (internal_config.no_shconf)
-
return;
-
-
/* map the config before hugepage address so that we don't waste a page */
-
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);
-
}
-
/*使用mmap分配内存,一般都是先open一个文件,然后调用ftruncate进行文件大小设置,最后进行mmap*/
-
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);
-
}
-
-
retval = fcntl(mem_cfg_fd, F_SETLK, &wr_lock);
-
if (retval < 0){
-
close(mem_cfg_fd);
-
rte_exit(EXIT_FAILURE, "Cannot create lock on '%s'. Is another primary "
-
"process running?\n", pathname);
-
}
-
-
rte_mem_cfg_addr = mmap(rte_mem_cfg_addr, sizeof(*rte_config.mem_config),
-
PROT_READ | PROT_WRITE, MAP_SHARED, mem_cfg_fd, 0);
-
-
if (rte_mem_cfg_addr == MAP_FAILED){
-
rte_panic("Cannot mmap memory for rte_config\n");
-
}
-
memcpy(rte_mem_cfg_addr, &early_mem_config, sizeof(early_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;
-
-
}
l rte_eal_memory_init
根据进程是否为RTE_PROC_PRIMARY分别调用rte_eal_hugepage_init和rte_eal_hugepage_attach函数。
l rte_eal_hugepage_init
这个函数是内存初始化的重点,整体流程如下:
我们分段分析。
-
int
-
rte_eal_hugepage_init(void)
-
{
-
struct rte_mem_config *mcfg;
-
struct hugepage_file *hugepage = NULL, *tmp_hp = NULL;
-
struct hugepage_info used_hp[MAX_HUGEPAGE_SIZES];
-
-
uint64_t memory[RTE_MAX_NUMA_NODES];
-
-
unsigned hp_offset;
-
int i, j, new_memseg;
-
int nr_hugefiles, nr_hugepages = 0;
-
void *addr;
-
-
test_proc_pagemap_readable();
-
-
memset(used_hp, 0, sizeof(used_hp));
-
-
/*获取rte_config->mem_config */
-
mcfg = rte_eal_get_configuration()->mem_config;
-
/* hugetlbfs can be disabled */
-
if (internal_config.no_hugetlbfs) {
-
addr = mmap(NULL, internal_config.memory, PROT_READ | PROT_WRITE,
-
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
-
if (addr == MAP_FAILED) {
-
RTE_LOG(ERR, EAL, "%s: mmap() failed: %s\n", __func__,
-
strerror(errno));
-
return -1;
-
}
-
mcfg->memseg[0].phys_addr = (phys_addr_t)(uintptr_t)addr;
-
mcfg->memseg[0].addr = addr;
-
mcfg->memseg[0].hugepage_sz = RTE_PGSIZE_4K;
-
mcfg->memseg[0].len = internal_config.memory;
-
mcfg->memseg[0].socket_id = 0;
-
return 0;
-
}
函数一开始,将rte_config_init函数获取的配置结构放到本地变量 mcfg 上,然后检查系统是否开启hugetlbfs,如果不开启,则直接通过系统的malloc函数申请配置需要的内存,然后跳出这个函数。
-
/* calculate total number of hugepages available. at this point we haven't
-
* yet started sorting them so they all are on socket 0 */
-
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];
-
}
-
/*
-
* allocate a memory area for hugepage table.
-
* this isn't shared memory yet. due to the fact that we need some
-
* processing done on these pages, shared memory will be created
-
* at a later stage.
-
*/
-
/*注意这里分配内存使用的不是hugepage,后面会将这些结构拷贝到hugepage,这里内存会被释放*/
-
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));
-
-
hp_offset = 0; /* where we start the current page size entries */
-
-
huge_register_sigbus();
计算系统中的hugepage个数,存放在nr_hugepages中。然后分配struct hugepage_file的数组,每个结构对应一个hugepage页面信息。注意这个数组的内存还不在hugepage的共享内存中,而是普通的进程私有内存。
-
/* map all hugepages and sort them */
-
/*遍历每种size的hugepage,如1G,2M的一页*/
-
for (i = 0; i < (int)internal_config.num_hugepage_sizes; i ++){
-
unsigned pages_old, pages_new;
-
struct hugepage_info *hpi;
-
-
/*
-
* we don't yet mark hugepages as used at this stage, so
-
* we just map all hugepages available to the system
-
* all hugepages are still located on socket 0
-
*/
-
hpi = &internal_config.hugepage_info[i];
-
-
if (hpi->num_pages[0] == 0)/*如果这种size的hugepage的个数为0,则跳过*/
-
continue;
-
-
/* map all hugepages available */
-
pages_old = hpi->num_pages[0];
-
/*为每个hugepage创建文件并mmap,相关hugepage信息保存在tmp_hp[hp_offset]开启的数组*/
-
pages_new = map_all_hugepages(&tmp_hp[hp_offset], hpi, 1);
-
if (pages_new < pages_old) {/*说明有写页面没有mmap成功,可能中间有其他进程占用了*/
-
RTE_LOG(DEBUG, EAL,
-
"%d not %d hugepages of size %u MB allocated\n",
-
pages_new, pages_old,
-
(unsigned)(hpi->hugepage_sz / 0x100000));
-
-
int pages = pages_old - pages_new;
-
-
nr_hugepages -= pages;
-
hpi->num_pages[0] = pages_new; /*更新可用页面为mmap成功的页面*/
-
if (pages_new == 0)
-
continue;
-
}
-
/*查询每个hugepage起始的物理地址,记录在hugepage_file.physaddr*/
-
/* find physical addresses and sockets for each hugepage */
-
if (find_physaddrs(&tmp_hp[hp_offset], hpi) < 0){
-
RTE_LOG(DEBUG, EAL, "Failed to find phys addr for %u MB pages\n",
-
(unsigned)(hpi->hugepage_sz / 0x100000));
-
goto fail;
-
}
-
/*查询每个hugepage所在的socket,记录在hugepage_file.socket_id*/
-
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;
-
}
-
/*根据物理地址对tmp_hp中每个hugepage进行排序*/
-
qsort(&tmp_hp[hp_offset], hpi->num_pages[0],
-
sizeof(struct hugepage_file), cmp_physaddr);
-
-
/* remap all hugepages */
-
/*对排序好的hugepage再次进行mmap*/
-
if (map_all_hugepages(&tmp_hp[hp_offset], hpi, 0) !=
-
hpi->num_pages[0]) {
-
RTE_LOG(ERR, EAL, "Failed to remap %u MB pages\n",
-
(unsigned)(hpi->hugepage_sz / 0x100000));
-
goto fail;
-
}
-
/*解除第一次mmap关系*/
-
/* unmap original mappings */
-
if (unmap_all_hugepages_orig(&tmp_hp[hp_offset], hpi) < 0)
-
goto fail;
-
-
/* we have processed a num of hugepages of this size, so inc offset */
-
hp_offset += hpi->num_pages[0]; /*更新hp_offset,每次循环处理一个size的所有hugepage*/
-
}
构建hugepage 结构数组分下面几步:
(1) 循环遍历系统所有的hugetlbfs 文件系统,一般来说,一个系统只会使用一种hugetlbfs ,所以这一层的循环可以认为没有作用,一种 hugetlbfs 文件系统对应的基础数据包括:页面大小,比如2M,页面数目,比如2K个页面;
(2) 其次,将特定的hugetlbfs的全部页面映射到本进程,放到本进程的 hugepage 数组管理,这个过程主要由 map_all_hugepages函数完成,第一次映射的虚拟地址存放在 hugepage结构的 orig_va变量;
(3) 遍历hugepage数组,找到每个虚拟地址对应的物理地址和所属的物理cpu,将这些信息也记入 hugepage数组,物理地址记录在hugepage结构的phyaddr变量,物理cpu号记录在 hugepage结构的socket_id变量;
(4) 跟据物理地址大小对hugepage数组做排序;
(5) 根据排序结果重新映射,这个也是由函数 map_all_hugepages完成,重新映射后的虚拟地址存放在hugepage结构的final_va变量;
(6) 将第一次映射关系解除,即将orig_va 变量对应的虚拟地址空间返回给内核。
下面看 map_all_hugepages的实现过程。
l map_all_hugepages
-
static unsigned
-
map_all_hugepages(struct hugepage_file *hugepg_tbl,
-
struct hugepage_info *hpi, int orig)
-
{
-
int fd;
-
unsigned i;
-
void *virtaddr;
-
void *vma_addr = NULL;
-
size_t vma_len = 0;
-
/*遍历每个hugepage页面*/
-
for (i = 0; i < hpi->num_pages[0]; i++) {
-
uint64_t hugepage_sz = hpi->hugepage_sz;
-
-
if (orig) { /*如果是第一次调用这个函数*/
-
hugepg_tbl[i].file_id = i;/*hugepage页面的编号*/
-
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); /*构造hugepage对应的磁盘文件名称,如:/mnt/huge/retmap_0*/
-
hugepg_tbl[i].filepath[sizeof(hugepg_tbl[i].filepath) - 1] = '\0';
-
}
-
else if (vma_len == 0) {/*第二次映射调用,且第一次进入循环*/
-
unsigned j, num_pages;
-
-
/* reserve a virtual area for next contiguous
-
* physical block: count the number of
-
* contiguous physical pages. */
-
/*遍历hugepage页面,找物理内存最大的连续区间*/
-
for (j = i+1; j < hpi->num_pages[0] ; j++) {
-
if (hugepg_tbl[j].physaddr !=
-
hugepg_tbl[j-1].physaddr + hugepage_sz)
-
break;
-
}/*所有的已分配物理页未必连续,这里只是找最大的连续物理内存区间*/
-
num_pages = j - i; /*连续页面的个数*/
-
vma_len = num_pages * hugepage_sz; /*连续页面的大小*/
-
-
/* get the biggest virtual memory area up to
-
* vma_len. If it fails, vma_addr is NULL, so
-
* let the kernel provide the address. */
-
vma_addr = get_virtual_area(&vma_len, hpi->hugepage_sz); /*申请和连续物理内存同样大小的连续虚拟地址空间*/
-
if (vma_addr == NULL)
-
vma_len = hugepage_sz;
-
}
-
-
/* try to create hugepage file */
-
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 */
-
/*第一次mmap时vma_addr为NULL,内核会自动选取mmap虚拟地址,第二次vma_addr是计算出来的*/
-
virtaddr = mmap(vma_addr, hugepage_sz, PROT_READ | PROT_WRITE,
-
MAP_SHARED | MAP_POPULATE, fd, 0);
-
if (virtaddr == MAP_FAILED) {
-
RTE_LOG(DEBUG, EAL, "%s(): mmap failed: %s\n", __func__,
-
strerror(errno));
-
close(fd);
-
return i;
-
}
-
-
if (orig) {/*如果是第一次映射,映射虚拟地址保存在orig_va*/
-
hugepg_tbl[i].orig_va = virtaddr;
-
}
-
else { /*第二次映射,映射虚拟地址保存在final_va*/
-
hugepg_tbl[i].final_va = virtaddr;
-
}
-
-
if (orig) {
-
/* In linux, hugetlb limitations, like cgroup, are
-
* enforced at fault time instead of mmap(), even
-
* with the option of MAP_POPULATE. Kernel will send
-
* a SIGBUS signal. To avoid to be killed, save stack
-
* environment here, if SIGBUS happens, we can jump
-
* back here.
-
*/
-
if (huge_wrap_sigsetjmp()) {
-
RTE_LOG(DEBUG, EAL, "SIGBUS: Cannot mmap more "
-
"hugepages of size %u MB\n",
-
(unsigned)(hugepage_sz / 0x100000));
-
munmap(virtaddr, hugepage_sz);
-
close(fd);
-
unlink(hugepg_tbl[i].filepath);
-
return i;
-
}
-
*(int *)virtaddr = 0;
-
}
-
-
-
/* set shared flock on the file. */
-
if (flock(fd, LOCK_SH | LOCK_NB) == -1) {
-
RTE_LOG(DEBUG, EAL, "%s(): Locking file failed:%s \n",
-
__func__, strerror(errno));
-
close(fd);
-
return i;
-
}
-
-
close(fd);
-
-
vma_addr = (char *)vma_addr + hugepage_sz;
-
vma_len -= hugepage_sz;
-
}
-
-
return i;
-
}
这个函数是复用的,共有两次调用。对于第一次调用,就是根据hugetlbfs 文件系统的页面数m,构造m个文件名称并创建文件,每个文件对应一个大页面,然后通过mmap系统调用映射到进程的一块虚拟地址空间,并将虚拟地址存放在hugepage结构的orig_va地址上。如果该hugetlbfs有1K个页面,最终会在hugetlbfs 挂载的目录上生成 1K 个文件,这1K 个文件mmap到进程的虚拟地址由进程内部的hugepage数组维护对于第二次调用,由于hugepage数组已经基于物理地址排序,这些有序的物理地址可能有2种情况,一种是连续的,另一种是不连续的,这时候的调用会遍历这个hugepage数组,然后统计连续物理地址的最大内存,这个统计有什么好处?因为第二次的映射需要保证物理内存连续的其虚拟内存也是连续的,在获取了最大连续物理内存大小后,比如是100个页面大小,会调用 get_virtual_area 函数向内涵申请100个页面大小的虚拟空间,如果成功,说明虚拟地址可以满足,然后循环100次,每次映射mmap的首个参数就是get_virtual_area函数返回的虚拟地址+i*页面大小,这样,这100个页面的虚拟地址和物理地址都是连续的,虚拟地址存放到final_va 变量上。
那么究竟是如何找到连续的虚拟地址空间呢?
-
static void *
-
get_virtual_area(size_t *size, size_t hugepage_sz)
-
{
-
void *addr;
-
int fd;
-
long aligned_addr;
-
-
if (internal_config.base_virtaddr != 0) {
-
addr = (void*) (uintptr_t) (internal_config.base_virtaddr +
-
baseaddr_offset);
-
}
-
else addr = NULL;
-
-
RTE_LOG(DEBUG, EAL, "Ask a virtual area of 0x%zx bytes\n", *size);
-
-
fd = open("/dev/zero", O_RDONLY);
-
if (fd < 0){
-
RTE_LOG(ERR, EAL, "Cannot open /dev/zero\n");
-
return NULL;
-
}
-
do {
-
/*注意这里使用的私有映射*/
-
addr = mmap(addr,
-
(*size) + hugepage_sz, PROT_READ, MAP_PRIVATE, fd, 0);
-
if (addr == MAP_FAILED)
-
*size -= hugepage_sz;
-
} while (addr == MAP_FAILED && *size > 0);/*未必有这么大的连续虚拟空间,所以如果失败需要减少虚拟空间大小*/
-
-
if (addr == MAP_FAILED) {
-
close(fd);
-
RTE_LOG(ERR, EAL, "Cannot get a virtual area: %s\n",
-
strerror(errno));
-
return NULL;
-
}
-
-
munmap(addr, (*size) + hugepage_sz);
-
close(fd);
-
-
/* align addr to a huge page size boundary */
-
aligned_addr = (long)addr;
-
aligned_addr += (hugepage_sz - 1);
-
aligned_addr &= (~(hugepage_sz - 1));
-
addr = (void *)(aligned_addr);
-
-
RTE_LOG(DEBUG, EAL, "Virtual area found at %p (size = 0x%zx)\n",
-
addr, *size);
-
-
/* increment offset */
-
baseaddr_offset += *size;
-
-
return addr;
-
}
下面看 find_physaddr的实现过程。
l find_physaddr
这个函数的作用就是找到hugepage数组里每个虚拟地址对应的物理地址,并存放到 phyaddr变量上,最终实现由函数rte_mem_virt2phy(const
void * virt)函数实现,其原理相当于页表查找,主要是通过linux的页表文件 /proc/self/pagemap 实现。/proc/self/pagemap 页表文件记录了本进程的页表,即本进程虚拟地址到物理地址的映射关系,主要是通过虚拟地址的前面若干位定位到物理页框,然后物理页框+虚拟地址偏移构成物理地址,其实现如下
-
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;
-
-
/* when using dom0, /proc/self/pagemap always returns 0, check in
-
* dpdk memory by browsing the memsegs */
-
if (rte_xen_dom0_supported()) {
-
struct rte_mem_config *mcfg;
-
struct rte_memseg *memseg;
-
unsigned i;
-
-
mcfg = rte_eal_get_configuration()->mem_config;
-
for (i = 0; i < RTE_MAX_MEMSEG; i++) {
-
memseg = &mcfg->memseg[i];
-
if (memseg->addr == NULL)
-
break;
-
if (virtaddr > memseg->addr &&
-
virtaddr < RTE_PTR_ADD(memseg->addr,
-
memseg->len)) {
-
return memseg->phys_addr +
-
RTE_PTR_DIFF(virtaddr, memseg->addr);
-
}
-
}
-
-
return RTE_BAD_PHYS_ADDR;
-
}
-
-
/* Cannot parse /proc/self/pagemap, no need to log errors everywhere */
-
if (!proc_pagemap_readable)
-
return RTE_BAD_PHYS_ADDR;
-
-
/* standard page size */
-
page_size = getpagesize();
-
-
fd = open("/proc/self/pagemap", O_RDONLY);
-
if (fd < 0) {
-
RTE_LOG(ERR, EAL, "%s(): cannot open /proc/self/pagemap: %s\n",
-
__func__, strerror(errno));
-
return RTE_BAD_PHYS_ADDR;
-
}
-
-
virt_pfn = (unsigned long)virtaddr / page_size;
-
offset = sizeof(uint64_t) * virt_pfn;
-
if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
-
RTE_LOG(ERR, EAL, "%s(): seek error in /proc/self/pagemap: %s\n",
-
__func__, strerror(errno));
-
close(fd);
-
return RTE_BAD_PHYS_ADDR;
-
}
-
-
retval = read(fd, &page, PFN_MASK_SIZE);
-
close(fd);
-
if (retval < 0) {
-
RTE_LOG(ERR, EAL, "%s(): cannot read /proc/self/pagemap: %s\n",
-
__func__, strerror(errno));
-
return RTE_BAD_PHYS_ADDR;
-
} else if (retval != PFN_MASK_SIZE) {
-
RTE_LOG(ERR, EAL, "%s(): read %d bytes from /proc/self/pagemap "
-
"but expected %d:\n",
-
__func__, retval, PFN_MASK_SIZE);
-
return RTE_BAD_PHYS_ADDR;
-
}
-
-
/*
-
* the pfn (page frame number) are bits 0-54 (see
-
* pagemap.txt in linux Documentation)
-
*/
-
physaddr = ((page & 0x7fffffffffffffULL) * page_size)
-
+ ((unsigned long)virtaddr % page_size);
-
-
return physaddr;
-
}
l find_numasocket
下面看 find_numasocket的实现过程这个函数的作用是找到hugepage数组里每个虚拟地址对应的物理cpu号,基本原理是通过linux提供的 /proc/self/numa_maps 文件,
/proc/self/numa_maps 文件记录了本 进程的虚拟地址与物理cpu号(多核系统)的对应关系,在遍历的时候将非huge page的虚拟地址过滤掉,剩下的虚拟地址与hugepage数组里的orig_va 比较,实现如下:
-
static int
-
find_numasocket(struct hugepage_file *hugepg_tbl, struct hugepage_info *hpi)
-
{
-
int socket_id;
-
char *end, *nodestr;
-
unsigned i, hp_count = 0;
-
uint64_t virt_addr;
-
char buf[BUFSIZ];
-
char hugedir_str[PATH_MAX];
-
FILE *f;
-
-
f = fopen("/proc/self/numa_maps", "r");
-
if (f == NULL) {
-
RTE_LOG(NOTICE, EAL, "cannot open /proc/self/numa_maps,"
-
" consider that all memory is in socket_id 0\n");
-
return 0;
-
}
-
-
snprintf(hugedir_str, sizeof(hugedir_str),
-
"%s/%s", hpi->hugedir, internal_config.hugefile_prefix);
-
-
/* parse numa map */
-
while (fgets(buf, sizeof(buf), f) != NULL) {
-
-
/* ignore non huge page */
-
if (strstr(buf, " huge ") == NULL &&
-
strstr(buf, hugedir_str) == NULL)
-
continue;
-
-
/* get zone addr */
-
virt_addr = strtoull(buf, &end, 16);
-
if (virt_addr == 0 || end == buf) {
-
RTE_LOG(ERR, EAL, "%s(): error in numa_maps parsing\n", __func__);
-
goto error;
-
}
-
-
/* get node id (socket id) */
-
nodestr = strstr(buf, " N");
-
if (nodestr == NULL) {
-
RTE_LOG(ERR, EAL, "%s(): error in numa_maps parsing\n", __func__);
-
goto error;
-
}
-
nodestr += 2;
-
end = strstr(nodestr, "=");
-
if (end == NULL) {
-
RTE_LOG(ERR, EAL, "%s(): error in numa_maps parsing\n", __func__);
-
goto error;
-
}
-
end[0] = '\0';
-
end = NULL;
-
-
socket_id = strtoul(nodestr, &end, 0);
-
if ((nodestr[0] == '\0') || (end == NULL) || (*end != '\0')) {
-
RTE_LOG(ERR, EAL, "%s(): error in numa_maps parsing\n", __func__);
-
goto error;
-
}
-
-
/* if we find this page in our mappings, set socket_id */
-
for (i = 0; i < hpi->num_pages[0]; i++) {
-
void *va = (void *)(unsigned long)virt_addr;
-
if (hugepg_tbl[i].orig_va == va) {
-
hugepg_tbl[i].socket_id = socket_id;
-
hp_count++;
-
}
-
}
-
}
-
-
if (hp_count < hpi->num_pages[0])
-
goto error;
-
-
fclose(f);
-
return 0;
-
-
error:
-
fclose(f);
-
return -1;
-
}
sort_by_physaddr 根据hugepage结构的phyaddr 排序,比较简单unmap_all_hugepages_orig 调用 mumap 系统调用将 hugepage结构的orig_va 虚拟地址返回给内核。
上面几步就完成了hugepage数组的构造,现在这个数组对应了某个hugetlbfs系统的大页面,数组的每一个节点是一个hugepage结构,该结构的phyaddr存放着该页面的物理内存地址,final_va存放着phyaddr映射到进程空间的虚拟地址,socket_id存放着物理cpu号,如果多个hugepage结构的final_va虚拟地址是连续的,则其 phyaddr物理地址也是连续的。
下面是rte_eal_hugepage_init函数的余下部分,我们知道之前的进程是对整个系统的可用页面进行mmap,但是我们进程实际并不需要这么多内存,所以需要对多余的内存进行释放,接下来的一段代码就是在做这个工作。
-
if (internal_config.memory == 0 && internal_config.force_sockets == 0)
-
internal_config.memory = eal_get_hugepage_mem_size();
-
-
nr_hugefiles = nr_hugepages;
-
-
-
/* clean out the numbers of pages */
-
/*清除hugepage_info中的page数量信息,因为之前将所有hugepage记录在了socket 0上*/
-
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;
-
/*根据之前查找的每个page的socket信息,重新更新每个socket上的hugepage计数*/
-
/* get hugepages for each 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]++;
-
}
-
}
-
}
-
/*memory[i] 记录着当前socket所需要申请的内存数量,这是通过参数指定的*/
-
/* make a copy of socket_mem, needed for number of pages calculation */
-
for (i = 0; i < RTE_MAX_NUMA_NODES; i++)
-
memory[i] = internal_config.socket_mem[i];
-
-
/* calculate final number of pages */
-
/*这个函数会根据当前进程指定所需要的实际内存大小计算出所需要实际的hugepage页面数量,之前我们mmap了系统的所有free的页面,但程序实际并不需要这么多,所以我们需要计算出实际需要的页面数量,而used_hp记录了实际需要内存大小的hugepage信息*/
-
nr_hugepages = calc_num_pages_per_socket(memory,
-
internal_config.hugepage_info, used_hp,
-
internal_config.num_hugepage_sizes);
-
-
/* error if not enough memory available */
-
if (nr_hugepages < 0)
-
goto fail;
-
-
/* reporting */
-
for (i = 0; i < (int) internal_config.num_hugepage_sizes; i++) {
-
for (j = 0; j < RTE_MAX_NUMA_NODES; j++) {
-
if (used_hp[i].num_pages[j] > 0) {
-
RTE_LOG(DEBUG, EAL,
-
"Requesting %u pages of size %uMB"
-
" from socket %i\n",
-
used_hp[i].num_pages[j],
-
(unsigned)
-
(used_hp[i].hugepage_sz / 0x100000),
-
j);
-
}
-
}
-
}
-
/*创建存放 hugepage_file结构的共享内存文件,注意这里nr_hugefiles是系统所有可用的页面数,而不仅是程序所需要的页面数*/
-
/* create shared memory */
-
hugepage = create_shared_memory(eal_hugepage_info_path(),
-
nr_hugefiles * sizeof(struct hugepage_file));
-
-
if (hugepage == NULL) {
-
RTE_LOG(ERR, EAL, "Failed to create shared memory!\n");
-
goto fail;
-
}
-
memset(hugepage, 0, nr_hugefiles * sizeof(struct hugepage_file));
-
-
/*
-
* unmap pages that we won't need (looks at used_hp).
-
* also, sets final_va to NULL on pages that were unmapped.
-
*/
-
/*对之前多mmap的页面进行unmmap,毕竟我们不需要那么多内存*/
-
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;
-
}
-
-
/*
-
* copy stuff from malloc'd hugepage* to the actual shared memory.
-
* this procedure only copies those hugepages that have final_va
-
* not NULL. has overflow protection.
-
*/
-
/*将hugepage_file数组拷贝到共享内存文件,注意只拷贝final_va不为NULL的结构,而前面我们已经将不需要的页面设置为NULL,所以这里只拷贝的是程序实际所需要的页面结构*/
-
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;
-
}
-
-
/*如果设置了internal_config.hugepage_unlink,则将程序使用的页面mmap文件进行unlink,即删除磁盘文件,注意这并不影响程序对内存的使用*/
-
/* free the hugepage backing files */
-
if (internal_config.hugepage_unlink &&
-
unlink_hugepage_files(tmp_hp, internal_config.num_hugepage_sizes) < 0) {
-
RTE_LOG(ERR, EAL, "Unlinking hugepage files failed!\n");
-
goto fail;
-
}
-
-
/* free the temporary hugepage table */
-
free(tmp_hp);
-
tmp_hp = NULL;
这一步之后,就会创建磁盘文件.rte_hugepage_info,其中存放着当前进程实际使用的的hugepage信息。并释放hugepage数组,其他进程通过映射 hugepage_info 文件就可以获取 hugepage数组,从而管理hugepage共享内存。
下面是rte_eal_hugepage_init函数的最后一部分。主要分两个方面,一是将hugepage数组里属于同一个物理cpu,物理内存连续的多个hugepage 用一层 memseg 结构管理起来。 一个memseg 结构维护的内存必然是同一个物理cpu上的,虚拟地址和物理地址都连续的内存,最终的memzone 接口是通过操作memseg实现的;2是将 hugepage数组和memseg数组的信息记录到共享文件里,方便从进程获取;
-
/* first memseg index shall be 0 after incrementing it below */
-
j = -1;
-
for (i = 0; i < nr_hugefiles; i++) {
-
new_memseg = 0;
-
-
/* if this is a new section, create a new memseg */
-
if (i == 0)
-
new_memseg = 1;
-
else if (hugepage[i].socket_id != hugepage[i-1].socket_id)
-
new_memseg = 1;
-
else if (hugepage[i].size != hugepage[i-1].size)
-
new_memseg = 1;
-
-
else if ((hugepage[i].physaddr - hugepage[i-1].physaddr) !=
-
hugepage[i].size)
-
new_memseg = 1;
-
else if (((unsigned long)hugepage[i].final_va -
-
(unsigned long)hugepage[i-1].final_va) != hugepage[i].size)
-
new_memseg = 1;
-
-
if (new_memseg) {/*新建的memseg,用首个hugepage作为初始值*/
-
j += 1;
-
if (j == RTE_MAX_MEMSEG)
-
break;
-
-
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;
-
}
-
/* continuation of previous memseg */
-
else {/*非新建的memseg*/
-
mcfg->memseg[j].len += mcfg->memseg[j].hugepage_sz;
-
}
-
hugepage[i].memseg_id = j;/*将memseg_id放在hugepage中*/
-
}
这个函数后,整个内存状态如下所示。
l rte_eal_memzone_init
最后我们看rte_eal_memzone_init函数,这个函数内部主要调用了rte_eal_malloc_heap_init,我们直接看这个函数。
-
int
-
rte_eal_malloc_heap_init(void)
-
{
-
struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
-
unsigned ms_cnt;
-
struct rte_memseg *ms;
-
-
if (mcfg == NULL)
-
return -1;
-
/*遍历所有memseg,对每个memseg调用malloc_heap_add_memseg*/
-
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_heap_add_memseg主要是为这个memseg创建相应的内存管理结构。
l malloc_heap_add_memseg
-
/*
-
* Expand the heap with a memseg.
-
* This reserves the zone and sets a dummy malloc_elem header at the end
-
* to prevent overflow. The rest of the zone is added to free list as a single
-
* large free block
-
*/
-
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 */
-
/*start_elem位于这个memseg(一段连续物理内存)的首部*/
-
struct malloc_elem *start_elem = (struct malloc_elem *)ms->addr;
-
/*end_elem位于这个memseg(一段连续物理内存)的尾部(还留了MALLOC_ELEM_OVERHEAD的空间防止越界)*/
-
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);
-
/*elem_size为这段连续内存的大小*/
-
const size_t elem_size = (uintptr_t)end_elem - (uintptr_t)start_elem;
-
/*初始化start_elem成员,其state设置为ELEM_FREE*/
-
malloc_elem_init(start_elem, heap, ms, elem_size);
-
/*初始化end_elem,其state设置为ELEM_BUSY,其pre指向start_elem*/
-
malloc_elem_mkend(end_elem, start_elem);
-
/*根据elem_size从free_head->free_head中找到合适的idx指向start_elem*/
-
malloc_elem_free_list_insert(start_elem);
-
-
heap->total_size += elem_size;
-
}
这段代码执行后,数据结构及内存关系就如下图所示。
下面说下如何根据elem_size(也就是memseg对应的连续内存大小)从free_head->free_head中找到合适的idx的。free_head->free_head按照连续内存大小,划分问若干个链表,如下所示:
*
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]