DPDK : 解析内存初始化的过程

说明

       这一篇文章主要是对DPDK的EAL(Environment Abstraction Layer)中内存的初始化的解析,这是DPDK内存管理的基础
       由于个人水平所限,若所写的博文中存在错误,希望大家能帮忙指出。

       注:
              1,在DPDK中,初始化由primary process完成。而其他process统称为secondary process,其可以通过读取一些文件来获取primary process的初始化信息,从而使得自身与primary process保持相同的内存映像。
              2, DPDK采用了一种集中式控制的方式,比如在多进程的场景中,若一个secondary process要申请内存,则向primary process发起请求,由primary process完成相应操作后在通知secondary process(DPDK : 进程间通信以及在内存管理的应用)。

一,初始化相关的代码入口

从lib/librte_eal/linux/eal/eal.c中的方法int rte_eal_init(int argc,char **argv)开始:

int
rte_eal_init(int argc, char **argv){
	······
	rte_config_init();
	if (internal_config.no_hugetlbfs == 0) {
		/* rte_config isn't initialized yet */
		ret = internal_config.process_type == RTE_PROC_PRIMARY ?
				eal_hugepage_info_init() :
				eal_hugepage_info_read();
		······
	}
	······
	if (rte_eal_memzone_init() < 0) { ······ }
	if (rte_eal_memory_init() < 0) { ······ }
    if (rte_eal_malloc_heap_init() < 0) { ······  }
}

从上面可以看出,内存的初始化过程主要包含5个方面的初始化, rte_config_init(), eal_hugepage_info_init(), rte_eal_memzone_init(), rte_eal_memory_init(), rte_eal_malloc_heap_init(), 下面依次对这五个方面进行解析:

二,相关的结构体的说明

在开始之前,先对一些结构体中关于内存的变量进行一些说明(使用······说明省略了一些变量,不影响理解DPDK内存初始化):

********************rte_eal.h********************
struct rte_config { //运行时环境的配置
	······
	/** PA or VA mapping mode */
	enum rte_iova_mode iova_mode; //指明了DMA使用虚拟地址(virtual address, 简称VA), 还是物理地址(physical address, 简称PA)

	/**
	 * Pointer to memory configuration, which may be shared across multiple
	 * DPDK instances
	 */
	 //这个指针指向的内存空间存放了一个DPDK instance的内存分布情况
	 //DPDK内存初始化过程主要是初始化struct rte_mem_config中的每一项
	struct rte_mem_config *mem_config;
} __attribute__((__packed__));
********************eal_internal_cfg.h*******************
struct hugepage_info {//用于存放每一个hugepage的信息
	uint64_t hugepage_sz;	/**< size of a huge page */
	char hugedir[PATH_MAX];	/**< dir where hugetlbfs is mounted */
	uint32_t num_pages[RTE_MAX_NUMA_NODES];	/**< number of hugepages of that size on each socket */

	int lock_descriptor;    /**< file descriptor for hugepage dir */
	//lock_descriptor指的是挂载点(即hugedir字段)对应的file descriptor
};

/**
 * internal configuration
 */
struct internal_config { //DPDK的全局配置信息
	volatile size_t memory;           /**< amount of asked memory */ 	
	//请求分配的内存数量
	·······
	volatile unsigned no_hugetlbfs;   /**< true to disable hugetlbfs */  
	//是否允许使用hugetlbfs
	
	unsigned hugepage_unlink;         /**< true to unlink backing files */ 
	//是否删除hugepage文件(DPDK在memalloc时将每一个hugepage当做一个文件处理)
	·······
	volatile unsigned no_shconf;      /**< true if there is no shared config */ 
	//是否允许共享,不允许的话primary process不会将初始化信息写入到文件 
	
	volatile unsigned in_memory;
	/**< true if DPDK should operate entirely in-memory and not create any
	 * shared files or runtime data.
	 */
	volatile enum rte_proc_type_t process_type; /**< multi-process proc type */  
	//用于区分primary process, 或者secondary process
	
	/** true to try allocating memory on specific sockets */
	volatile unsigned force_sockets; //强制在指定的socket上分配内存
	volatile uint64_t socket_mem[RTE_MAX_NUMA_NODES]; /**< amount of memory per socket */  
	//表示每一个socket分配的内存数量
	
	volatile unsigned force_socket_limits; //设置是否限制socket分配的内存
	volatile uint64_t socket_limit[RTE_MAX_NUMA_NODES]; /**< limit amount of memory per socket */  
	//每一个socket分配的内存的上限
	
	uintptr_t base_virtaddr;          /**< base address to try and reserve memory from */
	volatile unsigned legacy_mem; //指明是legacy mode, 或者dynamic mode
	/**< true to enable legacy memory behavior (no dynamic allocation,
	 * IOVA-contiguous segments).
	 */
	volatile unsigned match_allocations;
	/**< true to free hugepages exactly as allocated */
	volatile unsigned single_file_segments; //指明是single-file-segments mode, 或者 page-per-file mode
	/**< true if storing all pages within single files (per-page-size,
	 * per-node) non-legacy mode only.
	 */
	 
	unsigned num_hugepage_sizes;      /**< how many sizes on this system */
	struct hugepage_info hugepage_info[MAX_HUGEPAGE_SIZES];
};
********************rte_eal_memconfig.h*******************
struct rte_mem_config {
	volatile uint32_t magic;   /**< Magic number - Sanity check. */
	/* memory topology */
	uint32_t nchannel;    /**< Number of channels (0 if unknown). */
	uint32_t nrank;       /**< Number of ranks (0 if unknown). */
	······
	/* memory segments amemnd zones */
	struct rte_fbarray memzones; /**< Memzone descriptors. */
	
	//每一个struct rte_memseg_list中使用进行标识
	//memsegs 可能存在多个具有相同的struct rte_memseg_list
	struct rte_memseg_list memsegs[RTE_MAX_MEMSEG_LISTS];
	/**< list of dynamic arrays holding memsegs */
	······
	/* Heaps of Malloc */
	struct malloc_heap malloc_heaps[RTE_MAX_HEAPS];
    ······
	/* address of mem_config in primary process. used to map shared config into
	 * exact same address the primary process maps it.
	 */
	uint64_t mem_cfg_addr; //这个地址等于struct rte_config中的struct rte_mem_config *mem_config

	/* legacy mem and single file segments options are shared */
	uint32_t legacy_mem;	//指明内存是legacy mode, 还是dynamic mode
	uint32_t single_file_segments; // 指明memalloc是single-file-segments mode, 还是page-per-file mode
    ······
} __attribute__((__packed__));

struct rte_memseg_list {//每一个rte_memseg_list由page_sz和socket_id唯一标识
	RTE_STD_C11
	union {
		void *base_va;
		/**< Base virtual address for this memseg list. */
		uint64_t addr_64;
		/**< Makes sure addr is always 64-bits */
	};//指向一块用于存放rte_memseg的内存空间
	uint64_t page_sz; /**< Page size for all memsegs in this list. */
	int socket_id; /**< Socket ID for all memsegs in this list. */
	······
	size_t len; /**< Length of memory area covered by this memseg list. */
	 //指明具有base_va所指向的内存空间的字节数总量
	······
	struct rte_fbarray memseg_arr; //用于管理base_va指向的内存空间,包含rte_memseg相关的元数据
};

********************rte_memory.h*******************
struct rte_memseg { //一个rte_memseg等同于一个hugepage
	RTE_STD_C11
	union {
		phys_addr_t phys_addr;  /**< deprecated - Start physical address. */
		rte_iova_t iova;        /**< Start IO address. */
	};
	RTE_STD_C11
	union {
		void *addr;         /**< Start virtual address. */
		uint64_t addr_64;   /**< Makes sure addr is always 64 bits */
	};
	size_t len;               /**< Length of the segment. */
	uint64_t hugepage_sz;       /**< The pagesize of underlying memory */
	int32_t socket_id;          /**< NUMA socket ID. */
	uint32_t nchannel;          /**< Number of channels. */
	uint32_t nrank;             /**< Number of ranks. */
	uint32_t flags;             /**< Memseg-specific flags */
} __rte_packed;
********************eal_hugepages.h*******************
struct hugepage_file {
	void *orig_va;      /**< virtual addr of first mmap() */
	void *final_va;     /**< virtual addr of 2nd mmap() */
	uint64_t physaddr;  /**< physical addr */
	size_t size;        /**< the page size */
	int socket_id;      /**< NUMA socket ID */
	int file_id;        /**< the '%d' in HUGEFILE_FMT */  	//这是第file_id个大小为size的hugepage
	
	char filepath[MAX_HUGEPAGE_PATH]; /**< path to backing file on filesystem */
	//filepath指明hugepage对应的文件
};

三,对于primary process的内存初始化过程:

1, rte_config_init() :

       这个函数主要是为struct rte_config中的struct rte_mem_config *mem_config(简称mcfg)申请一块内存空间,并且在运行时目录下创建一个名字为config的文件,并且将mcfg的内容写进此文件。这样,secondary process在初始化时就能通过读取config文件来创建和primary process一样的内存映像。

       在这里采用了mmap()的方式将config文件和mcfg进行了映射,所以在后面的初始化操作中,一旦对config进行了写操作,也能够立刻反映到其他的进程中(类似于使用共享内存通信)。

2, eal_hugepage_info_init() : 读取系统中的hugepage的信息。

       在linux系统中,会打开系统目录/sys/kernel/mm/hugepages,遍历每一个目录项下获取系统支持的hugepage size。然后从/proc/mounts中根据hugepage size获取对应挂载点(mount point), 然后计算在不同socket中每一种free hugepage的数量,。将每一种大页的相关信息存放在internal_config->hugepage_info中。

       然后会在runtime dir下创建一个名字为hugepage_info的文件,将internal_config->hugepage_info写入到该文件。

3,rte_eal_memzone_init()

       初始化mcfg->memzones, 申请一块内存空间,用于保存以后内存分配时使用到的struct memzone。(memzone所使用的内存空间也是从rte_heap中分配的,rte_memzone_reserve和rte_malloc之间的区别尚未解决)

4,rte_eal_memory_init() : 这是内存初始化过程的核心,其中包括了memseg_primary_init(), eal_memalloc_init(), rte_eal_hugepage_init(), rte_eal_memdevice_init().

       1), memseg_primary_init() :

	确定每一种类型(由socket id和page sz确定)的struct rte_memseg_list的数量,及其所包含的mem segment的数量。
	然后,根据确定的数量为mcfg->memsegs中的struct rte_memseg_list分配虚拟内存空间。

       2), eal_memalloc_init() :

	初始化struct fd_list
		如果是single-file-segments mode, 则对于一个rte_memseg_list,只使用一个file descriptor(fd_list中的memseg_list_fd)
		如果是file-per-page, 则对于一个rte_memseg_list中的每一个mem segment, 都会使用一个file descriptor(fd_list中的fds)

       3), rte_eal_hugepage_init()

	*如果是legacy mem, 则调用eal_legacy_hugepage_init()
		1), 根据internal_config->hugepage_info初始化hugepage_file, 并且将这些hugepage_file, 根据
			对应的rte_memseg_list中的rte_memseg进行映射。
		2),然后对hugepage_file排序(使得按照页的size降序排序,同一种size按照物理地址升序排序),
			然后根据internal_config->socket_mem计算hugepage在不同socket的分布,
		3),之后对所有的hugepage_file进行重映射,使得虚拟内存连续的mem segments在物理内存上也是连续的,
			并且同一个rte_memseg_list所有的mem_sgement的虚拟地址和物理地址都是单调递增。
		4),接着,设置fd_list中对应的file descriptor。
		5),这个方法会将hugepage_file写入到hugepage_data文件。
		
		在实现的过程中采用了read-ahead, 目的是为了保证虚拟内存连续的mem segments在物理内存上也是连续的,
	同时也能够提前载入物理页,提高系统的性能。
		而对于nohugepage的情况,将其视为legacy, single-file mode,采用的页的大小为4K。
		
	*如果是dynamic mem, 则调用eal_hugepage_init()
		根据socket_mem的需求,计算hugepage在不同socket的分布。然后使用了eal_memalloc_alloc_seg_bluk进行分配,
	  由于这个方法是一个一个mem segment进行分配,所以不能保证分配完成后,虚拟空间上连续的mem segments在物理上也是连续的.
		采用了pre-allocate,能够提高系统的性能。

       4), rte_eal_memdevice_init().:

	设置mcfg->nchannel, mcfg->nrank
5, rte_eal_malloc_heap_init()

       初始化mcfg->malloc_heaps;并且注册进程间通信的handle,用于多进程环境下的内存分配;初始化heap的结构。
       其中每一个socket会对应一个heap。

初始化完成后heap的结构如下(一个例子) :

DPDK : 解析内存初始化的过程_第1张图片

       假设系统支持两种大小的hugepage(2MB, 1GB)
       上图的heap包含两个rte_memseg_list, 每一个都包含3个contiguous mem segments(其中可能包含一个或多个hugepage), 总共有6个contiguous mem segments(图中浅黄色的部分). 每一个contigous mem segments都包含一个malloc_elem,用于记录此contiguous mem segments的元数据。每一个struct malloc_heap都会指向第一个malloc_elem和最后一个malloc_elem;并且一个heap中,所有的malloc_elem会组成一个双向链表。

四,对于secondary process的内存初始化过程:

1, rte_config_init()

       使用mmap()将config文件映射到此进程的mcfg,这样可以直接读取primary process的内存映像.

2, eal_hugepage_info_init() :

       读取hugepage文件的内容,并保存在struct internal_config->hugepage_info中。

3,rte_eal_memzone_init()

       根据config文件中关于memzones的内容, 创建一个和primary process具有相同内存映像的mcfg->memzones

4,rte_eal_memory_init() : 这是内存初始化过程的核心,其中包括了memseg_secondary_init(), eal_memalloc_init(), rte_eal_hugepage_attach(), rte_eal_memdevice_init().

       1), memseg_secondary_init() :

	直接根据**config**文件的内容创建和primary process相同的虚拟内存空间视图。

       2), eal_memalloc_init() :

	对mcfg中的struct rte_memseg_list, 创建一个本地副本(即local_memsegs),用于同步memory hotplug
	初始化struct fd_list
		如果是single-file-segments mode, 则对于一个rte_memseg_list,只使用一个file descriptor(fd_list中的memseg_list_fd)
		如果是file-per-page, 则对于一个rte_memseg_list中的每一个mem segment, 都会使用一个file descriptor(fd_list中的fds)

       3)rte_eal_hugepage_attach()

	如果是legacy mem, eal_legacy_hugepage_attach()
		读取hugepage_data文件,根据文件的内容建立与primary process相应的内存映像,并且设置fd_list中相应的file descriptor.
	如果是dynamic mem, eal_hugepage_attach()
		调用eal_memalloc_sync_with_primary(), 将primary process的mcfg->memsegs同步到此进程的local_memsegs.

        4)rte_eal_memdevice_init() :

	不做任何操作.
5, rte_eal_malloc_heap_init()

       初始化mcfg->malloc_heaps;并且注册进程间通信的handle,用于多进程环境下的内存分配;初始化heap的结构。

五,总结

       1,如果没有采用hugetlbfs,则默认采用系统页(大小为4K)
       2, DPDK有两种内存模式 :

	legacy mode : 保证虚拟空间连续的contiguous mem segments在物理空间上也是连续的
	dynamic mode : 分配hugepage时是一个一个分配的,不能和legacy mode有一样的保证

       3, DPDK在memalloc时有两种模式single-file-segments, page-per-file, 每一种都在hugetlbfs的挂载点上有相应的文件形式(即存在于内存中的文件),这样在内存分配时可以使用对file descriptor操作的系统调用对内存进行操作。
       4,每一个socket有一个heap, 每一个heap包含若干个rte_memseg_list, 每一个rte_memseg_list包含若干rte_memseg, 一个rte_memseg对应于一个memory page
       5, 在分配内存时,采用了read-ahead, pre-allocated等方法,能够减少由于页错误而阻塞的情况,提高系统的性能。

你可能感兴趣的:(DPDK源代码的解析)