1. 程式人生 > >dpdk 程式碼分析一 : 記憶體初始化

dpdk 程式碼分析一 : 記憶體初始化

一  前言

http://www.dpdk.org/  dpdk 是 intel 開發的x86晶片上用於高效能網路處理的基礎庫,業內比較常用的模式是linux-app模式,即

利用該基礎庫,在使用者層空間做資料包處理,有了這個基礎庫,可以方便地在寫應用層的網路包處理高效能程式,目前該庫已經開源。

Main libraries

  • multicore framework   多核框架,dpdk庫面向intel i3/i5/i7/ep 等多核架構晶片,內建了對多核的支援
  • huge page memory  記憶體管理,dpdk庫基於linux hugepage實現了一套記憶體管理基礎庫,為應用層包處理做了很多優化
  • ring buffers          共享佇列,dpdk庫提供的無鎖多生產者-多消費者佇列,是應用層包處理程式的基礎元件
  • poll-mode drivers     輪詢驅動,dpdk庫基於linux uio實現的使用者態網絡卡驅動

下面分幾個模組說明上述元件的功能和實現。

二 記憶體管理

1 hugepage技術

hugepage(2M/1G..)相對於普通的page(4K)來說有幾個特點,a hugepage 這種頁面不受虛擬記憶體管理影響,不會被替換出記憶體,而

普通的4kpage 如果實體記憶體不夠可能會被虛擬記憶體管理模組替換到交換區。 b 同樣的記憶體大小,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的方式比較簡單,

/sys/kernel/mm/hugepages/hugepages-2048kB/  通過修改這個目錄下的檔案可以修改hugepage頁面的大小和總數目

mount -t hugetlbfs nodev /mnt/huge linux將hugepage實現為一種檔案系統hugetlbfs,需要將該檔案系統mount到某個檔案

mmap /mnt/huge   在使用者程序裡通過mmap 對映hugetlbfs mount 的目標檔案,這個mmap返回的地址就是大頁面的了

2 多程序共享

mmap 系統呼叫可以設定為共享的對映,dpdk的記憶體共享就依賴於此,在這多個程序中,分為兩種角色,第一種是主程序(RTE_PROC_PRIMARY),第二種是從程序(RTE_PROC_SECONDARY)。主程序只有一個,必須在從程序之前啟動,

負責執行DPDK庫環境的初始化,從程序attach到主程序初始化的DPDK上,主程序先mmap hugetlbfs 檔案,構建記憶體管理相關結構

將這些結構存入hugetlbfs 上的配置檔案rte_config,然後其他程序mmap rte_config檔案,獲取記憶體管理結構,dpdk採用了一定的

技巧,使得最終同樣的共享實體記憶體在不同程序內部對應的虛擬地址是完全一樣的,意味著一個程序內部的基於dpdk的共享資料和指向

這些共享資料的指標,可以在不同程序間通用。

記憶體全域性配置結構rte_config

rte_config 是每個程式私有的資料結構,這些東西都是每個程式的私有配置。

lcore_role:這個DPDK程式使用-c引數設定的它同時跑在哪幾個核上。

master_lcore:DPDK的架構上,每個程式分配的lcore_role 有一個主核,對使用者來說影響不大。

lcore_count:這個程式可以使用的核數。

process_type:DPDK多程序:一個程式是主程式,否則初始化DPDK記憶體表,其他從程式使用這個表。RTE_PROC_PRIMARY/RTE_PROC_SECONDARY

mem_config:指向裝置各個DPDK程式共享的記憶體配置結構,這個結構被mmap到檔案/var/run/.rte_config,通過這個方式多程序實現對mem_config結構的共享。

Hugepage頁表陣列

這個是struct hugepage陣列,每個struct hugepage 都代表一個hugepage 頁面,儲存的每個頁面的實體地址和程式的虛擬地址的對映關係。然後,把整個陣列對映到檔案/var/run /. rte_hugepage_info,同樣這個檔案也是裝置共享的,主/從程序都能訪問它。

file_id: 每個檔案在hugepage 檔案系統中都有一個編號,就是陣列1-N

filepath:%s/%smap_%file_id       mount 的hugepage檔案系統中的檔案路徑名

size; 這個hugepage頁面的size,2M還是1G

socket_id:這個頁面屬於那個CPU socket 。

Physaddr:這個hugepage 頁面的實體地址

orig_va:它和final_va一樣都是指這個huagepage頁面的虛擬地址。這個地址是主程式初始化huagepage用的,後來就沒用了。

final_va:這個最終這個頁面對映到主/從程式中的虛擬地址。

首先因為整個陣列都對映到檔案裡面,所有的程式之間都是共享的。主程式負責初始化這個陣列,首先在它內部通過mmap把所有的hugepage物理頁面都對映到虛存空間裡面,然後把這種對映關係儲存到這個檔案裡面。從程式啟動的時候,讀取這個檔案,然後在它記憶體也建立和它一模一樣的對映,這樣的話,整個DPDK管理的記憶體在所有的程式裡面都是可見,而且地址都一樣。

在對各個頁面的實體地址份配虛擬地址時,DPDK儘可能把實體地址連續的頁面分配連續的虛存地址上,這個東西還是比較有用的,因為CPU/cache/記憶體控制器的等等看到的都是實體記憶體,我們在訪問記憶體時,如果實體地址連續的話,效能會高一些。

至於到底哪些地址是連續的,那些不是連續的,DPDK在這個結構之上又有一個新的結構rte_mem_config. Memseg來管理。因為rte_mem_config也對映到檔案裡面,所有的程式都可見rte_mem_config. Memseg結構。

rte_mem_config

這個資料結構mmap 到檔案/var/run /.rte_config中,主/從程序通過這個檔案訪問實現對這個資料結構的共享。

在每個程式內,使用rte_config .mem_config 訪問這個結構。

memseg

memseg 陣列是維護實體地址的,在上面講到struct hugepage結構對每個hugepage物理頁面都儲存了它在程式裡面的虛存地址。memseg 陣列的作用是將實體地址、虛擬地址都連續的hugepage,並且都在同一個socket,pagesize 也相同的hugepage頁面集合,把它們都劃在一個memseg結構裡面,這樣做的好處就是優化記憶體。

Rte_memseg這個結構也很簡單:

phys_addr:這個memseg的包含的所有的hugepage頁面的起始地址

addr:這些hugepage頁面的起始的虛存地址

len:這個memseg的包含的空間size

hugepage_sz; 這些頁面的size 2M /1G?

 這些資訊都是從hugepage頁表數組裡面獲得的。

 free_memseg

上面memseg是儲存一個整體的這種對映的對映,最終這些地址是要被分配出去的。free_memseg記錄了當前整個DPDK記憶體空閒的memseg段,注意,這是對所有程序而言的。初始化等於memseg表示所有的記憶體都是free的,後面隨著分配記憶體,它越來越小。

必須得說明,rte_mem_config結構在各個程式裡面都是共享的,所以對任何成員的修改都必須加鎖。

mlock :讀寫鎖,用來保護memseg結構。

rte_memzone_reserve

一般情況下,各個功能分配記憶體都使用這個函式分配一個memzone

const struct rte_memzone *

rte_memzone_reserve(const char *name, size_t len, int socket_id,

                    unsigned flags)

{

1、這裡就在free_memseg數組裡面找滿足socket_id的要求,最小的memseg,從這裡面劃出來一塊記憶體,當然這時候free_memseg需要更新把這部分記憶體刨出去。

2、把這塊記憶體記錄到memzone裡面。

}

上面提到rte_memzone_reserve分配記憶體後,同時在rte_mem_config.memzone數組裡面分配一個元素儲存它。對於從memseg中分配記憶體,以memzone為單位來分配,對於所有的分配情況,都記錄在memzone數組裡面,當然這個陣列是多程序共享,大家都能看到。

       在rte_mem_config結構裡面memzone_idx 變數記錄當前分配的memzone,每申請一次這個變數+1。

這個rte_memzone結構如下:

Name:給這片記憶體起個名字。

phys_addr:這個memzone 分配的記憶體區的實體地址

addr:這個memzone 分配的記憶體區的虛擬地址

len:這個zone的空間長度

hugepage_sz:這個zone的頁面size

socket_id:頁面的socket號

rte_memzone 是dpdk記憶體管理最終向客戶程式提供的基礎介面,通過 rte_memzone_reverse 可以獲取基於

dpdk hugepage 的屬於同一個物理cpu的實體記憶體連續的虛擬記憶體也連續的一塊地址。rte_ring/rte_malloc/rte_mempool等

元件都是依賴於rte_memzone 元件實現的。

3 dpdk 記憶體初始化原始碼解析

 入口:

  rte_eal_init(int argc,char ** argv)   dpdk 執行環境初始化入口函式

      —— eal_hugepage_info_init  這4個是記憶體相關的初始化函式

      ——rte_config_init

      ——rte_eal_memory_init

      ——rte_eal_memzone_init

3.1 eal_hugepage_info_init

這個函式比較簡單,主要是從 /sys/kernel/mm/hugepages 目錄下面讀取目錄名和檔名,從而獲取系統的hugetlbfs檔案系統數,

以及每個 hugetlbfs 的大頁面數目和每個頁面大小,並儲存在一個檔案裡,這個函式,只有主程序會呼叫。存放在internal_config結構裡

3.2 rte_config_init

構造 rte_config 結構

  rte_config_init

    ——rte_eal_config_create  主程序執行

    ——rte_eal_config_attach 從程序執行

rte_eal_config_create  和 rte_eal_config_attach 做的事情比較簡單,就是將 /var/run/.config 檔案shared 型

mmap 到自己的程序空間的 rte_config.mem_config結構上,這樣主程序和從程序都可以訪問這塊記憶體,

rte_eal_config_attach

3.3 rte_eal_memory_init

 rte_eal_memory_init

    ——rte_eal_hugepage_init   主程序執行,dpdk 記憶體初始化核心函式

    ——rte_eal_hugepage_attach  從程序執行

rte_eal_hugepage_init  函式分幾個步驟:

/*
* 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
*/

  

函式一開始,將rte_config_init函式獲取的配置結構放到本地變數 mcfg 上,然後檢查系統是否開啟hugetlbfs,如果

不開啟,則直接通過系統的malloc函式申請配置需要的記憶體,然後跳出這個函式。

接下來主要是構建 hugepage 結構的陣列 tmp_hp(上圖)

下面就是重點了。。

構建hugepage 結構陣列分下面幾步

首先,迴圈遍歷系統所有的hugetlbfs 檔案系統,一般來說,一個系統只會使用一種hugetlbfs ,所以這一層的迴圈可以認為

沒有作用,一種 hugetlbfs 檔案系統對應的基礎資料包括:頁面大小,比如2M,頁面數目,比如2K個頁面

其次,將特定的hugetlbfs的全部頁面對映到本程序,放到本程序的 hugepage 陣列管理,這個過程主要由 map_all_hugepages函式完成,

第一次對映的虛擬地址存放在 hugepage結構的 orig_va變數

第三,遍歷hugepage陣列,找到每個虛擬地址對應的實體地址和所屬的物理cpu,將這些資訊也記入 hugepage陣列,實體地址

記錄在hugepage結構的phyaddr變數,物理cpu號記錄在 hugepage結構的socket_id變數

第四,跟據實體地址大小對hugepage陣列做排序

第五,根據排序結果重新對映,這個也是由函式 map_all_hugepages完成,重新對映後的虛擬地址存放在hugepage結構的final_va變數

第六,將第一次對映關係解除,即將orig_va 變數對應的虛擬地址空間返回給核心

下面看 map_all_hugepages的實現過程

這個函式是複用的,共有兩次呼叫。

對於第一次呼叫,就是根據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 變數上。

下面看 find_physaddr的實現過程

這個函式的作用就是找到hugepage數組裡每個虛擬地址對應的實體地址,並存放到 phyaddr變數上,最終實現由函式

rte_mem_virt2phy(const void * virt)函式實現,其原理相當於頁表查詢,主要是通過linux的頁表文件 /proc/self/pagemap 實現

/proc/self/pagemap 頁表文件記錄了本程序的頁表,即本程序虛擬地址到實體地址的對映關係,主要是通過虛擬地址的前面若干位

定位到物理頁框,然後物理頁框+虛擬地址偏移構成實體地址,其實現如下

下面看 find_numasocket的實現過程

這個函式的作用是找到hugepage數組裡每個虛擬地址對應的物理cpu號,基本原理是通過linux提供的 /proc/self/numa_maps 檔案,

/proc/self/numa_maps 檔案記錄了本 程序的虛擬地址與物理cpu號(多核系統)的對應關係,在遍歷的時候將非huge page的虛擬地址

過濾掉,剩下的虛擬地址與hugepage數組裡的orig_va 比較,實現如下

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函式的餘下部分,主要分兩個方面,一是將hugepage數組裡 屬於同一個物理cpu,實體記憶體連續

的多個hugepage 用一層 memseg 結構管理起來。 一個memseg 結構維護的記憶體必然是同一個物理cpu上的,虛擬地址和物理

地址都連續的記憶體,最終的memzone 介面是通過操作memseg實現的;2是將 hugepage陣列和memseg陣列的資訊記錄到共享檔案裡,

方便從程序獲取;

 

遍歷hugepage陣列,將實體地址連續的hugepage放到一個memseg結構上,同時將該memseg id 放到 hugepage結構

的 memseg_id 變數上

下面是建立檔案 hugepage_info 到共享記憶體上,然後hugepage陣列的資訊拷貝到這塊共享記憶體上,並釋放hugepage陣列,

其他程序通過對映 hugepage_info 檔案就可以獲取 hugepage陣列,從而管理hugepage共享記憶體