從原理到實(shí)踐:掌握DPDK內(nèi)存池技術(shù)
共 44219字,需瀏覽 89分鐘
·
2024-04-29 08:00
前言:本文整理下之前的學(xué)習(xí)筆記,基于DPDK17.11版本源碼分析。主要分析一下內(nèi)存管理部分代碼。
一、概述
內(nèi)存管理是數(shù)據(jù)面開(kāi)發(fā)套件(DPDK)的一個(gè)核心部分,以此為基礎(chǔ),DPDK的其他部分和用戶應(yīng)用得以發(fā)揮其最佳性能。本系列文章將詳細(xì)介紹DPDK提供的各種內(nèi)存管理的功能。
但在此之前,有必要先談一談為何DPDK中內(nèi)存管理要以現(xiàn)有的方式運(yùn)作,它背后又有怎樣的原理,再進(jìn)一步探討DPDK具體能夠提供哪些與內(nèi)存相關(guān)的功能。本文將先介紹DPDK內(nèi)存的基本原理,并解釋它們是如何幫助DPDK實(shí)現(xiàn)高性能的。
請(qǐng)注意,雖然DPDK支持FreeBSD,而且也會(huì)有正在運(yùn)行的Windows端口,但目前大多數(shù)與內(nèi)存相關(guān)的功能僅適用于Linux。
先看一下下面的圖片,其中左邊部分為DPDK內(nèi)存層級(jí)結(jié)構(gòu),下面三層在rte_eal_init初始化時(shí)完成,上面三層由用戶調(diào)用API生成。右邊為每層內(nèi)存結(jié)構(gòu)提供的API,供上層或者APP使用。
下圖為內(nèi)存管理相關(guān)的數(shù)據(jù)結(jié)構(gòu),其中rte_config->mem_config指向共享內(nèi)存文件 /var/run/.rte_config,對(duì)應(yīng)的結(jié)構(gòu)體為struct rte_mem_config,其中mem_cfg_addr保存了映射文件/var/run/.rte_config后的虛擬地址,從進(jìn)程使用此值進(jìn)行映射,保證主從進(jìn)程可以使用相同的虛擬地址訪問(wèn)結(jié)構(gòu)體rte_mem_config。
此共享內(nèi)存struct rte_mem_config還保存了如下幾個(gè)重要的結(jié)構(gòu)體:
a. memseg: 將同時(shí)滿足下面四個(gè)條件的大頁(yè)放在一個(gè)memseg中
1. 同socket
2. hugepage 大小相同
3. 物理地址連續(xù)
4. 虛擬地址連續(xù)
b. malloc_heap: 將相同socket的memseg掛在同一個(gè)malloc_heap上,對(duì)外提供API的最底層實(shí)現(xiàn)
c. memzone: 用來(lái)申請(qǐng)整塊內(nèi)存
d. tailq_head: 共享隊(duì)列實(shí)現(xiàn),主從進(jìn)程可以同時(shí)訪問(wèn)。
內(nèi)存初始化流程如下,后面會(huì)詳細(xì)看每個(gè)函數(shù)的實(shí)現(xiàn)。
rte_eal_init
//internal_config為本進(jìn)程全局變量,用來(lái)保存參數(shù)等信息
eal_reset_internal_config(&internal_config);
//解析參數(shù),保存到 internal_config
eal_parse_args(argc, argv);
//收集系統(tǒng)上可用的大頁(yè)內(nèi)存,保存到 internal_config->hugepage_info[3]
eal_hugepage_info_init();
//初始化全局變量rte_config,并將 rte_config->mem_config 進(jìn)行映射
rte_config_init();
//大頁(yè)內(nèi)存映射,并保存到 rte_config->mem_config->memseg[]
rte_eal_memory_init();
//將 memseg 插入 rte_config->mem_config->malloc_heaps[]
rte_eal_memzone_init();
//主進(jìn)程初始化完成
rte_eal_mcfg_complete();
/* ALL shared mem_config related INIT DONE */
//主進(jìn)程完成了所有共享內(nèi)存的初始化,設(shè)置RTE_MAGIC。
//從進(jìn)程在等待magic變成RTE_MAGIC后才能繼續(xù)下去。
if (rte_config.process_type == RTE_PROC_PRIMARY)
rte_config.mem_config->magic = RTE_MAGIC;
二、標(biāo)準(zhǔn)大頁(yè)
現(xiàn)代CPU架構(gòu)中,內(nèi)存管理并不以單個(gè)字節(jié)進(jìn)行,而是以頁(yè)為單位,即虛擬和物理連續(xù)的內(nèi)存塊。這些內(nèi)存塊通常(但不是必須) 存儲(chǔ)在RAM中。在英特爾?64和IA-32架構(gòu)上,標(biāo)準(zhǔn)系統(tǒng)的頁(yè)面大小為4KB。
基于安全性和通用性的考慮,軟件的應(yīng)用程序訪問(wèn)的內(nèi)存位置使用的是操作系統(tǒng)分配的虛擬地址。運(yùn)行代碼時(shí),該虛擬地址需要被轉(zhuǎn)換為硬件使用的物理地址。這種轉(zhuǎn)換是操作系統(tǒng)通過(guò)頁(yè)表轉(zhuǎn)換來(lái)完成的,頁(yè)表在分頁(yè)粒度級(jí)別上(即4KB一個(gè)粒度)將虛擬地址映射到物理地址。為了提高性能,最近一次使用的若干頁(yè)面地址被保存在一個(gè)稱為轉(zhuǎn)換檢測(cè)緩沖區(qū)(TLB)的高速緩存中。每一分頁(yè)都占有TLB的一個(gè)條目。如果用戶的代碼訪問(wèn)(或最近訪問(wèn)過(guò))16 KB的內(nèi)存,即4頁(yè),這些頁(yè)面很有可能會(huì)在TLB緩存中。
如果其中一個(gè)頁(yè)面不在TLB緩存中,嘗試訪問(wèn)該頁(yè)面中包含的地址將導(dǎo)致TLB查詢失敗;也就是說(shuō),操作系統(tǒng)寫(xiě)入TLB的頁(yè)地址必須是在它的全局頁(yè)表中進(jìn)行查詢操作獲取的。因此,TLB查詢失敗的代價(jià)也相對(duì)較高(某些情況下代價(jià)會(huì)非常高),所以最好將當(dāng)前活動(dòng)的所有頁(yè)面都置于TLB中以盡可能減少TLB查詢失敗。
然而,TLB的大小有限,而且實(shí)際上非常小,和DPDK通常處理的數(shù)據(jù)量(有時(shí)高達(dá)幾十GB)比起來(lái),在任一給定的時(shí)刻,4KB 標(biāo)準(zhǔn)頁(yè)面大小的TLB所覆蓋的內(nèi)存量(幾MB)微不足道。這意味著,如果DPDK采用常規(guī)內(nèi)存,使用DPDK的應(yīng)用會(huì)因?yàn)門(mén)LB頻繁的查詢失敗在性能上大打折扣。
為解決這個(gè)問(wèn)題,DPDK依賴于標(biāo)準(zhǔn)大頁(yè)。從名字中很容易猜到,標(biāo)準(zhǔn)大頁(yè)類似于普通的頁(yè)面,只是會(huì)更大。有多大呢?在英特爾?64和1A-32架構(gòu)上,目前可用的兩種大頁(yè)大小為2MB和1GB。也就是說(shuō),單個(gè)頁(yè)面可以覆蓋2 MB或1 GB大小的整個(gè)物理和虛擬連續(xù)的存儲(chǔ)區(qū)域。
這兩種頁(yè)面大小DPDK都可以支持。有了這樣的頁(yè)面大小,就可以更容易覆蓋大內(nèi)存區(qū)域,也同時(shí)避免(同樣多的)TLB查詢失敗。反過(guò)來(lái),在處理大內(nèi)存區(qū)域時(shí),更少的TLB查詢失敗也會(huì)使性能得到提升,DPDK的用例通常如此。
2.1eal_hugepage_info_init 收集可用大頁(yè)內(nèi)存
遍歷/sys/kernel/mm/hugepages下面的目錄收集可用的大頁(yè)。
/*
* when we initialize the hugepage info, everything goes
* to socket 0 by default. it will later get sorted by memory
* initialization procedure.
*/
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;
//打開(kāi)目錄 /sys/kernel/mm/hugepages
dir = opendir(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]);
//打開(kāi)文件 /proc/mounts 遍歷所有掛載點(diǎn),找到 hugetlbfs
//相關(guān)的掛載點(diǎn),再找到和hpi->hugepage_sz相等的掛載
//點(diǎn),返回其掛載目錄。
hpi->hugedir = get_hugepage_dir(hpi->hugepage_sz);
//如果對(duì)應(yīng)hugepage_sz的掛載點(diǎn),則跳過(guò)此種類型大頁(yè)
/* 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 */
flock(hpi->lock_descriptor, LOCK_EX);
//刪除掛載點(diǎn)目錄下以"map_"開(kāi)頭的文件
/* clear out the hugepages dir from unused pages */
clear_hugedir(hpi->hugedir);
//獲取空閑的大頁(yè)數(shù)量,free_hugepages減去
//resv_hugepages。此時(shí)還不知道大頁(yè)位于哪個(gè)socket,
//所以先將大頁(yè)數(shù)量統(tǒng)一放到socket0上
/* for now, put all pages into socket 0,
* later they will be sorted */
hpi->num_pages[0] = get_num_hugepages(dirent->d_name);
//大頁(yè)種類加一
num_sizes++;
}
closedir(dir);
//保存大頁(yè)種類: 2M, 1G等
internal_config.num_hugepage_sizes = num_sizes;
//按照大頁(yè)大小排序 hugepage_info,從大到小
/* 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);
//如果有可用的大頁(yè),返回0,否則返回-1
/* 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;
}
rte_config_init
映射文件 /var/run/.rte_config,用來(lái)保存結(jié)構(gòu)體 rte_config->mem_config 的內(nèi)容。主進(jìn)程映射此文件,將映射后的虛擬地址保存到 rte_config->mem_config->mem_cfg_addr,從進(jìn)程需要映射兩次此文件,第一次是為了獲取主進(jìn)程使用的虛擬地址mem_cfg_addr,然后再使用此虛擬地址進(jìn)行映射,這樣保存主從進(jìn)程可以使用相同的虛擬地方訪問(wèn) rte_config->mem_config指向的共享內(nèi)存。
/* Sets up rte_config structure with the pointer to shared memory config.*/
static void
rte_config_init(void)
{
rte_config.process_type = internal_config.process_type;
switch (rte_config.process_type){
case RTE_PROC_PRIMARY:
//主進(jìn)程初始化
rte_eal_config_create();
break;
case RTE_PROC_SECONDARY:
//從進(jìn)程attach
rte_eal_config_attach();
//等待主進(jìn)程初始化完成
rte_eal_mcfg_wait_complete(rte_config.mem_config);
//再次attach
rte_eal_config_reattach();
break;
case RTE_PROC_AUTO:
case RTE_PROC_INVALID:
rte_panic("Invalid process type\n");
}
}
主進(jìn)程映射文件 /var/run/.rte_config
/* create memory configuration in shared/mmap memory. Take out
* a write lock on the memsegs, so we can auto-detect primary/secondary.
* This means we never close the file while running (auto-close on exit).
* We also don't lock the whole file, so that in future we can use read-locks
* on other parts, e.g. memzones, to detect if there are running secondary
* processes. */
static void
rte_eal_config_create(void)
{
void *rte_mem_cfg_addr;
int retval;
//獲取config所在路徑 /var/run/.rte_config
const char *pathname = eal_runtime_config_path();
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;
//打開(kāi)config文件路徑 /var/run/.rte_config
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);
}
//將文件大小修改為 mem_config 的長(zhǎng)度
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);
}
//給文件mem_cfg_fd加上鎖,可以調(diào)用eal_proc_type_detect檢測(cè)是主進(jìn)程還是從進(jìn)程。
//只有主進(jìn)程能lock成功。
static struct flock wr_lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = offsetof(struct rte_mem_config, memseg),
.l_len = sizeof(early_mem_config.memseg),
};
fcntl(mem_cfg_fd, F_SETLK, &wr_lock);
//映射config文件
rte_mem_cfg_addr = mmap(rte_mem_cfg_addr, sizeof(*rte_config.mem_config),
PROT_READ | PROT_WRITE, MAP_SHARED, mem_cfg_fd, 0);
//將early_mem_config內(nèi)容保存到共享內(nèi)存 rte_mem_cfg_addr
memcpy(rte_mem_cfg_addr, &early_mem_config, sizeof(early_mem_config));
//將映射后的虛擬地址保存到 rte_config.mem_config
rte_config.mem_config = rte_mem_cfg_addr;
//最后將映射后的虛擬地址 rte_mem_cfg_addr 保存到共享內(nèi)存 mem_config->mem_cfg_addr,
//以便從進(jìn)程獲取后映射相同的虛擬地址。
/* 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;
}
因?yàn)橹挥兄鬟M(jìn)程能lock,所以可以使用函數(shù)eal_proc_type_detect檢查當(dāng)前進(jìn)程是主進(jìn)程還是從進(jìn)程。
/* Detect if we are a primary or a secondary process */
enum rte_proc_type_t
eal_proc_type_detect(void)
{
enum rte_proc_type_t ptype = RTE_PROC_PRIMARY;
const char *pathname = eal_runtime_config_path();
/* if we can open the file but not get a write-lock we are a secondary
* process. NOTE: if we get a file handle back, we keep that open
* and don't close it to prevent a race condition between multiple opens */
if (((mem_cfg_fd = open(pathname, O_RDWR)) >= 0) &&
(fcntl(mem_cfg_fd, F_SETLK, &wr_lock) < 0))
ptype = RTE_PROC_SECONDARY;
RTE_LOG(INFO, EAL, "Auto-detected process type: %s\n",
ptype == RTE_PROC_PRIMARY ? "PRIMARY" : "SECONDARY");
return ptype;
}
從進(jìn)程第一此映射/var/run/.rte_config,可能在主進(jìn)程之前就映射了,映射后主從進(jìn)程都可以操作這塊內(nèi)存,但主要目的還是為了獲取主進(jìn)程的虛擬地址 mem_cfg_addr。
/* attach to an existing shared memory config */
static void
rte_eal_config_attach(void)
{
struct rte_mem_config *mem_config;
//獲取config所在路徑 /var/run/.rte_config
const char *pathname = eal_runtime_config_path();
if (internal_config.no_shconf)
return;
//打開(kāi)文件 /var/run/.rte_config
if (mem_cfg_fd < 0){
mem_cfg_fd = open(pathname, O_RDWR);
if (mem_cfg_fd < 0)
rte_panic("Cannot open '%s' for rte_mem_config\n", pathname);
}
//映射config文件,只讀模式。而且第一個(gè)參數(shù)為NULL,即不知道虛擬地址
/* map it as read-only first */
mem_config = (struct rte_mem_config *) mmap(NULL, sizeof(*mem_config),
PROT_READ, MAP_SHARED, mem_cfg_fd, 0);
if (mem_config == MAP_FAILED)
rte_panic("Cannot mmap memory for rte_config! error %i (%s)\n",
errno, strerror(errno));
rte_config.mem_config = mem_config;
}
等待主進(jìn)程初始化完成。主進(jìn)程初始化完成后會(huì)調(diào)用rte_eal_mcfg_complete將mcfg->magic設(shè)置為 RTE_MAGIC。
inline static void
rte_eal_mcfg_wait_complete(struct rte_mem_config* mcfg)
{
/* wait until shared mem_config finish initialising */
while(mcfg->magic != RTE_MAGIC)
rte_pause();
}
主進(jìn)程初始化成功后,從進(jìn)程就可以使用主進(jìn)程映射的虛擬地址 mem_cfg_addr進(jìn)行第二次映射
/* reattach the shared config at exact memory location primary process has it */
static void
rte_eal_config_reattach(void)
{
struct rte_mem_config *mem_config;
void *rte_mem_cfg_addr;
if (internal_config.no_shconf)
return;
//從共享配置文件獲取主進(jìn)程映射的虛擬地址 mem_cfg_addr
/* save the address primary process has mapped shared config to */
rte_mem_cfg_addr = (void *) (uintptr_t) rte_config.mem_config->mem_cfg_addr;
//去除從進(jìn)程之前的對(duì)mem_config的映射
/* unmap original config */
munmap(rte_config.mem_config, sizeof(struct rte_mem_config));
//指定主進(jìn)程映射的虛擬地址 mem_cfg_addr重新映射,獲取和主進(jìn)程一樣的虛擬地址
/* remap the config at proper address */
mem_config = (struct rte_mem_config *) mmap(rte_mem_cfg_addr,
sizeof(*mem_config), PROT_READ | PROT_WRITE, MAP_SHARED,
mem_cfg_fd, 0);
if (mem_config == MAP_FAILED || mem_config != rte_mem_cfg_addr) {
if (mem_config != MAP_FAILED)
/* errno is stale, don't use */
rte_panic("Cannot mmap memory for rte_config at [%p], got [%p]"
" - please use '--base-virtaddr' option\n",
rte_mem_cfg_addr, mem_config);
else
rte_panic("Cannot mmap memory for rte_config! error %i (%s)\n",
errno, strerror(errno));
}
close(mem_cfg_fd);
//將映射后的mem_config保存到從進(jìn)程本地變量 rte_config.mem_config
rte_config.mem_config = mem_config;
}
這樣主從進(jìn)程都能使用相同的虛擬地址訪問(wèn)共享配置 rte_config.mem_config。
2.2rte_eal_memory_init映射大頁(yè)內(nèi)存
主進(jìn)程先映射,將映射后的虛擬地址保存到文件/var/run/.rte_hugepage_info中,從進(jìn)程讀取此文件,以相同的虛擬地址進(jìn)行映射,保證主從進(jìn)程以相同的地址訪問(wèn)大頁(yè)內(nèi)存,這也是實(shí)現(xiàn)進(jìn)程間傳遞報(bào)文零拷貝的關(guān)鍵。
/* init memory subsystem */
int
rte_eal_memory_init(void)
{
RTE_LOG(DEBUG, EAL, "Setting up physically contiguous memory...\n");
const int retval = rte_eal_process_type() == RTE_PROC_PRIMARY ?
rte_eal_hugepage_init() :
rte_eal_hugepage_attach();
if (retval < 0)
return -1;
if (internal_config.no_shconf == 0 && rte_eal_memdevice_init() < 0)
return -1;
return 0;
}
主進(jìn)程調(diào)用 rte_eal_hugepage_init 進(jìn)行大頁(yè)映射,并根據(jù)分類保存到memseg中。
/*
* 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
*/
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;
//測(cè)試物理地址是否可用,如果參數(shù)指定no_hugetlbfs不用大頁(yè)或者
//通過(guò)虛擬地址不能獲取物理地址,則認(rèn)為物理地址不可用,則設(shè)置phys_addrs_available為false。
test_phys_addrs_available();
memset(used_hp, 0, sizeof(used_hp));
//獲取全局共享配置 mem_config
/* get pointer to global configuration */
mcfg = rte_eal_get_configuration()->mem_config;
//遍歷 num_hugepage_sizes 中大頁(yè)內(nèi)存
/* 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;
//獲取所有大頁(yè)的個(gè)數(shù)
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.
*/
//分配整塊內(nèi)存,用于保存 nr_hugepages 個(gè) struct hugepage_file
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 */
//internal_config.socket_mem[i] 保存的是參數(shù) --socket-mem 1024,1024 指定的每個(gè)socket上的內(nèi)存。
//復(fù)制一份到局部變量 memory 中
/* make a copy of socket_mem, needed for balanced allocation. */
for (i = 0; i < RTE_MAX_NUMA_NODES; i++)
memory[i] = internal_config.socket_mem[i];
//開(kāi)始映射大頁(yè)內(nèi)存,主要工作為函數(shù)前面注釋的前6條。
/* map all hugepages and sort them */
//遍歷當(dāng)前系統(tǒng)上配置的幾種大頁(yè)內(nèi)存
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];
//如果為0,說(shuō)明此種大頁(yè)沒(méi)有free可用的,跳過(guò)
if (hpi->num_pages[0] == 0)
continue;
/* map all hugepages available */
//先獲取當(dāng)前此種類型的大頁(yè)個(gè)數(shù)
pages_old = hpi->num_pages[0];
//映射大頁(yè),返回實(shí)際成功應(yīng)該的個(gè)數(shù)。
//map_all_hugepages最后一個(gè)參數(shù)為1,表示第一次映射,將映射后的虛擬地址保存到 hugepg_tbl[i].orig_va,
//后面再單獨(dú)分析此函數(shù)
pages_new = map_all_hugepages(&tmp_hp[hp_offset], hpi, memory, 1);
if (pages_new < pages_old) {
RTE_LOG(DEBUG, EAL,
"%d not %d hugepages of size %u MB allocated\n",
pages_new, pages_old,
(unsigned)(hpi->hugepage_sz / 0x100000));
//獲取未映射成功的大頁(yè)個(gè)數(shù)
int pages = pages_old - pages_new;
//更新總大頁(yè)個(gè)數(shù)
nr_hugepages -= pages;
//保存映射成功的大頁(yè)
hpi->num_pages[0] = pages_new;
//如果映射成功的大頁(yè)個(gè)數(shù)為0,直接跳過(guò),開(kāi)始下一種大頁(yè)的映射
if (pages_new == 0)
continue;
}
//物理地址可用,則調(diào)用 find_physaddrs 獲取映射后的虛擬地址對(duì)應(yīng)的物理地址,
//并保存到 hugepg_tbl[i].physaddr
if (phys_addrs_available) {
/* find physical addresses 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 int)(hpi->hugepage_sz / 0x100000));
goto fail;
}
} else {
/* set physical addresses for each hugepage */
if (set_physaddrs(&tmp_hp[hp_offset], hpi) < 0) {
RTE_LOG(DEBUG, EAL, "Failed to set phys addr "
"for %u MB pages\n",
(unsigned int)(hpi->hugepage_sz / 0x100000));
goto fail;
}
}
//根據(jù)映射后的huge文件(rte_map0等)獲取socket,并保存到hugepg_tbl[i].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;
}
//根據(jù)物理地址從小到大排序 tmp_hp
qsort(&tmp_hp[hp_offset], hpi->num_pages[0],
sizeof(struct hugepage_file), cmp_physaddr);
//重新映射大頁(yè)內(nèi)存,這次最后一個(gè)參數(shù)為0,會(huì)將映射后的虛擬地址保存到 hugepg_tbl[i].final_va,
//這也是最終的虛擬地址。
//重新映射的目的是為了盡量找到物理地址和虛擬地址都連續(xù)的大頁(yè)內(nèi)存。
/* remap all hugepages */
if (map_all_hugepages(&tmp_hp[hp_offset], hpi, NULL, 0) !=
hpi->num_pages[0]) {
RTE_LOG(ERR, EAL, "Failed to remap %u MB pages\n",
(unsigned)(hpi->hugepage_sz / 0x100000));
goto fail;
}
//解除第一次映射的虛擬地址 hugepg_tbl[i].orig_va
/* unmap original mappings */
if (unmap_all_hugepages_orig(&tmp_hp[hp_offset], hpi) < 0)
goto fail;
//偏移,遍歷下一種大頁(yè)
/* we have processed a num of hugepages of this size, so inc offset */
hp_offset += hpi->num_pages[0];
}
//如果沒(méi)有通過(guò)參數(shù) -m 或者 --socket-mem 指定內(nèi)存,則獲取當(dāng)前所有大頁(yè)內(nèi)存總和
if (internal_config.memory == 0 && internal_config.force_sockets == 0)
internal_config.memory = eal_get_hugepage_mem_size();
nr_hugefiles = nr_hugepages;
//清空hugepage_info的大頁(yè)個(gè)數(shù)
/* clean out the numbers of pages */
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;
//前面獲取了大頁(yè)所在socket,這里計(jì)算每種大頁(yè)在各個(gè)socket上的大頁(yè)個(gè)數(shù)
/* 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]++;
}
}
}
//復(fù)制參數(shù)指定的socket內(nèi)存到memory
/* 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];
//計(jì)算最后需要的大頁(yè)個(gè)數(shù)。
//前面映射的當(dāng)前系統(tǒng)上所有可用的大頁(yè),但是如果參數(shù)指定了內(nèi)存大小,就有可能用不到所有的
//大頁(yè)。所以此函數(shù)時(shí)根據(jù)實(shí)際需要返回大頁(yè)個(gè)數(shù)。
//如果參數(shù)申請(qǐng)的內(nèi)存大于當(dāng)前可用的內(nèi)存,直接返回-1
/* calculate final number of pages */
nr_hugepages = calc_num_pages_per_socket(memory,
internal_config.hugepage_info, used_hp,
internal_config.num_hugepage_sizes);
//沒(méi)有足夠內(nèi)存
/* error if not enough memory available */
if (nr_hugepages < 0)
goto fail;
//創(chuàng)建文件 /var/run/.rte_hugepage_info,文件大小為nr_hugefiles * sizeof(struct hugepage_file),
//用來(lái)保存實(shí)際使用的大頁(yè)信息,并將此文件進(jìn)行mmap映射。
/* create shared memory */
hugepage = create_shared_memory(eal_hugepage_info_path(),
nr_hugefiles * sizeof(struct hugepage_file));
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.
*/
//刪除前面映射的但是不需要的大頁(yè)文件。
//比如當(dāng)前系統(tǒng)有10個(gè)1G可用大頁(yè)內(nèi)存,進(jìn)行映射后會(huì)生成10個(gè)1G的文件,
//但是參數(shù) --socket-mem 只指定了1G內(nèi)存,則需要?jiǎng)h除9個(gè)1G的文件。
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.
*/
//這里只將實(shí)際需要的大頁(yè)信息保存到 hugepage
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;
}
//如果參數(shù)指定了 unlink,則要將映射后大頁(yè)文件unlink掉。unlink會(huì)將文件刪除(ls 看不到文件了),
//但是前面打開(kāi)了這些文件,實(shí)際上不會(huì)真正刪除(lsof 可以看到)。
/* 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;
//前面完成了大頁(yè)內(nèi)存的映射,這里要將他們分別保存到 memseg 中。
//同時(shí)滿足下面四個(gè)條件的hugepage 放在同一個(gè)memseg中.
//1. 同socket
//2. hugepage 大小相同
//3. 物理地址連續(xù)
//4. 虛擬地址連續(xù)
/* 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 */
//第一個(gè)大頁(yè)內(nèi)存,肯定是新memseg
if (i == 0)
new_memseg = 1;
//和前一個(gè)大頁(yè)的socket不一樣,認(rèn)為是新的memseg
else if (hugepage[i].socket_id != hugepage[i-1].socket_id)
new_memseg = 1;
//和前一個(gè)大頁(yè)的大小不一樣,認(rèn)為是新的memseg
else if (hugepage[i].size != hugepage[i-1].size)
new_memseg = 1;
//和前一個(gè)大頁(yè)的物理地址不連續(xù),認(rèn)為是新的memseg
else if ((hugepage[i].physaddr - hugepage[i-1].physaddr) !=
hugepage[i].size)
new_memseg = 1;
//和前一個(gè)大頁(yè)的虛擬地址不連續(xù),認(rèn)為是新的memseg
else if (((unsigned long)hugepage[i].final_va -
(unsigned long)hugepage[i-1].final_va) != hugepage[i].size)
new_memseg = 1;
//將大頁(yè)信息保存到 memseg中。
//最壞情況下,每個(gè)大頁(yè)使用有一個(gè)memseg。
if (new_memseg) {
j += 1;
if (j == RTE_MAX_MEMSEG)
break;
mcfg->memseg[j].iova = 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 {
mcfg->memseg[j].len += mcfg->memseg[j].hugepage_sz;
}
hugepage[i].memseg_id = j;
}
if (i < nr_hugefiles) {
RTE_LOG(ERR, EAL, "Can only reserve %d pages "
"from %d requested\n"
"Current %s=%d is not enough\n"
"Please either increase it or request less amount "
"of memory.\n",
i, nr_hugefiles, RTE_STR(CONFIG_RTE_MAX_MEMSEG),
RTE_MAX_MEMSEG);
goto fail;
}
//將大頁(yè)信息保存到文件 /var/run/.rte_hugepage_info 后,就可以將其解除映射,
//等待從進(jìn)程讀取此文件即可。
munmap(hugepage, nr_hugefiles * sizeof(struct hugepage_file));
return 0;
}
/*
* Mmap all hugepages of hugepage table: it first open a file in
* hugetlbfs, then mmap() hugepage_sz data in it. If orig is set, the
* virtual address is stored in hugepg_tbl[i].orig_va, else it is stored
* in hugepg_tbl[i].final_va. The second mapping (when orig is 0) tries to
* map contiguous physical blocks in contiguous virtual blocks.
*/
static unsigned
map_all_hugepages(struct hugepage_file *hugepg_tbl, struct hugepage_info *hpi,
uint64_t *essential_memory __rte_unused, int orig)
{
int fd;
unsigned i;
void *virtaddr;
void *vma_addr = NULL;
size_t vma_len = 0;
//遍歷大頁(yè)
for (i = 0; i < hpi->num_pages[0]; i++) {
uint64_t hugepage_sz = hpi->hugepage_sz;
//如果是第一次映射,保存大頁(yè)索引,獲取大頁(yè)所在路徑
if (orig) {
hugepg_tbl[i].file_id = i;
hugepg_tbl[i].size = hugepage_sz;
//大頁(yè)文件所在路徑 /mnt/huge/rte_mapx
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';
}
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. */
//找到和當(dāng)前大頁(yè)物理地址連續(xù)的大頁(yè)
for (j = i+1; j < hpi->num_pages[0] ; j++) {
//前一個(gè)大頁(yè)的物理地址加上大頁(yè)大小不等于當(dāng)前頁(yè)的物理地址,
//說(shuō)明這兩個(gè)大頁(yè)物理地址不連續(xù)。
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. */
//如果物理地址連續(xù),則判斷虛擬地址是否也可以連續(xù)
vma_addr = get_virtual_area(&vma_len, hpi->hugepage_sz);
if (vma_addr == NULL)
vma_len = hugepage_sz;
}
//打開(kāi)大頁(yè)文件
/* 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));
goto out;
}
//第一次映射,vma_addr為NULL,讓kernel返回合適的虛擬地址
/* 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);
if (virtaddr == MAP_FAILED) {
RTE_LOG(DEBUG, EAL, "%s(): mmap failed: %s\n", __func__,
strerror(errno));
close(fd);
goto out;
}
//第一次映射,將返回的虛擬地址保存到 orig_va
if (orig) {
hugepg_tbl[i].orig_va = virtaddr;
}
else {//第二次映射,將返回的虛擬地址保存到 final_va
hugepg_tbl[i].final_va = virtaddr;
}
/* 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);
goto out;
}
close(fd);
//下一個(gè)大頁(yè)映射的虛擬地址為當(dāng)前虛擬地址加大頁(yè)大小,保證
//所有大頁(yè)的虛擬地址連續(xù)
vma_addr = (char *)vma_addr + hugepage_sz;
vma_len -= hugepage_sz;
}
out:
return i;
}
從進(jìn)程調(diào)用 rte_eal_hugepage_attach 映射和主進(jìn)程相同的虛擬地址。
/*
* This creates the memory mappings in the secondary process to match that of
* the server process. It goes through each memory segment in the DPDK runtime
* configuration and finds the hugepages which form that segment, mapping them
* in order to form a contiguous block in the virtual memory space
*/
int
rte_eal_hugepage_attach(void)
{
//獲取全局共享內(nèi)存配置
const struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
struct hugepage_file *hp = NULL;
unsigned num_hp = 0;
unsigned i, s = 0; /* s used to track the segment number */
unsigned max_seg = RTE_MAX_MEMSEG;
off_t size = 0;
int fd, fd_zero = -1, fd_hugepage = -1;
if (aslr_enabled() > 0) {
RTE_LOG(WARNING, EAL, "WARNING: Address Space Layout Randomization "
"(ASLR) is enabled in the kernel.\n");
RTE_LOG(WARNING, EAL, " This may cause issues with mapping memory "
"into secondary processes\n");
}
test_phys_addrs_available();
//打開(kāi) /dev/zero,用來(lái)測(cè)試虛擬地址是否可用
fd_zero = open("/dev/zero", O_RDONLY);
if (fd_zero < 0) {
RTE_LOG(ERR, EAL, "Could not open /dev/zero\n");
goto error;
}
//打開(kāi)文件 /var/run/.rte_hugepage_info
fd_hugepage = open(eal_hugepage_info_path(), O_RDONLY);
if (fd_hugepage < 0) {
RTE_LOG(ERR, EAL, "Could not open %s\n", eal_hugepage_info_path());
goto error;
}
//主進(jìn)程已經(jīng)將需要的大頁(yè)進(jìn)行映射,并保存到了 mem_config->memseg[]中,
//遍歷memseg,將主進(jìn)程保存到memseg的虛擬地址在從進(jìn)程映射,查看是否能
//映射成功,如果不能成功,則報(bào)錯(cuò)返回,說(shuō)明不能和主進(jìn)程使用相同的地址。
/* map all segments into memory to make sure we get the addrs */
for (s = 0; s < RTE_MAX_MEMSEG; ++s) {
void *base_addr;
/*
* the first memory segment with len==0 is the one that
* follows the last valid segment.
*/
if (mcfg->memseg[s].len == 0)
break;
/*
* fdzero is mmapped to get a contiguous block of virtual
* addresses of the appropriate memseg size.
* use mmap to get identical addresses as the primary process.
*/
base_addr = mmap(mcfg->memseg[s].addr, mcfg->memseg[s].len,
PROT_READ,
MAP_PRIVATE,
fd_zero, 0);
if (base_addr == MAP_FAILED ||
base_addr != mcfg->memseg[s].addr) {
max_seg = s;
if (base_addr != MAP_FAILED) {
/* errno is stale, don't use */
RTE_LOG(ERR, EAL, "Could not mmap %llu bytes "
"in /dev/zero at [%p], got [%p] - "
"please use '--base-virtaddr' option\n",
(unsigned long long)mcfg->memseg[s].len,
mcfg->memseg[s].addr, base_addr);
munmap(base_addr, mcfg->memseg[s].len);
} else {
RTE_LOG(ERR, EAL, "Could not mmap %llu bytes "
"in /dev/zero at [%p]: '%s'\n",
(unsigned long long)mcfg->memseg[s].len,
mcfg->memseg[s].addr, strerror(errno));
}
if (aslr_enabled() > 0) {
RTE_LOG(ERR, EAL, "It is recommended to "
"disable ASLR in the kernel "
"and retry running both primary "
"and secondary processes\n");
}
goto error;
}
}
//獲取文件 /var/run/.rte_hugepage_info 的實(shí)際大小
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;
}
//計(jì)算保存的大頁(yè)個(gè)數(shù)
num_hp = size / sizeof(struct hugepage_file);
RTE_LOG(DEBUG, EAL, "Analysing %u files\n", num_hp);
//再次遍歷 memseg
s = 0;
while (s < RTE_MAX_MEMSEG && mcfg->memseg[s].len > 0){
void *addr, *base_addr;
uintptr_t offset = 0;
size_t mapping_size;
/*
* free previously mapped memory so we can map the
* hugepages into the space
*/
//解除到 /dev/zero 的映射
base_addr = mcfg->memseg[s].addr;
munmap(base_addr, mcfg->memseg[s].len);
//找到memseg中的大頁(yè)進(jìn)行映射
/* find the hugepages for this segment and map them
* we don't need to worry about order, as the server sorted the
* entries before it did the second mmap of them */
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;
}
}
RTE_LOG(DEBUG, EAL, "Mapped segment %u of size 0x%llx\n", s,
(unsigned long long)mcfg->memseg[s].len);
s++;
}
/* unmap the hugepage config file, since we are done using it */
munmap(hp, size);
close(fd_zero);
close(fd_hugepage);
return 0;
}
rte_eal_memzone_init
雖然函數(shù)名字是memzone初始化,但更多的是初始化 malloc_heap。
/*
* Init the memzone subsystem
*/
int
rte_eal_memzone_init(void)
{
struct rte_mem_config *mcfg;
const struct rte_memseg *memseg;
/* get pointer to global configuration */
mcfg = rte_eal_get_configuration()->mem_config;
//從進(jìn)程不用執(zhí)行
/* secondary processes don't need to initialise anything */
if (rte_eal_process_type() == RTE_PROC_SECONDARY)
return 0;
memseg = rte_eal_get_physmem_layout();
if (memseg == NULL) {
RTE_LOG(ERR, EAL, "%s(): Cannot get physical layout\n", __func__);
return -1;
}
rte_rwlock_write_lock(&mcfg->mlock);
//清空 memzone 個(gè)數(shù)
/* delete all zones */
mcfg->memzone_cnt = 0;
memset(mcfg->memzone, 0, sizeof(mcfg->memzone));
rte_rwlock_write_unlock(&mcfg->mlock);
//初始化 heap
return 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中的socket插入malloc_heaps中
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;
}
將memseg放入 malloc_heap,首尾各分配一個(gè) malloc_elem,后者指向前者,將前者插入free_head
/*
* 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 */
//memseg的首地址作為第一個(gè) malloc_elem
struct malloc_elem *start_elem = (struct malloc_elem *)ms->addr;
//memseg的尾地址減去malloc_elem大小作為最后一個(gè) malloc_elem
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);
//首尾malloc_elem相減得出第一個(gè)elem的大小
const size_t elem_size = (uintptr_t)end_elem - (uintptr_t)start_elem;
//初始化第一個(gè) elem,狀態(tài)為free,表示可以被分配
malloc_elem_init(start_elem, heap, ms, elem_size);
elem->heap = heap;
elem->ms = ms;
elem->prev = NULL;
memset(&elem->free_list, 0, sizeof(elem->free_list));
elem->state = ELEM_FREE;
elem->size = size;
elem->pad = 0;
set_header(elem);
set_trailer(elem);
//初始化最后一個(gè) elem,并指向前一個(gè)elem,狀態(tài)為busy,表示永遠(yuǎn)不會(huì)被分配走
malloc_elem_mkend(end_elem, start_elem);
malloc_elem_init(elem, prev->heap, prev->ms, 0);
elem->prev = prev;
elem->state = ELEM_BUSY; /* mark busy so its never merged */
//根據(jù)第一個(gè)elem的大小插入對(duì)應(yīng)的free_head
malloc_elem_free_list_insert(start_elem);
size_t idx;
//根據(jù)size計(jì)算idx
idx = malloc_elem_free_list_index(elem->size - MALLOC_ELEM_HEADER_LEN);
elem->state = ELEM_FREE;
LIST_INSERT_HEAD(&elem->heap->free_head[idx], elem, free_list);
//保存此heap可分配的總內(nèi)存大小
heap->total_size += elem_size;
}
三、內(nèi)存分配
3.1將內(nèi)存固定到NUMA節(jié)點(diǎn)
當(dāng)分配常規(guī)內(nèi)存時(shí),理論上,它可以被分配到RAM中的任何位置。這在單CPU系統(tǒng)上沒(méi)有什么問(wèn)題,但是許多DPDK用戶是在支持非統(tǒng)一內(nèi)存訪問(wèn) (NUMA) 的多CPU系統(tǒng)上運(yùn)行應(yīng)用的。對(duì)于NUMA來(lái)說(shuō),所有內(nèi)存都是不同的:某一個(gè)CPU對(duì)一些內(nèi)存的訪問(wèn)(如不在該CPU所屬NUMA NODE上的內(nèi)存)將比其他內(nèi)存訪問(wèn)花費(fèi)更長(zhǎng)的時(shí)間,這是由于它們相對(duì)于執(zhí)行所述內(nèi)存訪問(wèn)的CPU所在的物理位置不同。
進(jìn)行常規(guī)內(nèi)存分配時(shí),通常無(wú)法控制該內(nèi)存分配到哪里,因此如果DPDK在這樣的系統(tǒng)上使用常規(guī)內(nèi)存,就可能會(huì)導(dǎo)致以下的情況:在一個(gè)CPU上執(zhí)行的線程卻在無(wú)意中訪問(wèn)屬于非本地NUMA節(jié)點(diǎn)的內(nèi)存。
雖然這種跨NUMA節(jié)點(diǎn)訪問(wèn)在所有現(xiàn)代操作系統(tǒng)上都比較少有,因?yàn)檫@樣的訪問(wèn)都是都是NUMA感知的,而且即使沒(méi)有DPDK還是有方法能對(duì)內(nèi)存實(shí)施NUMA定位。但是DPDK帶來(lái)的不僅僅是NUMA感知,事實(shí)上,整個(gè)DPDK API的構(gòu)建都旨在為每個(gè)操作提供明確的NUMA感知。如果不明確請(qǐng)求NUMA節(jié)點(diǎn)訪問(wèn)(其中所述結(jié)構(gòu)必須位于內(nèi)存中),通常無(wú)法分配給定的DPDK數(shù)據(jù)結(jié)構(gòu)。
DPDK API提供的這種明確的NUMA感知有助于確保用戶應(yīng)用在每個(gè)操作中都能考慮到NUMA感知;換句話說(shuō),DPDK API可以減少寫(xiě)出編寫(xiě)性能差的代碼的可能性。
硬件、物理地址和直接內(nèi)存存取(DMA)
DPDK被認(rèn)為是一組用戶態(tài)的網(wǎng)絡(luò)包輸入/輸出庫(kù),到目前為止,它基本上保持了最初的任務(wù)聲明。但是,電腦上的硬件不能處理用戶空間的虛擬地址,因?yàn)樗荒芨兄魏斡脩魬B(tài)的進(jìn)程和其所分配到的用戶空間虛擬地址。相反,它只能訪問(wèn)真實(shí)的物理地址上的內(nèi)存,也就是CPU、RAM和系統(tǒng)所有其他的部分用來(lái)相互通信的地址。
出于對(duì)效率的考量,現(xiàn)代硬件幾乎總是使用直接內(nèi)存存取(DMA)事務(wù)。通常,為了執(zhí)行一個(gè)DMA事務(wù),內(nèi)核需要參與創(chuàng)建一個(gè)支持DMA的存儲(chǔ)區(qū)域,將進(jìn)程內(nèi)虛擬地址轉(zhuǎn)換成硬件能夠理解的真實(shí)物理地址,并啟動(dòng)DMA事務(wù)。這是大多數(shù)現(xiàn)代操作系統(tǒng)中輸入輸出的工作方式;然而,這是一個(gè)耗時(shí)的過(guò)程,需要上下文切換、轉(zhuǎn)換和查找操作,這不利于高性能輸入/輸出。
DPDK的內(nèi)存管理以一種簡(jiǎn)單的方式解決了這個(gè)問(wèn)題。每當(dāng)一個(gè)內(nèi)存區(qū)域可供DPDK使用時(shí),DPDK就通過(guò)詢問(wèn)內(nèi)核來(lái)計(jì)算它的物理地址。由于DPDK使用鎖定內(nèi)存,通常以大頁(yè)的形式,底層內(nèi)存區(qū)域的物理地址預(yù)計(jì)不會(huì)改變,因此硬件可以依賴這些物理地址始終有效,即使內(nèi)存本身有一段時(shí)間沒(méi)有使用。然后,DPDK會(huì)在準(zhǔn)備由硬件完成的輸入/輸出事務(wù)時(shí)使用這些物理地址,并以允許硬件自己?jiǎn)?dòng)DMA事務(wù)的方式配置硬件。這使DPDK避免不必要的開(kāi)銷,并且完全從用戶空間執(zhí)行輸入/輸出。
IOMMU和IOVA
默認(rèn)情況下,任何硬件都可以訪問(wèn)整個(gè)系統(tǒng),因此它可以在任何地方執(zhí)行DMA 事務(wù)。這有許多安全隱患。例如,流氓和/或不可信進(jìn)程(包括在VM (虛擬機(jī))內(nèi)運(yùn)行的進(jìn)程)可能使用硬件設(shè)備來(lái)讀寫(xiě)內(nèi)核空間,和幾乎其他任何存儲(chǔ)位置。為了解決這個(gè)問(wèn)題,現(xiàn)代系統(tǒng)配備了輸入輸出內(nèi)存管理單元(IOMMU)。這是一種硬件設(shè)備,提供DMA地址轉(zhuǎn)換和設(shè)備隔離功能,因此只允許特定設(shè)備執(zhí)行進(jìn)出特定內(nèi)存區(qū)域(由IOMMU指定)的DMA 事務(wù),而不能訪問(wèn)系統(tǒng)內(nèi)存地址空間的其余部分。
由于IOMMU的參與,硬件使用的物理地址可能不是真實(shí)的物理地址,而是IOMMU分配給硬件的(完全任意的)輸入輸出虛擬地址(IOVA)。一般來(lái)說(shuō),DPDK社區(qū)可以互換使用物理地址和IOVA這兩個(gè)術(shù)語(yǔ),但是根據(jù)上下文,這兩者之間的區(qū)別可能很重要。例如,DPDK 17.11和更新的DPDK長(zhǎng)期支持(LTS)版本在某些情況下可能根本不使用實(shí)際的物理地址,而是使用用戶空間虛擬地址(甚至完全任意的地址)來(lái)實(shí)現(xiàn)DMA。IOMMU負(fù)責(zé)地址轉(zhuǎn)換,因此硬件永遠(yuǎn)不會(huì)注意到兩者之間的差異。
根據(jù)DPDK的初始化方式,IOVA地址可能代表也可能不代表實(shí)際的物理地址,但有一點(diǎn)始終是正確的:DPDK知道底層內(nèi)存布局,因此可以利用這一點(diǎn)。例如,它可以以創(chuàng)建IOVA連續(xù)虛擬區(qū)域的方式映射頁(yè)面,或者甚至利用IOMMU來(lái)重新排列內(nèi)存映射,以使內(nèi)存看起來(lái)IOVA連續(xù),即使底層物理內(nèi)存可能不連續(xù)。
因此,這種對(duì)底層物理內(nèi)存區(qū)域的感知是DPDK工具包中的又一個(gè)利器。大多數(shù)數(shù)據(jù)結(jié)構(gòu)不關(guān)心IOVA地址,但當(dāng)它們關(guān)心時(shí),DPDK為軟件和硬件提供了利用物理內(nèi)存布局的工具,并針對(duì)不同的用例進(jìn)行優(yōu)化
請(qǐng)注意,IOMMU不會(huì)自行設(shè)置任何映射。相反,平臺(tái)、硬件和操作系統(tǒng)必須進(jìn)行配置,來(lái)使用IOMMU。這種配置說(shuō)明超出了本系列文章的范圍,但是在DPDK文檔和其他地方有相關(guān)說(shuō)明。一旦系統(tǒng)和硬件設(shè)置為使IOMMU,DPDK就可以使用IOMMU為DPDK分配的任何內(nèi)存區(qū)域設(shè)置DMA映射。使用IOMMU是運(yùn)行DPDK的推薦方法,因?yàn)檫@樣做更安全,并且它提供了可用性優(yōu)勢(shì)。
3.2內(nèi)存分配和管理
DPDK不使用常規(guī)內(nèi)存分配函數(shù),如malloc()。相反,DPDK管理自己的內(nèi)存。更具體地說(shuō),DPDK分配大頁(yè)并在此內(nèi)存中創(chuàng)建一個(gè)堆(heap)并將其提供給用戶應(yīng)用程序并用于存取應(yīng)用程序內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。
使用自定義內(nèi)存分配器有許多優(yōu)點(diǎn)。最明顯的一個(gè)是終端應(yīng)用程序的性能優(yōu)勢(shì):DPDK創(chuàng)建應(yīng)用程序要使用的內(nèi)存區(qū)域,并且應(yīng)用程序可以原生支持大頁(yè)、NUMA節(jié)點(diǎn)親和性、對(duì)DMA地址的訪問(wèn)、IOVA連續(xù)性等等性能優(yōu)勢(shì),而無(wú)需任何額外的開(kāi)發(fā)。
DPDK內(nèi)存分配總是在CPU高速緩存行(cache line)的邊界上對(duì)齊,每個(gè)分配的起始地址將是系統(tǒng)高速緩存行大小的倍數(shù)。這種方法防止了許多常見(jiàn)的性能問(wèn)題,例如未對(duì)齊的訪問(wèn)和錯(cuò)誤的數(shù)據(jù)共享,其中單個(gè)高速緩存行無(wú)意中包含(可能不相關(guān)的)多個(gè)內(nèi)核同時(shí)訪問(wèn)的數(shù)據(jù)。對(duì)于需要這種對(duì)齊的用例(例如,分配硬件環(huán)結(jié)構(gòu)),也支持任何其他二次冪值 (當(dāng)然> =高速緩存行大小)。
DPDK中的任何內(nèi)存分配也是線程安全的。這意味著在任何CPU核心上發(fā)生的任何分配都是原子的,不會(huì)干擾任何其他分配。這可能看起來(lái)很無(wú)足輕重 (畢竟,常規(guī)glibc內(nèi)存分配例程通常也是線程安全的),但是一旦在多處理環(huán)境中考慮,它的重要性就會(huì)變得更加清晰。
DPDK支持特定風(fēng)格的協(xié)同多處理,其中主進(jìn)程管理所有DPDK資源,多個(gè)輔助進(jìn)程可以連接到主進(jìn)程,并共享由主進(jìn)程管理的資源的訪問(wèn)。
DPDK的共享內(nèi)存實(shí)現(xiàn)不僅通過(guò)映射不同進(jìn)程中的相同資源 (類似于shmget () 機(jī)制) 來(lái)實(shí)現(xiàn),還通過(guò)復(fù)制另一個(gè)進(jìn)程中主進(jìn)程的地址空間來(lái)實(shí)現(xiàn)。因此,由于兩個(gè)進(jìn)程中的所有內(nèi)容都位于相同的地址,指向DPDK內(nèi)存對(duì)象的任何指針都將跨進(jìn)程工作,無(wú)需任何地址轉(zhuǎn)換。這對(duì)于跨進(jìn)程傳遞數(shù)據(jù)時(shí)的性能非常重要。
malloc_heap_alloc
對(duì)外提供API的最底層實(shí)現(xiàn)是malloc_heap,它提供了函數(shù)malloc_heap_alloc用來(lái)從heap中分配內(nèi)存。
/*
* Main function to allocate a block of memory from the heap.
* It locks the free list, scans it, and adds a new memseg if the
* scan fails. Once the new memseg is added, it re-scans and should return
* the new element after releasing the lock.
*/
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);
//分配內(nèi)存都要先加鎖
rte_spinlock_lock(&heap->lock);
//先根據(jù)請(qǐng)求的內(nèi)存大小判斷是否有這么多可用內(nèi)存
elem = find_suitable_element(heap, size, flags, align, bound);
if (elem != NULL) {
//有可用內(nèi)存,則將memseg進(jìn)行分割
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]);
}
rte_memzone_reserve
rte_memzone_reserve用來(lái)從heap中分配一個(gè)內(nèi)存,可用指定長(zhǎng)度和socket。、
/*
* Return a pointer to a correctly filled memzone descriptor. If the
* allocation cannot be done, return NULL.
*/
const struct rte_memzone *
rte_memzone_reserve(const char *name, size_t len, int socket_id,
unsigned flags)
{
return rte_memzone_reserve_thread_safe(name, len, socket_id,
flags, RTE_CACHE_LINE_SIZE, 0);
}
rte_mempool_create
rte_mempool_create用來(lái)申請(qǐng)內(nèi)存保存固定大小的對(duì)象,會(huì)申請(qǐng)多個(gè)memzone,其中一個(gè)用于存放struct rte_mempool,其余的一個(gè)或者多個(gè)memzone存放固定大小的對(duì)象。后面介紹的mbuf會(huì)作為固定大小的對(duì)象存儲(chǔ)在mempool中。
rte_pktmbuf_pool_create
rte_mbuf用來(lái)存放報(bào)文,它是在應(yīng)用啟動(dòng)前調(diào)用rte_pktmbuf_pool_create申請(qǐng)好的內(nèi)存,后面申請(qǐng)和釋放只是指針的操作。
name: mempool的名字。
n: mempool中存放obj的個(gè)數(shù)。
cache_size: 每個(gè)cpu緩存obj的最大個(gè)數(shù)。
priv_size: mbuf結(jié)構(gòu)后面的內(nèi)存,可用來(lái)存放應(yīng)用的私有數(shù)據(jù)。
data_room_size: mbuf中存放報(bào)文的空間大小。
/* helper to create a mbuf pool */
struct rte_mempool *
rte_pktmbuf_pool_create(const char *name, unsigned n,
unsigned cache_size, uint16_t priv_size, uint16_t data_room_size,
int socket_id)
{
struct rte_mempool *mp;
struct rte_pktmbuf_pool_private mbp_priv;
const char *mp_ops_name;
unsigned elt_size;
int ret;
if (RTE_ALIGN(priv_size, RTE_MBUF_PRIV_ALIGN) != priv_size) {
RTE_LOG(ERR, MBUF, "mbuf priv_size=%u is not aligned\n",
priv_size);
rte_errno = EINVAL;
return NULL;
}
//mempool中一個(gè)對(duì)象的大小
elt_size = sizeof(struct rte_mbuf) + (unsigned)priv_size +
(unsigned)data_room_size;
mbp_priv.mbuf_data_room_size = data_room_size;
mbp_priv.mbuf_priv_size = priv_size;
//創(chuàng)建mempool結(jié)構(gòu),并插入共享鏈表rte_mempool_tailq
mp = rte_mempool_create_empty(name, n, elt_size, cache_size,
sizeof(struct rte_pktmbuf_pool_private), socket_id, 0);
if (mp == NULL)
return NULL;
mp_ops_name = rte_eal_mbuf_default_mempool_ops();
ret = rte_mempool_set_ops_byname(mp, mp_ops_name, NULL);
if (ret != 0) {
RTE_LOG(ERR, MBUF, "error setting mempool handler\n");
rte_mempool_free(mp);
rte_errno = -ret;
return NULL;
}
rte_pktmbuf_pool_init(mp, &mbp_priv);
//申請(qǐng)n個(gè)obj所占內(nèi)存,如果一個(gè)memzone不滿足,會(huì)申請(qǐng)多個(gè)memzone。將申請(qǐng)的memzone信息保存到rte_mempool_memhdr中,并插入mp->mem_list。同時(shí)將每個(gè)memzone按obj大小分成n份(每份就相當(dāng)于是一個(gè)mbuf),將每份的地址又保存到rte_mempool_objhdr,并插入mp->elt_list,然后將每份的地址入隊(duì)到mp->pool_data(rte_ring)。
ret = rte_mempool_populate_default(mp);
if (ret < 0) {
rte_mempool_free(mp);
rte_errno = -ret;
return NULL;
}
//調(diào)用rte_pktmbuf_init初始化mbuf
rte_mempool_obj_iter(mp, rte_pktmbuf_init, NULL);
return mp;
}
初始化后,mempool內(nèi)存結(jié)構(gòu)如下、
其中rte_mbuf的內(nèi)存結(jié)構(gòu)如下
rte_pktmbuf_alloc
rte_pktmbuf_alloc用來(lái)從指定的mempool中獲取一個(gè)mbuf,優(yōu)先從當(dāng)前cpu的cache里取,如果cache中沒(méi)了再?gòu)膒ool里取。
static inline struct rte_mbuf *rte_pktmbuf_alloc(struct rte_mempool *mp)
{
struct rte_mbuf *m;
if ((m = rte_mbuf_raw_alloc(mp)) != NULL)
rte_pktmbuf_reset(m);
return m;
}
3.3內(nèi)存池
DPDK也有一個(gè)內(nèi)存池管理器,在整個(gè)DPDK中廣泛用于管理大型對(duì)象池,對(duì)象大小固定。它的用途很多——包輸入/輸出、加密操作、事件調(diào)度和許多其他需要快速分配或解除分配固定大小緩沖區(qū)的用例。DPDK內(nèi)存池針對(duì)性能進(jìn)行了高度優(yōu)化,并支持可選的線程安全(如果用戶不需要線程安全,則無(wú)需為之付費(fèi))和批量操作,所有這些都會(huì)導(dǎo)致每個(gè)緩沖區(qū)的分配或空閑操作周期計(jì)數(shù)達(dá)到兩位數(shù)以下。
也就是說(shuō),即使DPDK內(nèi)存池的主題出現(xiàn)在幾乎所有關(guān)于DPDK內(nèi)存管理的討論中,從技術(shù)上講,內(nèi)存池管理器是一個(gè)建立在常規(guī)DPDK內(nèi)存分配器之上的庫(kù)。它不是標(biāo)準(zhǔn)DPDK內(nèi)存分配工具的一部分,它的內(nèi)部工作與DPDK內(nèi)存管理例程完全分離 (并且非常不同) 。因此,這超出了本系列文章的范圍。但是,有關(guān)DPDK內(nèi)存池管理器庫(kù)的更多信息可以在DPDK文檔中找到。
