HugePages(大內(nèi)存頁)實(shí)現(xiàn)完全解析
在《一文讀懂 HugePages的原理》一文中介紹了 HugePages(大內(nèi)存頁)的原理和使用,現(xiàn)在我們來分析一下 Linux 內(nèi)核是怎么實(shí)現(xiàn) HugePages 分配的。
本文使用 Linux 內(nèi)核 2.6.23 版本
HugePages分配器初始化
在內(nèi)核初始化時,會調(diào)用 hugetlb_init 函數(shù)對 HugePages 分配器進(jìn)行初始化,其實(shí)現(xiàn)如下:
- static int __init hugetlb_init(void)
- {
- unsigned long i;
- // 1. 初始化空閑大內(nèi)存頁鏈表 hugepage_freelists,
- // 內(nèi)核使用 hugepage_freelists 鏈表把空閑的大內(nèi)存頁連接起來,
- // 為了分析簡單,我們可以把 MAX_NUMNODES 當(dāng)成 1
- for (i = 0; i < MAX_NUMNODES; ++i)
- INIT_LIST_HEAD(&hugepage_freelists[i]);
- // 2. max_huge_pages 為系統(tǒng)能夠使用的大頁內(nèi)存的數(shù)量,
- // 由系統(tǒng)啟動項(xiàng) hugepages 指定,
- // 這里主要申請大內(nèi)存頁, 并且保存到 hugepage_freelists 鏈表中.
- for (i = 0; i < max_huge_pages; ++i) {
- if (!alloc_fresh_huge_page())
- break;
- }
- max_huge_pages = free_huge_pages = nr_huge_pages = i;
- return 0;
- }
hugetlb_init 函數(shù)主要完成兩個工作:
- 初始化空閑大內(nèi)存頁鏈表 hugepage_freelists,這個鏈表保存了系統(tǒng)中能夠使用的大內(nèi)存。
- 為系統(tǒng)申請空閑的大內(nèi)存頁,并且保存到 hugepage_freelists 鏈表中。
我們再來分析下 alloc_fresh_huge_page 函數(shù)是怎么申請大內(nèi)存頁的,其實(shí)現(xiàn)如下:
- static int alloc_fresh_huge_page(void)
- {
- static int prev_nid;
- struct page *page;
- int nid;
- ...
- // 1. 申請一個大的物理內(nèi)存頁...
- page = alloc_pages_node(nid, htlb_alloc_mask|__GFP_COMP|__GFP_NOWARN,
- HUGETLB_PAGE_ORDER);
- if (page) {
- // 2. 設(shè)置釋放大內(nèi)存頁的回調(diào)函數(shù)為 free_huge_page
- set_compound_page_dtor(page, free_huge_page);
- ...
- // 3. put_page 函數(shù)將會調(diào)用上面設(shè)置的 free_huge_page 函數(shù)把內(nèi)存頁放入到緩存隊(duì)列中
- put_page(page);
- return 1;
- }
- return 0;
- }
所以,alloc_fresh_huge_page 函數(shù)主要完成三個工作:
- 調(diào)用 alloc_pages_node 函數(shù)申請一個大內(nèi)存頁(2MB)。
- 設(shè)置大內(nèi)存頁的釋放回調(diào)函數(shù)為 free_huge_page,當(dāng)釋放大內(nèi)存頁時,將會調(diào)用這個函數(shù)進(jìn)行釋放操作。
- 調(diào)用 put_page 函數(shù)釋放大內(nèi)存頁,其將會調(diào)用 free_huge_page 函數(shù)進(jìn)行相關(guān)操作。
那么,我們來看看 free_huge_page 函數(shù)是怎么釋放大內(nèi)存頁的,其實(shí)現(xiàn)如下:
- static void free_huge_page(struct page *page)
- {
- ...
- enqueue_huge_page(page); // 把大內(nèi)存頁放置到空閑大內(nèi)存頁鏈表中
- ...
- }
free_huge_page 函數(shù)主要調(diào)用 enqueue_huge_page 函數(shù)把大內(nèi)存頁添加到空閑大內(nèi)存頁鏈表中,其實(shí)現(xiàn)如下:
- static void enqueue_huge_page(struct page *page)
- {
- int nid = page_to_nid(page); // 我們假設(shè)這里一定返回 0
- // 把大內(nèi)存頁添加到空閑鏈表 hugepage_freelists 中
- list_add(&page->lru, &hugepage_freelists[nid]);
- // 增加計(jì)數(shù)器
- free_huge_pages++;
- free_huge_pages_node[nid]++;
- }
從上面的實(shí)現(xiàn)可知,enqueue_huge_page 函數(shù)只是簡單的把大內(nèi)存頁添加到空閑鏈表 hugepage_freelists 中,并且增加計(jì)數(shù)器。
假如我們設(shè)置了系統(tǒng)能夠使用的大內(nèi)存頁為 100 個,那么空閑大內(nèi)存頁鏈表 hugepage_freelists 的結(jié)構(gòu)如下圖所示:
所以,HugePages 分配器初始化的調(diào)用鏈為:
- hugetlb_init()
- |
- +——> alloc_fresh_huge_page()
- |
- |——> alloc_pages_node()
- |——> set_compound_page_dtor()
- +——> put_page()
- |
- +——> free_huge_page()
- |
- +——> enqueue_huge_page()
hugetlbfs 文件系統(tǒng)
為系統(tǒng)準(zhǔn)備好空閑的大內(nèi)存頁后,現(xiàn)在來了解下怎樣分配大內(nèi)存頁。在《一文讀懂 HugePages的原理》一文中介紹過,要申請大內(nèi)存頁,必須使用 mmap 系統(tǒng)調(diào)用把虛擬內(nèi)存映射到 hugetlbfs 文件系統(tǒng)中的文件中。
免去繁瑣的文件系統(tǒng)掛載過程,我們主要來看看當(dāng)使用 mmap 系統(tǒng)調(diào)用把虛擬內(nèi)存映射到 hugetlbfs 文件系統(tǒng)的文件時會發(fā)生什么事情。
每個文件描述符對象都有個 mmap 的方法,此方法會在調(diào)用 mmap 函數(shù)映射到文件時被觸發(fā),我們來看看 hugetlbfs 文件的 mmap 方法所對應(yīng)的真實(shí)函數(shù),如下:
- const struct file_operations hugetlbfs_file_operations = {
- .mmap = hugetlbfs_file_mmap,
- .fsync = simple_sync_file,
- .get_unmapped_area = hugetlb_get_unmapped_area,
- };
從上面的代碼可以發(fā)現(xiàn),hugetlbfs 文件的 mmap 方法被設(shè)置為 hugetlbfs_file_mmap 函數(shù)。所以當(dāng)調(diào)用 mmap 函數(shù)映射 hugetlbfs 文件時,將會調(diào)用 hugetlbfs_file_mmap 函數(shù)來處理。
而 hugetlbfs_file_mmap 函數(shù)最主要的工作就是把虛擬內(nèi)存分區(qū)對象的 vm_flags 字段添加 VM_HUGETLB 標(biāo)志位,如下代碼:
- static int
- hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma)
- {
- ...
- vma->vm_flags |= VM_HUGETLB | VM_RESERVED; // 為虛擬內(nèi)存分區(qū)添加 VM_HUGETLB 標(biāo)志位
- ...
- return ret;
- }
為虛擬內(nèi)存分區(qū)對象設(shè)置 VM_HUGETLB 標(biāo)志位的作用是:當(dāng)對虛擬內(nèi)存分區(qū)進(jìn)行物理內(nèi)存映射時,會進(jìn)行特殊的處理,下面將會介紹。
虛擬內(nèi)存與物理內(nèi)存映射
使用 mmap 函數(shù)映射到 hugetlbfs 文件后,會返回一個虛擬內(nèi)存地址。當(dāng)對這個虛擬內(nèi)存地址進(jìn)行訪問(讀寫)時,由于此虛擬內(nèi)存地址還沒有與物理內(nèi)存地址進(jìn)行映射,將會觸發(fā) 缺頁異常,內(nèi)核會調(diào)用 do_page_fault 函數(shù)對 缺頁異常 進(jìn)行修復(fù)。
我們來看看整個流程,如下圖所示:
所以,最終會調(diào)用 do_page_fault 函數(shù)對 缺頁異常 進(jìn)行修復(fù)操作,我們來看看 do_page_fault 做了什么工作,實(shí)現(xiàn)如下:
- asmlinkage void
- __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
- {
- ...
- struct mm_struct *mm;
- struct vm_area_struct *vma;
- unsigned long address;
- ...
- mm = tsk->mm; // 1. 獲取當(dāng)前進(jìn)程對應(yīng)的內(nèi)存管理對象
- address = read_cr2(); // 2. 獲取觸發(fā)缺頁異常的虛擬內(nèi)存地址
- ...
- vma = find_vma(mm, address); // 3. 通過虛擬內(nèi)存地址獲取對應(yīng)的虛擬內(nèi)存分區(qū)對象
- ...
- // 4. 調(diào)用 handle_mm_fault 函數(shù)對異常進(jìn)行修復(fù)
- fault = handle_mm_fault(mm, vma, address, write);
- ...
- return;
- }
上面代碼對 do_page_fault 進(jìn)行了精簡,精簡后主要完成4個工作:
- 獲取當(dāng)前進(jìn)程對應(yīng)的內(nèi)存管理對象。
- 調(diào)用 read_cr2 獲取觸發(fā)缺頁異常的虛擬內(nèi)存地址。
- 通過觸發(fā) 缺頁異常 的虛擬內(nèi)存地址獲取對應(yīng)的虛擬內(nèi)存分區(qū)對象。
- 調(diào)用 handle_mm_fault 函數(shù)對 缺頁異常 進(jìn)行修復(fù)。
我們繼續(xù)來看看 handle_mm_fault 函數(shù)的實(shí)現(xiàn),代碼如下:
- int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
- unsigned long address, int write_access)
- {
- ...
- if (unlikely(is_vm_hugetlb_page(vma))) // 虛擬內(nèi)存分區(qū)是否需要使用 HugePages
- return hugetlb_fault(mm, vma, address, write_access); // 如果使用 HugePages, 就調(diào)用 hugetlb_fault 進(jìn)行處理
- ...
- }
對 handle_mm_fault 函數(shù)進(jìn)行精簡后,邏輯就非常清晰。如果虛擬內(nèi)存分區(qū)使用 HugePages,那么就調(diào)用 hugetlb_fault 函數(shù)進(jìn)行處理(由于我們分析使用 HugePages 的情況,所以剛好進(jìn)入這個分支)。
hugetlb_fault 函數(shù)主要對進(jìn)程的頁表進(jìn)行填充,所以我們先來回顧一下 HugePages 對應(yīng)的頁表結(jié)構(gòu),如下圖:
從上圖可以看出,使用 HugePages 后,頁中間目錄 直接指向物理內(nèi)存頁。所以,hugetlb_fault 函數(shù)主要就是對 頁中間目錄項(xiàng) 進(jìn)行填充。實(shí)現(xiàn)如下:
- int hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
- unsigned long address, int write_access)
- {
- pte_t *ptep;
- pte_t entry;
- int ret;
- ptep = huge_pte_alloc(mm, address); // 1. 找到虛擬內(nèi)存地址對應(yīng)的頁中間目錄項(xiàng)
- ...
- entry = *ptep;
- if (pte_none(entry)) { // 如果頁中間目錄項(xiàng)還沒進(jìn)行映射
- // 2. 那么調(diào)用 hugetlb_no_page 函數(shù)進(jìn)行映射操作
- ret = hugetlb_no_page(mm, vma, address, ptep, write_access);
- ...
- return ret;
- }
- ...
- }
對 hugetlb_fault 函數(shù)進(jìn)行精簡后,主要完成兩個工作:
- 通過觸發(fā) 缺頁異常 的虛擬內(nèi)存地址找到其對應(yīng)的 頁中間目錄項(xiàng)。
- 調(diào)用 hugetlb_no_page 函數(shù)對 頁中間目錄項(xiàng) 進(jìn)行映射操作。
我們再來看看 hugetlb_no_page 函數(shù)怎么對 頁中間目錄項(xiàng) 進(jìn)行填充:
- static int
- hugetlb_no_page(struct mm_struct *mm, struct vm_area_struct *vma,
- unsigned long address, pte_t *ptep, int write_access)
- {
- ...
- page = find_lock_page(mapping, idx);
- if (!page) {
- ...
- // 1. 從空閑大內(nèi)存頁鏈表 hugepage_freelists 中申請一個大內(nèi)存頁
- page = alloc_huge_page(vma, address);
- ...
- }
- ...
- // 2. 通過大內(nèi)存頁的物理地址生成頁中間目錄項(xiàng)的值
- new_pte = make_huge_pte(vma, page, ((vma->vm_flags & VM_WRITE)
- && (vma->vm_flags & VM_SHARED)));
- // 3. 設(shè)置頁中間目錄項(xiàng)的值為上面生成的值
- set_huge_pte_at(mm, address, ptep, new_pte);
- ...
- return ret;
- }
通過對 hugetlb_no_page 函數(shù)進(jìn)行精簡后,主要完成3個工作:
- 調(diào)用 alloc_huge_page 函數(shù)從空閑大內(nèi)存頁鏈表 hugepage_freelists 中申請一個大內(nèi)存頁。
- 通過大內(nèi)存頁的物理地址生成頁中間目錄項(xiàng)的值。
- 設(shè)置頁中間目錄項(xiàng)的值為上面生成的值。
至此,HugePages 的映射過程已經(jīng)完成。
還有個問題,就是 CPU 怎么知道 頁中間表項(xiàng) 指向的是 頁表 還是 大內(nèi)存頁 呢?
這是因?yàn)?頁中間表項(xiàng) 有個 PSE 的標(biāo)志位,如果將其設(shè)置為1,那么就表明其指向 大內(nèi)存頁 ,否則就指向 頁表。
總結(jié)
本文介紹了 HugePages 實(shí)現(xiàn)的整個流程,當(dāng)然本文也只是介紹了申請內(nèi)存的流程,釋放內(nèi)存的流程并沒有分析,如果有興趣的話可以自己查閱源碼。