一文讀懂 Linux 記憶體分配全過程

記憶體分割槽物件

Linux 會把程序虛擬記憶體空間劃分為多個分割槽,在 Linux 核心中使用

vm_area_struct

物件來表示,其定義如下:

1struct vm_area_struct { 2   struct mm_struct *vm_mm;        // 分割槽所屬的記憶體管理物件 3 4   unsigned long vm_start;         // 分割槽的開始地址 5   unsigned long vm_end;           // 分割槽的結束地址 6 7   struct vm_area_struct *vm_next; // 透過這個指標把程序所有的記憶體分割槽連線成一個連結串列 8  。。。 9   struct rb_node vm_rb;           // 紅黑樹的節點, 用於儲存到記憶體分割槽紅黑樹中10  。。。11};

我們對

vm_area_struct

物件進行了簡化,只保留了本文需要的欄位。 核心就是使用

vm_area_struct

物件來記錄一個記憶體分割槽(如

程式碼段

資料段

堆空間

等),下面介紹一下

vm_area_struct

物件各個欄位的作用:

vm_mm

:指定了當前記憶體分割槽所屬的記憶體管理物件。

vm_start

:記憶體分割槽的開始地址。

vm_end

:記憶體分割槽的結束地址。

vm_next

:透過這個指標把程序中所有的記憶體分割槽連線成一個連結串列。

vm_rb

:另外,為了快速查詢記憶體分割槽,核心還把程序的所有記憶體分割槽儲存到一棵紅黑樹中。

vm_rb

就是紅黑樹的節點,用於把記憶體分割槽儲存到紅黑樹中。

假如程序 A 現在有 4 個記憶體分割槽,它們的範圍如下:

程式碼段

:00400000 ~ 00401000

資料段

:00600000 ~ 00601000

堆空間

:00983000 ~ 009a4000

棧空間

:7f37ce866000 ~ 7f3fce867000

那麼這 4 個記憶體分割槽在核心中的結構如 圖1 所示:

一文讀懂 Linux 記憶體分配全過程

在 圖1 中,我們可以看到有個

mm_struct

的物件,此物件每個程序都持有一個,是程序虛擬記憶體空間和物理記憶體空間的管理物件。我們簡單介紹一下這個物件,其定義如下:

1struct mm_struct {2   struct vm_area_struct *mmap;  // 指向由程序記憶體分割槽連線成的連結串列3   struct rb_root mm_rb;         // 核心使用紅黑樹儲存程序的所有記憶體分割槽, 這個是紅黑樹的根節點4   unsigned long start_brk, brk; // 堆空間的開始地址和結束地址5  。。。6};

我們來介紹下

mm_struct

物件各個欄位的作用:

mmap

:指向由程序所有記憶體分割槽連線成的連結串列。

mm_rb

:核心為了加快查詢記憶體分割槽的速度,使用了紅黑樹儲存所有記憶體分割槽,這個就是紅黑樹的根節點。

start_brk

:堆空間的開始記憶體地址。

brk

:堆空間的頂部記憶體地址。

我們來回顧一下程序虛擬記憶體空間的佈局圖,如 圖2 所示:

一文讀懂 Linux 記憶體分配全過程

start_brk

brk

欄位用來記錄堆空間的範圍, 如 圖2 所示。一般來說,

start_brk

是不會變的,而

brk

會隨著分配記憶體和釋放記憶體而變化。

推薦

【Linux核心記憶體管理專題訓練營】火熱開營!!

最新Linux核心技術詳解

獨家Linux核心記憶體管理乾貨分享

兩天持續技術輸出:

—————————— 第一天: 1。物理記憶體對映及空間劃分 2。ARM32/64頁表的對映過程 3。分配物理頁面及Slab分配器 4。實戰:VMA查詢/插入/合併 —————————— 第二天: 5。實戰:mallocap系統呼叫實現 6。缺頁中斷處理/反向對映 7。回收頁面/匿名頁面生命週期 8。KSM實現/Dirty COW記憶體漏洞

原價“198”,現“0。02”特惠!

限時特價入營地址

Linux核心記憶體管理專題訓練營-學習影片教程-騰訊課堂

立即搶購加入吧

一文讀懂 Linux 記憶體分配全過程

虛擬記憶體分配

呼叫

malloc

申請記憶體時,最終會呼叫

brk

系統呼叫來從堆空間中分配記憶體。我們來分析一下

brk

系統呼叫的實現:

1unsigned long sys_brk(unsigned long brk) 2{ 3   unsigned long rlim, retval; 4   unsigned long newbrk, oldbrk; 5   struct mm_struct *mm = current->mm; 6  。。。 7   down_write(&mm->mmap_sem);  // 對記憶體管理物件進行上鎖 8  。。。 9   // 判斷堆空間的大小是否超出限制, 如果超出限制, 就不進行處理10   rlim = current->signal->rlim[RLIMIT_DATA]。rlim_cur;11   if (rlim < RLIM_INFINITY12       && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim)13       goto out;1415   newbrk = PAGE_ALIGN(brk);      // 新的brk值16   oldbrk = PAGE_ALIGN(mm->brk);  // 舊的brk值17   if (oldbrk == newbrk)          // 如果新舊的位置都一樣, 就不需要進行處理18       goto set_brk;19  。。。20   // 呼叫 do_brk 函式進行下一步處理21   if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)22       goto out;2324set_brk:25   mm->brk = brk; // 設定堆空間的頂部位置(brk指標)26out:27   retval = mm->brk;28   up_write(&mm->mmap_sem);29   return retval;30}

總結上面的程式碼,主要有以下幾個步驟:

1、判斷堆空間的大小是否超出限制,如果超出限制,就不作任何處理,直接返回舊的

brk

值。

2、如果新的

brk

值跟舊的

brk

值一致,那麼也不用作任何處理。

3、如果新的

brk

值發生變化,那麼就呼叫

do_brk

函式進行下一步處理。

4、設定程序的

brk

指標(堆空間頂部)為新的

brk

的值。

我們看到第 3 步呼叫了

do_brk

函式來處理,

do_brk

函式的實現有點小複雜,所以這裡介紹一下大概處理流程:

透過堆空間的起始地址

start_brk

從程序記憶體分割槽紅黑樹中找到其對應的記憶體分割槽物件(也就是

vm_area_struct

)。

把堆空間的記憶體分割槽物件的

vm_end

欄位設定為新的

brk

值。

至此,

brk

系統呼叫的工作就完成了(上面沒有分析釋放記憶體的情況),總結來說,

brk

系統呼叫的工作主要有兩部分:

把程序的

brk

指標設定為新的

brk

值。

把堆空間的記憶體分割槽物件的

vm_end

欄位設定為新的

brk

值。

物理記憶體分配

從上面的分析知道,

brk

系統呼叫申請的是

虛擬記憶體

,但儲存資料只能使用

物理記憶體

。所以,虛擬記憶體必須對映到物理記憶體才能被使用。

那麼什麼時候才進行記憶體對映呢?

當對沒有對映的虛擬記憶體地址進行讀寫操作時,CPU 將會觸發

缺頁異常

。核心接收到

缺頁異常

後, 會呼叫

do_page_fault

函式進行修復。

我們來分析一下

do_page_fault

函式的實現(精簡後):

1void do_page_fault(struct pt_regs *regs, unsigned long error_code) 2{ 3   struct vm_area_struct *vma; 4   struct task_struct *tsk; 5   unsigned long address; 6   struct mm_struct *mm; 7   int write; 8   int fault; 910   tsk = current;11   mm = tsk->mm;1213   address = read_cr2(); // 獲取導致頁缺失異常的虛擬記憶體地址14  。。。15   vma = find_vma(mm, address); // 透過虛擬記憶體地址從程序記憶體分割槽中查詢對應的記憶體分割槽物件16  。。。17   if (likely(vma->vm_start <= address)) // 如果找到記憶體分割槽物件18       goto good_area;19  。。。2021good_area:22   write = error_code & PF_WRITE;23  。。。24   // 呼叫 handle_mm_fault 函式對虛擬記憶體地址進行對映操作25   fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);26  。。。27}

do_page_fault

函式主要完成以下操作:

獲取導致頁缺失異常的虛擬記憶體地址,儲存到

address

變數中。

呼叫

find_vma

函式從程序記憶體分割槽中查詢異常的虛擬記憶體地址對應的記憶體分割槽物件。

如果找到記憶體分割槽物件,那麼呼叫

handle_mm_fault

函式對虛擬記憶體地址進行對映操作。

從上面的分析可知,對虛擬記憶體進行對映操作是透過

handle_mm_fault

函式完成的,而

handle_mm_fault

函式的主要工作就是完成對程序

頁表

的填充。 我們透過 圖3 來理解記憶體對映的原理,可以參考文章《一文讀懂 HugePages的原理》:

一文讀懂 Linux 記憶體分配全過程

下面我們來分析一下

handle_mm_fault

的實現,程式碼如下:

1int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, 2                   unsigned long address, unsigned int flags) 3{ 4   pgd_t *pgd;  // 頁全域性目錄項 5   pud_t *pud;  // 頁上級目錄項 6   pmd_t *pmd;  // 頁中間目錄項 7   pte_t *pte;  // 頁表項 8  。。。 9   pgd = pgd_offset(mm, address);         // 獲取虛擬記憶體地址對應的頁全域性目錄項10   pud = pud_alloc(mm, pgd, address);     // 獲取虛擬記憶體地址對應的頁上級目錄項11  。。。12   pmd = pmd_alloc(mm, pud, address);     // 獲取虛擬記憶體地址對應的頁中間目錄項13  。。。14   pte = pte_alloc_map(mm, pmd, address); // 獲取虛擬記憶體地址對應的頁表項15  。。。16   // 對頁表項進行對映17   return handle_pte_fault(mm, vma, address, pte, pmd, flags);18}

handle_mm_fault

函式主要對每一級的頁表進行對映(對照 圖3 就容易理解),最終呼叫

handle_pte_fault

函式對

頁表項

進行對映。

我們繼續來分析

handle_pte_fault

函式的實現,程式碼如下:

1static inline int 2handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, 3                unsigned long address, pte_t *pte, pmd_t *pmd, 4                unsigned int flags) 5{ 6   pte_t entry; 7 8   entry = *pte; 910   if (!pte_present(entry)) { // 還沒有對映到物理記憶體11       if (pte_none(entry)) {12          。。。13           // 呼叫 do_anonymous_page 函式進行匿名頁對映(堆空間就是使用匿名頁)14           return do_anonymous_page(mm, vma, address, pte, pmd, flags);15      }16      。。。17  }18  。。。19}

上面程式碼簡化了很多與本文無關的邏輯。從上面程式碼可以看出,

handle_pte_fault

函式最終會呼叫

do_anonymous_page

來完成記憶體對映操作,我們接著來分析下

do_anonymous_page

函式的實現:

1static int 2do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma, 3                 unsigned long address, pte_t *page_table, pmd_t *pmd, 4                 unsigned int flags) 5{ 6   struct page *page; 7   spinlock_t *ptl; 8   pte_t entry; 910   if (!(flags & FAULT_FLAG_WRITE)) { // 如果是讀操作導致的異常11       // 使用 `零頁` 進行對映12       entry = pte_mkspecial(pfn_pte(my_zero_pfn(address), vma->vm_page_prot));13      。。。14       goto setpte;15  }16  。。。17   // 如果是寫操作導致的異常18   // 申請一塊新的物理記憶體頁19   page = alloc_zeroed_user_highpage_movable(vma, address);20  。。。21   // 根據物理記憶體頁的地址生成對映關係22   entry = mk_pte(page, vma->vm_page_prot);23   if (vma->vm_flags & VM_WRITE)24       entry = pte_mkwrite(pte_mkdirty(entry));25  。。。26setpte:27   set_pte_at(mm, address, page_table, entry); // 設定頁表項為新的對映關係28  。。。29   return 0;30}

do_anonymous_page

函式的實現比較有趣,它會根據

缺頁異常

是由讀操作還是寫操作導致的,分為兩個不同的處理邏輯,如下:

如果是讀操作導致的,那麼將會使用

零頁

進行對映(

零頁

是 Linux 核心中一個比較特殊的記憶體頁,所有讀操作引起的

缺頁異常

都會指向此頁,從而可以減少物理記憶體的消耗),並且設定其為只讀(因為

零頁

是不能進行寫操作)。如果下次對此頁進行寫操作,將會觸發寫操作的

缺頁異常

,從而進入下面步驟。

如果是寫操作導致的,就申請一塊新的物理記憶體頁,然後根據物理記憶體頁的地址生成對映關係,再對頁表項進行填充(對映)。

總結

本文主要介紹了 Linux 記憶體分配的整個過程,當然只是介紹從堆空間分配的記憶體的過程。Linux 分配記憶體的方式還有很多,比如

mmap

HugePages

等,有興趣的可以查閱相關的資料和書籍。

作者:JaydenLie 連結:https://juejin。cn/post/6959754877400514574 來源:掘金 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。