国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

這篇具有很好參考價(jià)值的文章主要介紹了一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

本文基于內(nèi)核 5.4 版本源碼討論

在前面兩篇介紹 mmap 的文章中,筆者分別從原理角度以及源碼實(shí)現(xiàn)角度帶著大家深入到內(nèi)核世界深度揭秘了 mmap 內(nèi)存映射的本質(zhì)。從整個(gè) mmap 映射的過程可以看出,內(nèi)核只是在進(jìn)程的虛擬地址空間中尋找出一段空閑的虛擬內(nèi)存區(qū)域 vma 然后分配給本次映射而已。

    vma = vm_area_alloc(mm);
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = vm_get_page_prot(vm_flags);
    vma->vm_pgoff = pgoff;

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

如果是文件映射的話,內(nèi)核還會(huì)額外做一項(xiàng)工作,就是將分配出來的這段虛擬內(nèi)存區(qū)域 vma 與映射文件關(guān)聯(lián)映射起來。

vma->vm_file = get_file(file);
error = call_mmap(file, vma);

映射的核心就是將虛擬內(nèi)存區(qū)域 vm_area_struct 相關(guān)的內(nèi)存操作 vma->vm_ops 設(shè)置為文件系統(tǒng)的相關(guān)操作 ext4_file_vm_ops。這樣一來,進(jìn)程后續(xù)對(duì)這段虛擬內(nèi)存的讀寫就相當(dāng)于是讀寫映射文件了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

無論是匿名映射還是文件映射,內(nèi)核在處理 mmap 映射過程中貌似都是在進(jìn)程的虛擬地址空間中和虛擬內(nèi)存打交道,僅僅只是為 mmap 映射分配出一段虛擬內(nèi)存而已,整個(gè)映射過程我們并沒有看到物理內(nèi)存的身影。

那么大家所關(guān)心的物理內(nèi)存到底是什么時(shí)候映射進(jìn)來的呢 ?這就是今天本文要討論的主題 —— 缺頁中斷。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

1. 缺頁中斷產(chǎn)生的原因

如下圖所示,當(dāng) mmap 系統(tǒng)調(diào)用成功返回之后,內(nèi)核只是為進(jìn)程分配了一段 [vm_start , vm_end] 范圍內(nèi)的虛擬內(nèi)存區(qū)域 vma ,由于還未與物理內(nèi)存發(fā)生關(guān)聯(lián),所以此時(shí)進(jìn)程頁表中與 mmap 映射的虛擬內(nèi)存相關(guān)的各級(jí)頁目錄和頁表項(xiàng)還都是空的。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng) CPU 訪問這段由 mmap 映射出來的虛擬內(nèi)存區(qū)域 vma 中的任意虛擬地址時(shí),MMU 在遍歷進(jìn)程頁表的時(shí)候就會(huì)發(fā)現(xiàn),該虛擬內(nèi)存地址在進(jìn)程頂級(jí)頁目錄 PGD(Page Global Directory)中對(duì)應(yīng)的頁目錄項(xiàng) pgd_t 是空的,該 pgd_t 并沒有指向其下一級(jí)頁目錄 PUD(Page Upper Directory)。

也就是說,此時(shí)進(jìn)程頁表中只有一張頂級(jí)頁目錄表 PGD,而上層頁目錄 PUD(Page Upper Directory),中間頁目錄 PMD(Page Middle Directory),一級(jí)頁表(Page Table)內(nèi)核都還沒有創(chuàng)建。

由于現(xiàn)在被訪問到的虛擬內(nèi)存地址對(duì)應(yīng)的 pgd_t 是空的,進(jìn)程的四級(jí)頁表體系還未建立,所以 MMU 會(huì)產(chǎn)生一個(gè)缺頁中斷,進(jìn)程從用戶態(tài)轉(zhuǎn)入內(nèi)核態(tài)來處理這個(gè)缺頁異常。

此時(shí) CPU 會(huì)將發(fā)生缺頁異常時(shí),進(jìn)程正在使用的相關(guān)寄存器中的值壓入內(nèi)核棧中。比如,引起進(jìn)程缺頁異常的虛擬內(nèi)存地址會(huì)被存放在 CR2 寄存器中。同時(shí) CPU 還會(huì)將缺頁異常的錯(cuò)誤碼 error_code 壓入內(nèi)核棧中。

隨后內(nèi)核會(huì)在 do_page_fault 函數(shù)中來處理缺頁異常,該函數(shù)的參數(shù)都是內(nèi)核在處理缺頁異常的時(shí)候需要用到的基本信息:

dotraplinkage void
do_page_fault(struct pt_regs *regs, unsigned long error_code, unsigned long address)

struct pt_regs 結(jié)構(gòu)中存放的是缺頁異常發(fā)生時(shí),正在使用中的寄存器值的集合。address 表示觸發(fā)缺頁異常的虛擬內(nèi)存地址。

error_code 是對(duì)缺頁異常的一個(gè)描述,目前內(nèi)核只使用了 error_code 的前六個(gè)比特位來描述引起缺頁異常的具體原因,后面比特位的含義我們先暫時(shí)忽略。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

P(0) : 如果 error_code 第 0 個(gè)比特位置為 0 ,表示該缺頁異常是由于 CPU 訪問的這個(gè)虛擬內(nèi)存地址 address 背后并沒有一個(gè)物理內(nèi)存頁與之映射而引起的,站在進(jìn)程頁表的角度來說,就是 CPU 訪問的這個(gè)虛擬內(nèi)存地址 address 在進(jìn)程四級(jí)頁表體系中對(duì)應(yīng)的各級(jí)頁目錄項(xiàng)或者頁表項(xiàng)是空的(頁目錄項(xiàng)或者頁表項(xiàng)中的 P 位為 0 )。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

如果 error_code 第 0 個(gè)比特位置為 1,表示 CPU 訪問的這個(gè)虛擬內(nèi)存地址背后雖然有物理內(nèi)存頁與之映射,但是由于訪問權(quán)限不夠而引起的缺頁異常(保護(hù)異常),比如,進(jìn)程嘗試對(duì)一個(gè)只讀的物理內(nèi)存頁進(jìn)行寫操作,那么就會(huì)引起寫保護(hù)類型的缺頁異常。

R/W(1) : 表示引起缺頁異常的訪問類型是什么 ? 如果 error_code 第 1 個(gè)比特位置為 0,表示是由于讀訪問引起的。置為 1 表示是由于寫訪問引起的。

注意:該標(biāo)志位只是為了描述是哪種訪問類型造成了本次缺頁異常,這個(gè)和前面提到的訪問權(quán)限沒有關(guān)系。比如,進(jìn)程嘗試對(duì)一個(gè)可寫的虛擬內(nèi)存頁進(jìn)行寫入,訪問權(quán)限沒有問題,但是該虛擬內(nèi)存頁背后并未有物理內(nèi)存與之關(guān)聯(lián),所以也會(huì)導(dǎo)致缺頁異常。這種情況下,error_code 的 P 位就會(huì)設(shè)置為 0,R/W 位就會(huì)設(shè)置為 1 。

U/S(2):表示缺頁異常發(fā)生在用戶態(tài)還是內(nèi)核態(tài),error_code 第 2 個(gè)比特位設(shè)置為 0 表示 CPU 訪問內(nèi)核空間的地址引起的缺頁異常,設(shè)置為 1 表示 CPU 訪問用戶空間的地址引起的缺頁異常。

RSVD(3):這里用于檢測(cè)頁表項(xiàng)中的保留位(Reserved 相關(guān)的比特位)是否設(shè)置,這些頁表項(xiàng)中的保留位都是預(yù)留給內(nèi)核以后的相關(guān)功能使用的,所以在缺頁的時(shí)候需要檢查這些保留位是否設(shè)置,從而決定近一步的擴(kuò)展處理。設(shè)置為 1 表示頁表項(xiàng)中預(yù)留的這些比特位被使用了。設(shè)置為 0 表示頁表項(xiàng)中預(yù)留的這些比特位還沒有被使用。

I/D(4):設(shè)置為 1 ,表示本次缺頁異常是在 CPU 獲取指令的時(shí)候引起的。

PK(5):設(shè)置為 1,表示引起缺頁異常的虛擬內(nèi)存地址對(duì)應(yīng)頁表項(xiàng)中的 Protection 相關(guān)的比特位被設(shè)置了。

error_code 比特位的含義定義在文件 /arch/x86/include/asm/traps.h 中:

/*
 * Page fault error code bits:
 *
 *   bit 0 ==	 0: no page found	1: protection fault
 *   bit 1 ==	 0: read access		1: write access
 *   bit 2 ==	 0: kernel-mode access	1: user-mode access
 *   bit 3 ==				1: use of reserved bit detected
 *   bit 4 ==				1: fault was an instruction fetch
 *   bit 5 ==				1: protection keys block access
 */
enum x86_pf_error_code {
	X86_PF_PROT	=		1 << 0,
	X86_PF_WRITE	=		1 << 1,
	X86_PF_USER	=		1 << 2,
	X86_PF_RSVD	=		1 << 3,
	X86_PF_INSTR	=		1 << 4,
	X86_PF_PK	=		1 << 5,
};

2. 內(nèi)核處理缺頁中斷的入口 —— do_page_fault

經(jīng)過上一小節(jié)的介紹我們知道,缺頁中斷產(chǎn)生的根本原因是由于 CPU 訪問的這段虛擬內(nèi)存背后沒有物理內(nèi)存與之映射,表現(xiàn)的具體形式主要有三種:

  1. 虛擬內(nèi)存對(duì)應(yīng)在進(jìn)程頁表體系中的相關(guān)各級(jí)頁目錄或者頁表是空的,也就是說這段虛擬內(nèi)存完全沒有被映射過。

  2. 虛擬內(nèi)存之前被映射過,其在進(jìn)程頁表的各級(jí)頁目錄以及頁表中均有對(duì)應(yīng)的頁目錄項(xiàng)和頁表項(xiàng),但是其對(duì)應(yīng)的物理內(nèi)存被內(nèi)核 swap out 到磁盤上了。

  3. 虛擬內(nèi)存雖然背后映射著物理內(nèi)存,但是由于對(duì)物理內(nèi)存的訪問權(quán)限不夠而導(dǎo)致的保護(hù)類型的缺頁中斷。比如,嘗試去寫一個(gè)只讀的物理內(nèi)存頁。

雖然缺頁中斷產(chǎn)生的原因多種多樣,內(nèi)核也會(huì)根據(jù)不同的缺頁原因進(jìn)行不同的處理,但不管怎么說,一切的起點(diǎn)都是從 CPU 訪問虛擬內(nèi)存開始的,既然提到了虛擬內(nèi)存,我們就不得不回顧一下進(jìn)程虛擬內(nèi)存空間的布局:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在 64 位體系結(jié)構(gòu)下,進(jìn)程虛擬內(nèi)存空間總體上分為兩個(gè)部分,一部分是 128T 的用戶空間,地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF FFFF 。但實(shí)際上,Linux 內(nèi)核是用 TASK_SIZE_MAX 來定義用戶空間的末尾的,也就是說 Linux 內(nèi)核是使用 TASK_SIZE_MAX 來分割用戶虛擬地址空間與內(nèi)核虛擬地址空間的

#define TASK_SIZE_MAX  task_size_max()

#define task_size_max()  ((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

#define __VIRTUAL_MASK_SHIFT 47

#define PAGE_SHIFT  12
#define PAGE_SIZE  (_AC(1,UL) << PAGE_SHIFT)

TASK_SIZE_MAX 的計(jì)算邏輯首先是將 1 左移 47 位得到的地址是 0x0000800000000000,然后減去一個(gè) PAGE_SIZE (4K),就是 0x00007FFFFFFFF000,所以實(shí)際上,64 位體系結(jié)構(gòu)的 Linux 內(nèi)核中,進(jìn)程用戶空間實(shí)際可用的虛擬地址范圍是:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000。

進(jìn)程虛擬內(nèi)存空間的另一部分則是 128T 的內(nèi)核空間,虛擬地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF。由于在內(nèi)核空間的一開始包含了 8T 的地址空洞,所以內(nèi)核空間實(shí)際可用的虛擬地址范圍是:0xFFFF 8800 0000 0000 - 0xFFFF FFFF FFFF FFFF

既然進(jìn)程虛擬內(nèi)存地址范圍有用戶空間與內(nèi)核空間之分,那么當(dāng) CPU 訪問虛擬內(nèi)存地址時(shí)產(chǎn)生的缺頁中斷也要區(qū)分下是用戶空間產(chǎn)生的缺頁還是內(nèi)核空間產(chǎn)生的缺頁。

static int fault_in_kernel_space(unsigned long address)
{
    /*
     * On 64-bit systems, the vsyscall page is at an address above
     * TASK_SIZE_MAX, but is not considered part of the kernel
     * address space.
     */
    if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
        return false;
    // 在進(jìn)程虛擬內(nèi)存空間中,TASK_SIZE_MAX 以上的虛擬地址均屬于內(nèi)核空間
    return address >= TASK_SIZE_MAX;
}

當(dāng)引起缺頁中斷的虛擬內(nèi)存地址 address 是在 TASK_SIZE_MAX 之上時(shí),表示該缺頁地址是屬于內(nèi)核空間的,內(nèi)核的缺頁處理程序 __do_page_fault 就要進(jìn)入 do_kern_addr_fault 分支去處理內(nèi)核空間的缺頁中斷。

當(dāng)引起缺頁中斷的虛擬內(nèi)存地址 address 是在 TASK_SIZE_MAX 之下時(shí),表示該缺頁地址是屬于用戶空間的,內(nèi)核則進(jìn)入 do_user_addr_fault 分支處理用戶空間的缺頁中斷。

static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long hw_error_code,
        unsigned long address)
{
    // mmap_sem 是進(jìn)程虛擬內(nèi)存空間 mm_struct 的讀寫鎖
    // 內(nèi)核這里將 mmap_sem 預(yù)取到 cacheline 中,并標(biāo)記為獨(dú)占狀態(tài)( MESI 協(xié)議中的 X 狀態(tài))
    prefetchw(&current->mm->mmap_sem);

    // 這里判斷引起缺頁異常的虛擬內(nèi)存地址 address 是屬于內(nèi)核空間的還是用戶空間的
    if (unlikely(fault_in_kernel_space(address)))
        // 如果缺頁異常發(fā)生在內(nèi)核空間,則由 vmalloc_fault 進(jìn)行處理
        // 這里使用 unlikely 的原因是,內(nèi)核對(duì)內(nèi)存的使用通常是高優(yōu)先級(jí)的而且使用比較頻繁,所以內(nèi)核空間一般很少發(fā)生缺頁異常。
        do_kern_addr_fault(regs, hw_error_code, address);
    else
        // 缺頁異常發(fā)生在用戶態(tài)
        do_user_addr_fault(regs, hw_error_code, address);
}
NOKPROBE_SYMBOL(__do_page_fault);

進(jìn)程工作在內(nèi)核空間,就相當(dāng)于你工作在你們公司的核心部門,負(fù)責(zé)的是公司的核心業(yè)務(wù),公司所有的資源都會(huì)向核心部門傾斜,可以說是要什么給什么。

進(jìn)程在內(nèi)核空間工作也是一樣的道理,由于內(nèi)核負(fù)責(zé)的是整個(gè)系統(tǒng)最為核心的任務(wù),基本上系統(tǒng)中所有的資源都會(huì)向內(nèi)核傾斜,物理內(nèi)存資源也是一樣。內(nèi)核對(duì)內(nèi)存的申請(qǐng)優(yōu)先級(jí)是最高的,使用頻率也是最頻繁的。

所以在為內(nèi)核分配完虛擬內(nèi)存之后,都會(huì)立即分配物理內(nèi)存,而且是申請(qǐng)多少給多少,最大程度上優(yōu)先保證內(nèi)核的工作穩(wěn)定進(jìn)行。因此通常在內(nèi)核中,缺頁中斷一般很少發(fā)生,這也是在上面那段內(nèi)核代碼中,用 unlikely 修飾 fault_in_kernel_space 函數(shù)的原因。

而進(jìn)程工作在用戶空間,就相當(dāng)于你工作在你們公司的非核心部門,負(fù)責(zé)的是公司的邊緣業(yè)務(wù),公司沒有那么多的資源提供給你,你在工作中需要申請(qǐng)的資源,公司不會(huì)馬上提供給你,而是需要延遲到?jīng)]有這些資源你的工作就無法進(jìn)行的時(shí)候(你真正必須使用的時(shí)候),公司迫不得已才會(huì)把資源分配給你。也就是說,你用到什么的時(shí)候才會(huì)給你什么,而不是像你在核心部門那樣,要什么就給你什么。

比如,筆者在前面兩篇文章中為大家介紹的 mmap 內(nèi)存映射,就是工作在進(jìn)程用戶地址空間中的文件映射與匿名映射區(qū),進(jìn)程在使用 mmap 申請(qǐng)內(nèi)存的時(shí)候,內(nèi)核僅僅只是為進(jìn)程在文件映射與匿名映射區(qū)分配一段虛擬內(nèi)存,重要的物理內(nèi)存資源不會(huì)馬上分配,而是延遲到進(jìn)程真正使用的時(shí)候,才會(huì)通過缺頁中斷 __do_page_fault 進(jìn)入到 do_user_addr_fault 分支進(jìn)行物理內(nèi)存資源的分配。

內(nèi)核空間中的缺頁異常主要發(fā)生在進(jìn)程內(nèi)核虛擬地址空間中 32T 的 vmalloc 映射區(qū),這段區(qū)域的虛擬內(nèi)存地址范圍為:0xFFFF C900 0000 0000 - 0xFFFF E900 0000 0000。內(nèi)核中的 vmalloc 內(nèi)存分配接口就工作在這個(gè)區(qū)域,它用于將那些不連續(xù)的物理內(nèi)存映射到連續(xù)的虛擬內(nèi)存上。

3. 內(nèi)核態(tài)缺頁異常處理 —— do_kern_addr_fault

do_kern_addr_fault 函數(shù)的工作主要就是處理內(nèi)核虛擬內(nèi)存空間中 vmalloc 映射區(qū)里的缺頁異常,這一部分內(nèi)容,筆者會(huì)在 vmalloc_fault 函數(shù)中進(jìn)行介紹。

static void
do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code,
           unsigned long address)
{
    // 該缺頁的內(nèi)核地址 address 在內(nèi)核頁表中對(duì)應(yīng)的 pte 不能使用保留位(X86_PF_RSVD = 0)
    // 不能是用戶態(tài)的缺頁中斷(X86_PF_USER = 0)
    // 且不能是保護(hù)類型的缺頁中斷 (X86_PF_PROT = 0)
    if (!(hw_error_code & (X86_PF_RSVD | X86_PF_USER | X86_PF_PROT))) {
        // 處理 vmalloc 映射區(qū)里的缺頁異常
        if (vmalloc_fault(address) >= 0)
            return;
    }
}  

讀到這里,大家可能會(huì)有一個(gè)疑惑,作者你剛剛不是才說了嗎,工作在內(nèi)核就相當(dāng)于工作在公司的核心部門,要什么資源公司就會(huì)給什么資源,在內(nèi)核空間申請(qǐng)?zhí)摂M內(nèi)存的時(shí)候,都會(huì)馬上分配物理內(nèi)存資源,而且申請(qǐng)多少給多少。

既然物理內(nèi)存會(huì)馬上被分配,那為什么內(nèi)核空間中的 vmalloc 映射區(qū)還會(huì)發(fā)生缺頁中斷呢 ?

事實(shí)上,內(nèi)核空間里 vmalloc 映射區(qū)中發(fā)生的缺頁中斷與用戶空間里文件映射與匿名映射區(qū)以及堆中發(fā)生的缺頁中斷是不一樣的。

進(jìn)程在用戶空間中無論是通過 brk 系統(tǒng)調(diào)用在堆中申請(qǐng)內(nèi)存還是通過 mmap 系統(tǒng)調(diào)用在文件與匿名映射區(qū)中申請(qǐng)內(nèi)存,內(nèi)核都只是在相應(yīng)的虛擬內(nèi)存空間中劃分出一段虛擬內(nèi)存來給進(jìn)程使用。

當(dāng)進(jìn)程真正訪問到這段虛擬內(nèi)存地址的時(shí)候,才會(huì)產(chǎn)生缺頁中斷,近而才會(huì)分配物理內(nèi)存,最后將引起本次缺頁的虛擬地址在進(jìn)程頁表中對(duì)應(yīng)的全局頁目錄項(xiàng) pgd,上層頁目錄項(xiàng) pud,中間頁目錄 pmd,頁表項(xiàng) pte 都創(chuàng)建好,然后在 pte 中將虛擬內(nèi)存地址與物理內(nèi)存地址映射起來。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

而內(nèi)核通過 vmalloc 內(nèi)存分配接口在 vmalloc 映射區(qū)申請(qǐng)內(nèi)存的時(shí)候,首先也會(huì)在 32T 大小的 vmalloc 映射區(qū)中劃分出一段未被使用的虛擬內(nèi)存區(qū)域出來,我們暫且叫這段虛擬內(nèi)存區(qū)域?yàn)?vmalloc 區(qū),這一點(diǎn)和前面文章介紹的 mmap 非常相似,只不過 mmap 工作在用戶空間的文件與匿名映射區(qū),vmalloc 工作在內(nèi)核空間的 vmalloc 映射區(qū)。

內(nèi)核空間中的 vmalloc 映射區(qū)就是由這樣一段一段的 vmalloc 區(qū)組成的,每調(diào)用一次 vmalloc 內(nèi)存分配接口,就會(huì)在 vmalloc 映射區(qū)中映射出一段 vmalloc 虛擬內(nèi)存區(qū)域,而且每個(gè) vmalloc 區(qū)之間隔著一個(gè) 4K 大小的 guard page(虛擬內(nèi)存),用于防止內(nèi)存越界,將這些非連續(xù)的物理內(nèi)存區(qū)域隔離起來。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

和 mmap 不同的是,vmalloc 在分配完虛擬內(nèi)存之后,會(huì)馬上為這段虛擬內(nèi)存分配物理內(nèi)存,內(nèi)核會(huì)首先計(jì)算出由 vmalloc 內(nèi)存分配接口映射出的這一段虛擬內(nèi)存區(qū)域 vmalloc 區(qū)中包含的虛擬內(nèi)存頁數(shù),然后調(diào)用伙伴系統(tǒng)依次為這些虛擬內(nèi)存頁分配物理內(nèi)存頁。

3.1 vmalloc

下面是 vmalloc 內(nèi)存分配的核心邏輯,封裝在 __vmalloc_node_range 函數(shù)中:

/**
 * __vmalloc_node_range - allocate virtually contiguous memory
 * Allocate enough pages to cover @size from the page level
 * allocator with @gfp_mask flags.  Map them into contiguous
 * kernel virtual space, using a pagetable protection of @prot.
 *
 * Return: the address of the area or %NULL on failure
 */
void *__vmalloc_node_range(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, unsigned long vm_flags, int node,
            const void *caller)
{
    // 用于描述 vmalloc 虛擬內(nèi)存區(qū)域的數(shù)據(jù)結(jié)構(gòu),同 mmap 中的 vma 結(jié)構(gòu)很相似
    struct vm_struct *area;
    // vmalloc 虛擬內(nèi)存區(qū)域的起始地址
    void *addr;
    unsigned long real_size = size;
    // size 為要申請(qǐng)的 vmalloc 虛擬內(nèi)存區(qū)域大小,這里需要按頁對(duì)齊
    size = PAGE_ALIGN(size);
    // 因?yàn)樵诜峙渫?vmalloc 區(qū)之后,馬上就會(huì)為其分配物理內(nèi)存
    // 所以這里需要檢查 size 大小不能超過當(dāng)前系統(tǒng)中的空閑物理內(nèi)存
    if (!size || (size >> PAGE_SHIFT) > totalram_pages())
        goto fail;

    // 在內(nèi)核空間的 vmalloc 動(dòng)態(tài)映射區(qū)中,劃分出一段空閑的虛擬內(nèi)存區(qū)域 vmalloc 區(qū)出來
    // 這里虛擬內(nèi)存的分配過程和 mmap 在用戶態(tài)文件與匿名映射區(qū)分配虛擬內(nèi)存的過程非常相似,這里就不做過多的介紹了。
    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                vm_flags, start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;
    // 為 vmalloc 虛擬內(nèi)存區(qū)域中的每一個(gè)虛擬內(nèi)存頁分配物理內(nèi)存頁
    // 并在內(nèi)核頁表中將 vmalloc 區(qū)與物理內(nèi)存映射起來
    addr = __vmalloc_area_node(area, gfp_mask, prot, node);
    if (!addr)
        return NULL;

    return addr;
}

同 mmap 用 vm_area_struct 結(jié)構(gòu)來描述其在用戶空間的文件與匿名映射區(qū)分配出來的虛擬內(nèi)存區(qū)域一樣,內(nèi)核空間的 vmalloc 動(dòng)態(tài)映射區(qū)也有一種數(shù)據(jù)結(jié)構(gòu)來專門描述該區(qū)域中的虛擬內(nèi)存區(qū),這個(gè)結(jié)構(gòu)就是下面的 vm_struct。

// 用來描述 vmalloc 區(qū)
struct vm_struct {
    // vmalloc 動(dòng)態(tài)映射區(qū)中的所有虛擬內(nèi)存區(qū)域也都是被一個(gè)單向鏈表所串聯(lián)
    struct vm_struct    *next;
    // vmalloc 區(qū)的起始內(nèi)存地址
    void            *addr;
    // vmalloc 區(qū)的大小
    unsigned long       size;
    // vmalloc 區(qū)的相關(guān)標(biāo)記
    // VM_ALLOC 表示該區(qū)域是由 vmalloc 函數(shù)映射出來的
    // VM_MAP 表示該區(qū)域是由 vmap 函數(shù)映射出來的
    // VM_IOREMAP 表示該區(qū)域是由 ioremap 函數(shù)將硬件設(shè)備的內(nèi)存映射過來的
    unsigned long       flags;
    // struct page 結(jié)構(gòu)的數(shù)組指針,數(shù)組中的每一項(xiàng)指向該虛擬內(nèi)存區(qū)域背后映射的物理內(nèi)存頁。
    struct page     **pages;
    // 該虛擬內(nèi)存區(qū)域包含的物理內(nèi)存頁個(gè)數(shù)
    unsigned int        nr_pages;
    // ioremap 映射硬件設(shè)備物理內(nèi)存的時(shí)候填充
    phys_addr_t     phys_addr;
    // 調(diào)用者的返回地址(這里可忽略)
    const void      *caller;
};

由于內(nèi)核在分配完 vmalloc 虛擬內(nèi)存區(qū)之后,會(huì)馬上為其分配物理內(nèi)存,所以在 vm_struct 結(jié)構(gòu)中有一個(gè) struct page 結(jié)構(gòu)的數(shù)組指針 pages,用于指向該虛擬內(nèi)存區(qū)域背后映射的物理內(nèi)存頁。nr_pages 則是數(shù)組的大小,也表示該虛擬內(nèi)存區(qū)域包含的物理內(nèi)存頁個(gè)數(shù)。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在內(nèi)核中所有的這些 vm_struct 均是被一個(gè)單鏈表串聯(lián)組織的,在早期的內(nèi)核版本中就是通過遍歷這個(gè)單向鏈表來在 vmalloc 動(dòng)態(tài)映射區(qū)中尋找空閑的虛擬內(nèi)存區(qū)域的,后來為了提高查找效率引入了紅黑樹以及雙向鏈表來重新組織這些 vmalloc 區(qū)域,于是專門引入了一個(gè) vmap_area 結(jié)構(gòu)來描述 vmalloc 區(qū)域的組織形式。

struct vmap_area {
    // vmalloc 區(qū)的起始內(nèi)存地址
    unsigned long va_start;
    // vmalloc 區(qū)的結(jié)束內(nèi)存地址
    unsigned long va_end;
    // vmalloc 區(qū)所在紅黑樹中的節(jié)點(diǎn)
    struct rb_node rb_node;         /* address sorted rbtree */
    // vmalloc 區(qū)所在雙向鏈表中的節(jié)點(diǎn)
    struct list_head list;          /* address sorted list */
    // 用于關(guān)聯(lián) vm_struct 結(jié)構(gòu)
    struct vm_struct *vm;          
};

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

看起來和用戶空間中虛擬內(nèi)存區(qū)域的組織形式越來越像了,不同的是由于用戶空間是進(jìn)程間隔離的,所以組織用戶空間虛擬內(nèi)存區(qū)域的紅黑樹以及雙向鏈表是進(jìn)程獨(dú)占的。

struct mm_struct {
     struct vm_area_struct *mmap;  /* list of VMAs */
     struct rb_root mm_rb;
}

而內(nèi)核空間是所有進(jìn)程共享的,所以組織內(nèi)核空間虛擬內(nèi)存區(qū)域的紅黑樹以及雙向鏈表是全局的。

static struct rb_root vmap_area_root = RB_ROOT;
extern struct list_head vmap_area_list;

在我們了解了 vmalloc 動(dòng)態(tài)映射區(qū)中的相關(guān)數(shù)據(jù)結(jié)構(gòu)與組織形式之后,接下來我們看一看為 vmalloc 區(qū)分配物理內(nèi)存的過程:

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                 pgprot_t prot, int node)
{
    // 指向即將為 vmalloc 區(qū)分配的物理內(nèi)存頁
    struct page **pages;
    unsigned int nr_pages, array_size, i;

    // 計(jì)算 vmalloc 區(qū)所需要的虛擬內(nèi)存頁個(gè)數(shù)
    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
    // vm_struct 結(jié)構(gòu)中的 pages 數(shù)組大小,用于存放指向每個(gè)物理內(nèi)存頁的指針
    array_size = (nr_pages * sizeof(struct page *));

    // 首先要為 pages 數(shù)組分配內(nèi)存
    if (array_size > PAGE_SIZE) {
        // array_size 超過 PAGE_SIZE 大小則遞歸調(diào)用 vmalloc 分配數(shù)組所需內(nèi)存
        pages = __vmalloc_node(array_size, 1, nested_gfp|highmem_mask,
                PAGE_KERNEL, node, area->caller);
    } else {
        // 直接調(diào)用 kmalloc 分配數(shù)組所需內(nèi)存
        pages = kmalloc_node(array_size, nested_gfp, node);
    }

    // 初始化 vm_struct
    area->pages = pages;
    area->nr_pages = nr_pages;

    // 依次為 vmalloc 區(qū)中包含的所有虛擬內(nèi)存頁分配物理內(nèi)存
    for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            // 如果沒有特殊指定 numa node,則從當(dāng)前 numa node 中分配物理內(nèi)存頁
            page = alloc_page(alloc_mask|highmem_mask);
        else
            // 否則就從指定的 numa node 中分配物理內(nèi)存頁
            page = alloc_pages_node(node, alloc_mask|highmem_mask, 0);
        // 將分配的物理內(nèi)存頁依次存放到 vm_struct 結(jié)構(gòu)中的 pages 數(shù)組中
        area->pages[i] = page;
    }
    
    atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
    // 修改內(nèi)核主頁表,將剛剛分配出來的所有物理內(nèi)存頁與 vmalloc 虛擬內(nèi)存區(qū)域進(jìn)行映射
    if (map_vm_area(area, prot, pages))
        goto fail;
    // 返回 vmalloc 虛擬內(nèi)存區(qū)域起始地址
    return area->addr;
}

在內(nèi)核中,凡是有物理內(nèi)存出現(xiàn)的地方,就一定伴隨著頁表的映射,vmalloc 也不例外,當(dāng)分配完物理內(nèi)存之后,就需要修改內(nèi)核頁表,然后將物理內(nèi)存映射到 vmalloc 虛擬內(nèi)存區(qū)域中,當(dāng)然了,這個(gè)過程也伴隨著 vmalloc 區(qū)域中的這些虛擬內(nèi)存地址在內(nèi)核頁表中對(duì)應(yīng)的 pgd,pud,pmd,pte 相關(guān)頁目錄項(xiàng)以及頁表項(xiàng)的創(chuàng)建。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

大家需要注意的是,這里的內(nèi)核頁表指的是內(nèi)核主頁表,內(nèi)核主頁表的頂級(jí)頁目錄起始地址存放在 init_mm 結(jié)構(gòu)中的 pgd 屬性中,其值為 swapper_pg_dir。

struct mm_struct init_mm = {
   // 內(nèi)核主頁表
  .pgd    = swapper_pg_dir,
}

#define swapper_pg_dir init_top_pgt

內(nèi)核主頁表在系統(tǒng)初始化的時(shí)候被一段匯編代碼 arch\x86\kernel\head_64.S 所創(chuàng)建。后續(xù)在系統(tǒng)啟動(dòng)函數(shù) start_kernel 中調(diào)用 setup_arch 進(jìn)行初始化。

正如之前文章《一步一圖帶你構(gòu)建 Linux 頁表體系》?中介紹的那樣,普通進(jìn)程在內(nèi)核態(tài)亦或是內(nèi)核線程都是無法直接訪問內(nèi)核主頁表的,它們只能訪問內(nèi)核主頁表的 copy 副本,于是進(jìn)程頁表體系就分為了兩個(gè)部分,一個(gè)是進(jìn)程用戶態(tài)頁表(用戶態(tài)缺頁處理的就是這部分),另一個(gè)就是內(nèi)核頁表的 copy 部分(內(nèi)核態(tài)缺頁處理的是這部分)。

在 fork 系統(tǒng)調(diào)用創(chuàng)建進(jìn)程的時(shí)候,進(jìn)程的用戶態(tài)頁表拷貝自他的父進(jìn)程,而進(jìn)程的內(nèi)核態(tài)頁表則從內(nèi)核主頁表中拷貝,后續(xù)進(jìn)程陷入內(nèi)核態(tài)之后,訪問的就是內(nèi)核主頁表中拷貝的這部分。

這也引出了一個(gè)新的問題,就是內(nèi)核主頁表與其在進(jìn)程中的拷貝副本如何同步呢 ? 這就是本小節(jié),筆者想要和大家交代的主題 —— 內(nèi)核態(tài)缺頁異常的處理。

3.2 vmalloc_fault

當(dāng)內(nèi)核通過 vmalloc 內(nèi)存分配接口修改完內(nèi)核主頁表之后,主頁表中的相關(guān)頁目錄項(xiàng)以及頁表項(xiàng)的內(nèi)容就發(fā)生了改變,而這背后的一切,進(jìn)程現(xiàn)在還被蒙在鼓里,一無所知,此時(shí),進(jìn)程頁表中的內(nèi)核部分相關(guān)的頁目錄項(xiàng)以及頁表項(xiàng)還都是空的。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)進(jìn)程陷入內(nèi)核態(tài)訪問這部分頁表的的時(shí)候,會(huì)發(fā)現(xiàn)相關(guān)頁目錄或者頁表項(xiàng)是空的,就會(huì)進(jìn)入缺頁中斷的內(nèi)核處理部分,也就是前面提到的 vmalloc_fault 函數(shù)中,如果發(fā)現(xiàn)缺頁的虛擬地址在內(nèi)核主頁表頂級(jí)全局頁目錄表中對(duì)應(yīng)的頁目錄項(xiàng) pgd 存在,而缺頁地址在進(jìn)程頁表內(nèi)核部分對(duì)應(yīng)的 pgd 不存在,那么內(nèi)核就會(huì)把內(nèi)核主頁表中 pgd 頁目錄項(xiàng)里的內(nèi)容復(fù)制給進(jìn)程頁表內(nèi)核部分中對(duì)應(yīng)的 pgd。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

事實(shí)上,同步內(nèi)核主頁表的工作只需要將缺頁地址對(duì)應(yīng)在內(nèi)核主頁表中的頂級(jí)全局頁目錄項(xiàng) pgd 同步到進(jìn)程頁表內(nèi)核部分對(duì)應(yīng)的 pgd 地址處就可以了,正如上圖中所示,每一級(jí)的頁目錄項(xiàng)中存放的均是其下一級(jí)頁目錄表的物理內(nèi)存地址。

例如內(nèi)核主頁表這里的 pgd 存放的是其下一級(jí) —— 上層頁目錄 PUD 的起始物理內(nèi)存地址 ,PUD 中的頁目錄項(xiàng) pud 又存放的是其下一級(jí) —— 中間頁目錄 PMD 的起始物理內(nèi)存地址,依次類推,中間頁目錄項(xiàng) pmd 存放的又是頁表的起始物理內(nèi)存地址。

既然每一級(jí)頁目錄表中的頁目錄項(xiàng)存放的都是其下一級(jí)頁目錄表的起始物理內(nèi)存地址,那么頁目錄項(xiàng)中存放的就相當(dāng)于是下一級(jí)頁目錄表的引用,這樣一來我們就只需要同步最頂級(jí)的頁目錄項(xiàng) pgd 就可以了,后面只要與該 pgd 相關(guān)的頁目錄表以及頁表發(fā)生任何變化,由于是引用的關(guān)系,這些改變都會(huì)立刻自動(dòng)反應(yīng)到進(jìn)程頁表的內(nèi)核部分中,后面就不需要同步了。

/*
 * 64-bit:
 *
 *   Handle a fault on the vmalloc area
 */
static noinline int vmalloc_fault(unsigned long address)
{
    // 分別是缺頁虛擬地址 address 對(duì)應(yīng)在內(nèi)核主頁表的全局頁目錄項(xiàng) pgd_k ,以及進(jìn)程頁表中對(duì)應(yīng)的全局頁目錄項(xiàng) pgd
    pgd_t *pgd, *pgd_k;
    // p4d_t 用于五級(jí)頁表體系,當(dāng)前 cpu 架構(gòu)體系下一般采用的是四級(jí)頁表
    // 在四級(jí)頁表下 p4d 是空的,pgd 的值會(huì)賦值給 p4d
    p4d_t *p4d, *p4d_k;
    // 缺頁虛擬地址 address 對(duì)應(yīng)在進(jìn)程頁表中的上層目錄項(xiàng) pud
    pud_t *pud;
    // 缺頁虛擬地址 address 對(duì)應(yīng)在進(jìn)程頁表中的中間目錄項(xiàng) pmd
    pmd_t *pmd;
    // 缺頁虛擬地址 address 對(duì)應(yīng)在進(jìn)程頁表中的頁表項(xiàng) pte
    pte_t *pte;

    // 確保缺頁發(fā)生在內(nèi)核 vmalloc 動(dòng)態(tài)映射區(qū)
    if (!(address >= VMALLOC_START && address < VMALLOC_END))
        return -1;

    // 獲取缺頁虛擬地址 address 對(duì)應(yīng)在進(jìn)程頁表的全局頁目錄項(xiàng) pgd
    pgd = (pgd_t *)__va(read_cr3_pa()) + pgd_index(address);
    // 獲取缺頁虛擬地址 address 對(duì)應(yīng)在內(nèi)核主頁表的全局頁目錄項(xiàng) pgd_k
    pgd_k = pgd_offset_k(address);

    // 如果內(nèi)核主頁表中的 pgd_k 本來就是空的,說明 address 是一個(gè)非法訪問的地址,返回 -1 
    if (pgd_none(*pgd_k))
        return -1;

    // 如果開啟了五級(jí)頁表,那么頂級(jí)頁表就是 pgd,這里只需要同步頂級(jí)頁表項(xiàng)就可以了
    if (pgtable_l5_enabled()) {
        // 內(nèi)核主頁表中的 pgd_k 不為空,進(jìn)程頁表中的 pgd 為空,那么就同步頁表
        if (pgd_none(* )) {
            // 將主內(nèi)核頁表中的 pgd_k 內(nèi)容復(fù)制給進(jìn)程頁表對(duì)應(yīng)的 pgd
            set_pgd(pgd, *pgd_k);
            // 刷新 mmu
            arch_flush_lazy_mmu_mode();
        } else {
            BUG_ON(pgd_page_vaddr(*pgd) != pgd_page_vaddr(*pgd_k));
        }
    }

    // 四級(jí)頁表體系下,p4d 是頂級(jí)頁表項(xiàng),同樣也是只需要同步頂級(jí)頁表項(xiàng)即可,同步邏輯和五級(jí)頁表一模一樣
    // 因?yàn)槭撬募?jí)頁表,所以這里會(huì)將 pgd 賦值給 p4d,p4d_k ,后面就直接把 p4d 看做是頂級(jí)頁表了。
    p4d = p4d_offset(pgd, address);
    p4d_k = p4d_offset(pgd_k, address);
    // 內(nèi)核主頁表為空,則停止同步,返回 -1 ,表示正在訪問一個(gè)非法地址
    if (p4d_none(*p4d_k))
        return -1;
    // 內(nèi)核主頁表不為空,進(jìn)程頁表為空,則同步內(nèi)核頂級(jí)頁表項(xiàng) p4d_k 到進(jìn)程頁表對(duì)應(yīng)的 p4d 中,然后刷新 mmu
    if (p4d_none(*p4d) && !pgtable_l5_enabled()) {
        set_p4d(p4d, *p4d_k);
        arch_flush_lazy_mmu_mode();
    } else {
        BUG_ON(p4d_pfn(*p4d) != p4d_pfn(*p4d_k));
    }

    // 到這里,頁表的同步工作就完成了,下面代碼用于檢查內(nèi)核地址 address 在進(jìn)程頁表內(nèi)核部分中是否有物理內(nèi)存進(jìn)行映射
    // 如果沒有,則返回 -1 ,說明進(jìn)程在訪問一個(gè)非法的內(nèi)核地址,進(jìn)程隨后會(huì)被 kill 掉
    // 返回 0 表示表示地址 address 背后是有物理內(nèi)存映射的, vmalloc 動(dòng)態(tài)映射區(qū)的缺頁處理到此結(jié)束。

    // 根據(jù)頂級(jí)頁目錄項(xiàng) p4d 獲取 address 在進(jìn)程頁表中對(duì)應(yīng)的上層頁目錄項(xiàng) pud
    pud = pud_offset(p4d, address);
    if (pud_none(*pud))
        return -1;
    // 該 pud 指向的是 1G 大頁內(nèi)存
    if (pud_large(*pud))
        return 0;
     // 根據(jù) pud 獲取 address 在進(jìn)程頁表中對(duì)應(yīng)的中間頁目錄項(xiàng) pmd
    pmd = pmd_offset(pud, address);
    if (pmd_none(*pmd))
        return -1;
    // 該 pmd 指向的是 2M 大頁內(nèi)存
    if (pmd_large(*pmd))
        return 0;
    // 根據(jù) pmd 獲取 address 對(duì)應(yīng)的頁表項(xiàng) pte
    pte = pte_offset_kernel(pmd, address);
    // 頁表項(xiàng) pte 并沒有映射物理內(nèi)存
    if (!pte_present(*pte))
        return -1;

    return 0;
}
NOKPROBE_SYMBOL(vmalloc_fault);

在我們聊完內(nèi)核主頁表的同步過程之后,可能很多讀者朋友不禁要問,既然已經(jīng)有了內(nèi)核主頁表,而且內(nèi)核地址空間包括內(nèi)核頁表又是所有進(jìn)程共享的,那進(jìn)程為什么不能直接訪問內(nèi)核主頁表而是要訪問主頁表的拷貝部分呢 ? 這樣還能省去拷貝內(nèi)核主頁表(fork 時(shí)候)以及同步內(nèi)核主頁表(缺頁時(shí)候)這些個(gè)開銷。

之所以這樣設(shè)計(jì)一方面有硬件限制的原因,畢竟每個(gè) CPU 核心只會(huì)有一個(gè) CR3 寄存器來存放進(jìn)程頁表的頂級(jí)頁目錄起始物理內(nèi)存地址,沒辦法同時(shí)存放進(jìn)程頁表和內(nèi)核主頁表。

另一方面的原因則是操作頁表都是需要對(duì)其進(jìn)行加鎖的,無論是操作進(jìn)程頁表還是內(nèi)核主頁表。而且在操作頁表的過程中可能會(huì)涉及到物理內(nèi)存的分配,這也會(huì)引起進(jìn)程的阻塞。

而進(jìn)程本身可能處于中斷上下文以及競(jìng)態(tài)區(qū)中,不能加鎖,也不能被阻塞,如果直接對(duì)內(nèi)核主頁表加鎖的話,那么系統(tǒng)中的其他進(jìn)程就只能阻塞等待了。所以只能而且必須是操作主內(nèi)核頁表的拷貝,不能直接操作內(nèi)核主頁表。

好了,該向大家交代的現(xiàn)在都已經(jīng)交代完了,我們閑話不多說,繼續(xù)本文的主題內(nèi)容~~~

4. 用戶態(tài)缺頁異常處理 —— do_user_addr_fault

進(jìn)程用戶態(tài)虛擬地址空間的布局我們現(xiàn)在已經(jīng)非常熟悉了,在處理用戶態(tài)缺頁異常之前,內(nèi)核需要在進(jìn)程用戶空間眾多的虛擬內(nèi)存區(qū)域 vma 之中找到引起缺頁的內(nèi)存地址 address 究竟是屬于哪一個(gè) vma 。如果沒有一個(gè) vma 能夠包含 address , 那么就說明該 address 是一個(gè)還未被分配的虛擬內(nèi)存地址,進(jìn)程對(duì)該地址的訪問是非法的,自然也就不用處理缺頁了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

所以內(nèi)核就需要根據(jù)缺頁地址 address 通過 find_vma 函數(shù)在進(jìn)程地址空間中找出符合 address < vma->vm_end 條件的第一個(gè) vma 出來,也就是挨著 address 最近的一個(gè) vma。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

而缺頁地址 address 可以出現(xiàn)在進(jìn)程地址空間中的任意位置,根據(jù) address 的分布會(huì)有下面三種情況:

第一種情況就是 address 的后面沒有一個(gè) vma 出現(xiàn),也就是說進(jìn)程地址空間中沒有一個(gè) vma 符合條件:address < vma->vm_end。進(jìn)程訪問的是一個(gè)還未分配的虛擬內(nèi)存地址,屬于非法地址訪問,不需要處理缺頁。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

第二種情況就是 address 恰巧包含在一個(gè) vma 中,這個(gè)自然是正常情況,內(nèi)核開始處理該 vma 區(qū)域的缺頁異常。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

第三種情況是 address 不巧落在了 find_vma 的前面,也就是 address < find_vma->vm_start。這種情況自然也是非法地址訪問,不需要處理缺頁。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

但是這里有一種特殊情況就是萬一這個(gè) find_vma 是棧區(qū)怎么辦呢 ? 棧是允許擴(kuò)展的但不允許收縮,如果壓棧指令 push 引用了一個(gè)棧區(qū)之外的地址 address,這種異常不是由程序錯(cuò)誤所引起的,因此缺頁處理程序需要單獨(dú)處理?xiàng)^(qū)的擴(kuò)展。

如果 find_vma 中的 vm_flags 標(biāo)記了 VM_GROWSDOWN,表示該 vma 中的地址增長方向是由高到底了,說明這個(gè) vma 可能是棧區(qū)域,近而需要到 expand_stack 函數(shù)中判斷是否允許擴(kuò)展棧,如果允許的話,就將棧所屬的 vma 起始地址 vm_start 擴(kuò)展至 address 處。

現(xiàn)在我們已經(jīng)校驗(yàn)完了 vma,并確定了缺頁地址 address 是一個(gè)合法的地址,下面就可以放心地調(diào)用 handle_mm_fault 函數(shù)對(duì)這塊 vma 進(jìn)行缺頁處理了。

/* Handle faults in the user portion of the address space */
static inline
void do_user_addr_fault(struct pt_regs *regs,
            unsigned long hw_error_code,
            unsigned long address)
{
    struct vm_area_struct *vma;
    struct task_struct *tsk;
    struct mm_struct *mm;
 
    tsk = current;
    mm = tsk->mm;

       .............. 省略 ..............

    // 在進(jìn)程虛擬地址空間查找第一個(gè)符合條件:address < vma->vm_end 的虛擬內(nèi)存區(qū)域 vma
    vma = find_vma(mm, address);
    // 如果該缺頁地址 address 后面沒有 vma 跳轉(zhuǎn)到 bad_area 處理異常
    if (unlikely(!vma)) {
        bad_area(regs, hw_error_code, address);
        return;
    }
    // 缺頁地址 address 恰好落在一個(gè) vma 中,跳轉(zhuǎn)到 good_area 處理 vma 中的缺頁
    if (likely(vma->vm_start <= address))
        goto good_area;
    // 上面第三種情況,vma 不是棧區(qū),跳轉(zhuǎn)到 bad_area
    if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
        bad_area(regs, hw_error_code, address);
        return;
    }
    // vma 是棧區(qū),嘗試擴(kuò)展棧區(qū)到 address 地址處
    if (unlikely(expand_stack(vma, address))) {
        bad_area(regs, hw_error_code, address);
        return;
    }

    /*
     * Ok, we have a good vm_area for this memory access, so
     * we can handle it..
     */
good_area:
    // 處理 vma 區(qū)域的缺頁異常,返回值 fault 是一個(gè)位圖,用于描述缺頁處理過程中發(fā)生的狀況信息。
    fault = handle_mm_fault(vma, address, flags);
    // 本次缺頁是否屬于 VM_FAULT_MAJOR,缺頁處理過程中是否發(fā)生了物理內(nèi)存的分配以及磁盤 IO
    // 與其對(duì)應(yīng)的是 VM_FAULT_MINOR 表示缺頁處理過程中所需內(nèi)存頁已經(jīng)存在于內(nèi)存中了,只是修改頁表即可。
    major |= fault & VM_FAULT_MAJOR;

    /*
     * Major/minor page fault accounting. If any of the events
     * returned VM_FAULT_MAJOR, we account it as a major fault.
     */
    if (major) {
        // 統(tǒng)計(jì)進(jìn)程總共發(fā)生的 VM_FAULT_MAJOR 次數(shù)
        tsk->maj_flt++;
        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, address);
    } else {
        // 統(tǒng)計(jì)進(jìn)程總共發(fā)生的 VM_FAULT_MINOR 次數(shù)
        tsk->min_flt++;
        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, address);
    }

}
NOKPROBE_SYMBOL(do_user_addr_fault);

handle_mm_fault 函數(shù)會(huì)返回一個(gè) unsigned int 類型的位圖 vm_fault_t,通過這個(gè)位圖可以簡要描述一下在整個(gè)缺頁異常處理的過程中究竟發(fā)生了哪些狀況,方便內(nèi)核對(duì)各種狀況進(jìn)行針對(duì)性處理。

/**
 * Page fault handlers return a bitmask of %VM_FAULT values.
 */
typedef __bitwise unsigned int vm_fault_t;

比如,位圖 vm_fault_t 的第三個(gè)比特位置為 1 表示 VM_FAULT_MAJOR,置為 0 表示 VM_FAULT_MINOR。

enum vm_fault_reason {
	VM_FAULT_MAJOR          = (__force vm_fault_t)0x000004,
};

VM_FAULT_MAJOR 的意思是本次缺頁所需要的物理內(nèi)存頁還不在內(nèi)存中,需要重新分配以及需要啟動(dòng)磁盤 IO,從磁盤中 swap in 進(jìn)來。

VM_FAULT_MINOR 的意思是本次缺頁所需要的物理內(nèi)存頁已經(jīng)加載進(jìn)內(nèi)存中了,缺頁處理只需要修改頁表重新映射一下就可以了。

我們來看一個(gè)具體的例子,筆者在之前的文章?《從內(nèi)核世界透視 mmap 內(nèi)存映射的本質(zhì)(原理篇)》中為大家介紹多個(gè)進(jìn)程調(diào)用 mmap 對(duì)磁盤上的同一個(gè)文件進(jìn)行共享文件映射的時(shí)候,此時(shí)在各個(gè)進(jìn)程的地址空間中都只是各自分配了一段虛擬內(nèi)存用于共享文件映射而已,還沒有分配物理內(nèi)存頁。

當(dāng)?shù)谝粋€(gè)進(jìn)程開始訪問這段虛擬內(nèi)存映射區(qū)時(shí),由于沒有物理內(nèi)存頁,頁表還是空的,于是產(chǎn)生缺頁中斷,內(nèi)核則會(huì)在伙伴系統(tǒng)中分配一個(gè)物理內(nèi)存頁,然后將新分配的內(nèi)存頁加入到 page cache 中。

然后調(diào)用 readpage 激活塊設(shè)備驅(qū)動(dòng)從磁盤中讀取映射的文件內(nèi)容,用讀取到的內(nèi)容填充新分配的內(nèi)存頁,最后在進(jìn)程 1 頁表中建立共享映射的這段虛擬內(nèi)存與 page cache 中緩存的文件頁之間的關(guān)聯(lián)。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

由于進(jìn)程 1 的缺頁處理發(fā)生了物理內(nèi)存的分配以及磁盤 IO ,所以本次缺頁處理屬于 VM_FAULT_MAJOR。

當(dāng)進(jìn)程 2 訪問其地址空間中映射的這段虛擬內(nèi)存時(shí),由于頁表是空的,也會(huì)發(fā)生缺頁,但是當(dāng)進(jìn)程 2 進(jìn)入內(nèi)核中發(fā)現(xiàn)所映射的文件頁已經(jīng)被進(jìn)程 1 加載進(jìn) page cache 中了,進(jìn)程 2 的缺頁處理只需要將這個(gè)文件頁映射進(jìn)自己的頁表就可以了,不需要重新分配內(nèi)存以及發(fā)生磁盤 IO 。這種情況就屬于 VM_FAULT_MINOR。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

最后需要將進(jìn)程總共發(fā)生的 VM_FAULT_MAJOR 次數(shù)以及 VM_FAULT_MINOR 次數(shù)統(tǒng)計(jì)到進(jìn)程 task_struct 結(jié)構(gòu)中的相應(yīng)字段中:

struct task_struct {
    // 進(jìn)程總共發(fā)生的 VM_FAULT_MINOR 次數(shù)
    unsigned long           min_flt;
     // 進(jìn)程總共發(fā)生的 VM_FAULT_MAJOR 次數(shù)
    unsigned long           maj_flt;
}

我們可以在 ps 命令上增加 -o 選項(xiàng),添加 maj_flt ,min_flt 數(shù)據(jù)列來查看各個(gè)進(jìn)程的 VM_FAULT_MAJOR 次數(shù)和 VM_FAULT_MINOR 次數(shù)。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

5. handle_mm_fault 完善進(jìn)程頁表體系

饒了一大圈,現(xiàn)在我們終于來到了缺頁處理的核心邏輯,之前筆者提到,引起缺頁中斷的原因大概有三種:

  • 第一種是 CPU 訪問的虛擬內(nèi)存地址 address 之前完全沒有被映射過,其在頁表中對(duì)應(yīng)的各級(jí)頁目錄項(xiàng)以及頁表項(xiàng)都還是空的。

  • 第二種是 address 之前被映射過,但是映射的這塊物理內(nèi)存被內(nèi)核 swap out 到磁盤上了。

  • 第三種是 address 背后映射的物理內(nèi)存還在,只是由于訪問權(quán)限不夠引起的缺頁中斷,比如,后面要為大家介紹的寫時(shí)復(fù)制(COW)機(jī)制就屬于這一種。

下面筆者一種接一種的帶大家一起梳理,我們先來看第一種情況:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

由于現(xiàn)在正在被訪問的虛擬內(nèi)存地址 address 之前從來沒有被映射過,所以該虛擬內(nèi)存地址在進(jìn)程頁表中的各級(jí)頁目錄表中的目錄項(xiàng)以及頁表中的頁表項(xiàng)都是空的。內(nèi)核的首要任務(wù)就是先要將這些缺失的頁目錄項(xiàng)和頁表項(xiàng)一一補(bǔ)齊。

筆者在之前的文章《一步一圖帶你構(gòu)建 Linux 頁表體系》?中曾為大家介紹過,在當(dāng)前 64 位體系架構(gòu)下,其實(shí)只使用了 48 位來描述進(jìn)程的虛擬內(nèi)存空間,其中用戶態(tài)地址空間 128T,內(nèi)核態(tài)地址空間 128T,所以我們只需要使用 48 位的虛擬內(nèi)存地址就可以表示進(jìn)程虛擬內(nèi)存空間中的任意地址了。

而這 48 位的虛擬內(nèi)存地址內(nèi)又分為五個(gè)部分,它們分別是虛擬內(nèi)存地址在全局頁目錄表 PGD 中對(duì)應(yīng)的頁目錄項(xiàng) pgd_t 的偏移,在上層頁目錄表 PUD 中對(duì)應(yīng)的頁目錄項(xiàng) pud_t 的偏移,在中間頁目錄表 PMD 中對(duì)應(yīng)的頁目錄項(xiàng) pmd_t 的偏移,在頁表中對(duì)應(yīng)的頁表項(xiàng) pte_t 的偏移,以及在其背后映射的物理內(nèi)存頁中的偏移。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

內(nèi)核中使用 unsigned long 類型來表示各級(jí)頁目錄中的目錄項(xiàng)以及頁表中的頁表項(xiàng),在 64 位系統(tǒng)中它們都是占用 8 字節(jié)。

// 定義在內(nèi)核文件:/arch/x86/include/asm/pgtable_64_types.h
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long pgdval_t;

typedef struct { pteval_t pte; } pte_t;

// 定義在內(nèi)核文件:/arch/x86/include/asm/pgtable_types.h
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;

而各級(jí)頁目錄表以及頁表在內(nèi)核中其實(shí)本質(zhì)上都是一個(gè) 4K 物理內(nèi)存頁,只不過這些物理內(nèi)存頁存放的內(nèi)容比較特殊,它們存放的是頁目錄項(xiàng)和頁表項(xiàng)。一張頁目錄表可以存放 512 個(gè)頁目錄項(xiàng),一張頁表可以存放 512 個(gè)頁表項(xiàng)

// 全局頁目錄表 PGD 可以容納的頁目錄項(xiàng) pgd_t 的個(gè)數(shù)
#define PTRS_PER_PGD  512
// 上層頁目錄表 PUD 可以容納的頁目錄項(xiàng) pud_t 的個(gè)數(shù)
#define PTRS_PER_PUD  512
// 中間頁目錄表 PMD 可以容納的頁目錄項(xiàng) pmd_t 的個(gè)數(shù)
#define PTRS_PER_PMD  512
// 頁表可以容納的頁表項(xiàng) pte_t 的個(gè)數(shù)
#define PTRS_PER_PTE  512

因此我們可以把全局頁目錄表 PGD 看做是一個(gè)能夠存放 512 個(gè) pgd_t 的數(shù)組 —— pgd_t[PTRS_PER_PGD],虛擬內(nèi)存地址對(duì)應(yīng)在 pgd_t[PTRS_PER_PGD] 數(shù)組中的索引使用 9 個(gè)比特位就可以表示了。

在內(nèi)核中使用 pgd_offset 函數(shù)來定位虛擬內(nèi)存地址在全局頁目錄表 PGD 中對(duì)應(yīng)的頁目錄項(xiàng) pgd_t,這個(gè)過程和訪問數(shù)組一模一樣,事實(shí)上整個(gè) PGD 就是一個(gè) pgd_t[PTRS_PER_PGD] 數(shù)組。

首先我們通過 mm_struct-> pgd 獲取 pgd_t[PTRS_PER_PGD] 數(shù)組的首地址(全局頁目錄表 PGD 的起始內(nèi)存地址),然后將虛擬內(nèi)存地址右移 PGDIR_SHIFT(39)位再用掩碼 PTRS_PER_PGD - 1 將高位全部掩去,只保留低 9 位得到虛擬內(nèi)存地址在 pgd_t[PTRS_PER_PGD] 數(shù)組中的索引偏移 pgd_index。

然后將 mm_struct-> pgd 與 pgd_index 相加就可以定位到虛擬內(nèi)存地址在全局頁目錄表 PGD 中的頁目錄項(xiàng) pgd_t 了。

/*
 * a shortcut to get a pgd_t in a given mm
 */
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))

#define pgd_offset_pgd(pgd, address) (pgd + pgd_index((address)))

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

#define PGDIR_SHIFT		39
#define PTRS_PER_PGD		512

在后續(xù)即將要介紹的源碼實(shí)現(xiàn)中,大家還會(huì)看到一個(gè) p4d 的頁目錄,該頁目錄用于在五級(jí)頁表體系下表示四級(jí)頁目錄。

typedef unsigned long	p4dval_t;
typedef struct { p4dval_t p4d; } p4d_t;

而在四級(jí)頁表體系下,這個(gè) p4d 就不起作用了,但為了代碼上的統(tǒng)一處理,在四級(jí)頁表下,前面定位到的頂級(jí)頁目錄項(xiàng) pgd_t 會(huì)賦值給四級(jí)頁目錄項(xiàng) p4d_t,后續(xù)處理都會(huì)將 p4d_t 看做是頂級(jí)頁目錄項(xiàng),這一點(diǎn)需要和大家在這里先提前交代清楚。

static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long address)
{
    if (!pgtable_l5_enabled())
        // 四級(jí)頁表體系下,p4d_t 其實(shí)就是頂級(jí)頁目錄項(xiàng)
        return (p4d_t *)pgd;
    return (p4d_t *)pgd_page_vaddr(*pgd) + p4d_index(address);
}

現(xiàn)在我們已經(jīng)通過 pgd_offset 定位到虛擬內(nèi)存地址 address 對(duì)應(yīng)在全局頁目錄 PGD 的頁目錄項(xiàng) pgd_t(p4d_t)了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

接下來的任務(wù)就是根據(jù)這個(gè) p4d_t 定位虛擬內(nèi)存對(duì)應(yīng)在上層頁目錄 PUD 中的頁目錄項(xiàng) pud_t。但在定位之前,我們需要首先判斷這個(gè) p4d_t 是否是空的,如果是空的,說明在目前的進(jìn)程頁表中還不存在對(duì)應(yīng)的 PUD,需要馬上創(chuàng)建一個(gè)新的出來。

而 PUD 的相關(guān)信息全部都保存在 p4d_t 里,我們可以通過 native_p4d_val 函數(shù)將頂級(jí)頁目錄項(xiàng) p4d_t 中的值獲取出來。

static inline p4dval_t native_p4d_val(p4d_t p4d)
{
	return p4d.p4d;
}

在 64 位系統(tǒng)中,各級(jí)頁目錄項(xiàng)都是用 unsigned long 類型來表示的,共 8 個(gè)字節(jié),64 個(gè) bit,還記得我們之前在《一步一圖帶你構(gòu)建 Linux 頁表體系》 一文中介紹的頁目錄項(xiàng)比特位布局嗎 ?

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在頁目錄項(xiàng)剛剛被創(chuàng)建出來的時(shí)候,內(nèi)核會(huì)將他們?nèi)砍跏蓟癁?0 值,如果一個(gè)頁目錄項(xiàng)中除了第 5 , 6 比特位之外剩下的比特位全都為 0 的話,則表示這個(gè)頁目錄項(xiàng)是空的。

static inline int p4d_none(p4d_t p4d)
{
    // p4d_t 中除了第 5,6 比特位之外,剩余比特位如果全是 0 則表示 p4d_t 是空的
    return (native_p4d_val(p4d) & ~(_PAGE_KNL_ERRATUM_MASK)) == 0;
}
// 頁目錄項(xiàng)中第 5, 6 比特位置為 1
#define _PAGE_KNL_ERRATUM_MASK (_PAGE_DIRTY | _PAGE_ACCESSED)

如果我們通過 p4d_none 函數(shù)判斷出頂級(jí)頁目錄項(xiàng) p4d 是空的,那么就需要調(diào)用 __pud_alloc 函數(shù)分配一個(gè)新的上層頁目錄表 PUD 出來,然后用 PUD 的起始物理內(nèi)存地址以及頁目錄項(xiàng)的初始權(quán)限位 _PAGE_TABLE 填充 p4d。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

/*
 * Allocate page upper directory.
 * We've already handled the fast-path in-line.
 */
int __pud_alloc(struct mm_struct *mm, p4d_t *p4d, unsigned long address)
{
    // 調(diào)用 get_zeroed_page 申請(qǐng)一個(gè) 4k 物理內(nèi)存頁并初始化為 0 值作為新的 PUD
    // new 指向新分配的 PUD 起始內(nèi)存地址
    pud_t *new = pud_alloc_one(mm, address);
    if (!new)
        return -ENOMEM;
    // 操作進(jìn)程頁表需要加鎖
    spin_lock(&mm->page_table_lock);
    // 如果頂級(jí)頁目錄項(xiàng) p4d 中的 P 比特位置為 0 表示 p4d 目前還沒有指向其下一級(jí)頁目錄 PUD
    // 下面需要填充 p4d
    if (!p4d_present(*p4d)) {
        // 更新 mm->pgtables_bytes 計(jì)數(shù),該字段用于統(tǒng)計(jì)進(jìn)程頁表所占用的字節(jié)數(shù)
        // 由于這里新增了一張 PUD 目錄表,所以計(jì)數(shù)需要增加 PTRS_PER_PUD * sizeof(pud_t)
        mm_inc_nr_puds(mm);
        // 將 new 指向的新分配出來的 PUD 物理內(nèi)存地址以及相關(guān)屬性填充到頂級(jí)頁目錄項(xiàng) p4d 中
        p4d_populate(mm, p4d, new);
    } else  /* Another has populated it */
        // 釋放新創(chuàng)建的 PMD
        pud_free(mm, new);

    // 釋放頁表鎖
    spin_unlock(&mm->page_table_lock);
    return 0;
}

下面我們來看一下填充頂級(jí)頁目錄項(xiàng) p4d 的一些細(xì)節(jié),填充的邏輯封裝在下面的 p4d_populate 函數(shù)中。

static inline void p4d_populate(struct mm_struct *mm, p4d_t *p4d, pud_t *pud)
{
	set_p4d(p4d, __p4d(_PAGE_TABLE | __pa(pud)));
}

#define _KERNPG_TABLE	(_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED |	\
			 _PAGE_DIRTY | _PAGE_ENC)
#define _PAGE_TABLE	(_KERNPG_TABLE | _PAGE_USER)

各級(jí)頁目錄項(xiàng)以及頁表項(xiàng),它們的本質(zhì)其實(shí)就是一塊 8 字節(jié)大小,64 bits 的小內(nèi)存塊,內(nèi)核中使用 unsigned long 類型來修飾,各級(jí)頁目錄項(xiàng)以及頁表項(xiàng)在初始的時(shí)候,它們的這 64 個(gè)比特位全部為 0 值,所謂填充頁目錄項(xiàng)就是按照下圖所示的頁目錄項(xiàng)比特位布局,根據(jù)每個(gè)比特位的具體含義進(jìn)行相應(yīng)的填充。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

由于頁目錄項(xiàng)所承擔(dān)的一項(xiàng)最重要的工作就是定位其下一級(jí)頁目錄表的起始物理內(nèi)存地址,這里的下一級(jí)頁目錄表就是剛剛我們新創(chuàng)建出來的 PUD。所以第一件重要的事情就是通過 __pa(pud) 來獲取 PUD 的起始物理內(nèi)存地址,然后將 PUD 的物理內(nèi)存地址填充到頂級(jí)頁目錄項(xiàng) p4d 中的對(duì)應(yīng)比特位上。

由于物理內(nèi)存地址在內(nèi)核中都是按照 4K 對(duì)齊的,所以 PUD 物理內(nèi)存地址的低 12 位全部都是 0 ,我們可以利用這 12 個(gè)比特位存放一些權(quán)限標(biāo)記位,頁目錄項(xiàng)在初始化時(shí)需要置為 1 的權(quán)限標(biāo)記位定義在 _PAGE_TABLE 中。也就是說 _PAGE_TABLE 定義了頁目錄項(xiàng)初始權(quán)限標(biāo)記位集合。

#define _PAGE_BIT_PRESENT 0 /* is present */
#define _PAGE_BIT_RW  1 /* writeable */
#define _PAGE_BIT_USER  2 /* userspace addressable */
#define _PAGE_BIT_ACCESSED 5 /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY  6 /* was written to (raised by CPU) */


#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_RW (_AT(pteval_t, 1) << _PAGE_BIT_RW)
#define _PAGE_USER (_AT(pteval_t, 1) << _PAGE_BIT_USER)
#define _PAGE_ACCESSED (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED)
#define _PAGE_DIRTY (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY)

我們通過 _PAGE_TABLE 和 __pa(pud) 進(jìn)行或運(yùn)算 —— _PAGE_TABLE | __pa(pud),這樣就可以按照上圖中的比特位布局構(gòu)造出一個(gè) 8 字節(jié)的 unsigned long 類型的整數(shù)了,這個(gè)整數(shù)的第 12 到 35 比特位通過 __pa(pud) 填充進(jìn)來,低 12 位比特通過 _PAGE_TABLE 填充進(jìn)來。

隨后我們通過 __p4d 將這個(gè)剛剛構(gòu)造出來的 unsigned long 整數(shù)轉(zhuǎn)換成 p4d_t 類型。

#define __p4d(x)	native_make_p4d(x)

static inline p4d_t native_make_p4d(pudval_t val)
{
	return (p4d_t) { val };
}

最后我們通過 set_p4d 將我們剛剛構(gòu)造出來的 p4d_t 賦值給原始的 p4d_t。

# define set_p4d(p4dp, p4d)		native_set_p4d(p4dp, p4d)

這樣一來,缺頁的虛擬內(nèi)存地址對(duì)應(yīng)在頂級(jí)頁目錄表中的頁目錄項(xiàng) p4d_t 就被填充好了,現(xiàn)在它已經(jīng)指向了剛剛新創(chuàng)建出來的 PUD,并且擁有了初始的權(quán)限位。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

目前為止,我們只是完善了缺頁虛擬內(nèi)存地址對(duì)應(yīng)在進(jìn)程頁表頂級(jí)頁目錄中的目錄項(xiàng) p4d_t,在四級(jí)頁表體系下,我們還需要繼續(xù)向下逐級(jí)的去補(bǔ)齊虛擬內(nèi)存地址對(duì)應(yīng)在其他頁目錄中的目錄項(xiàng),處理邏輯上都是一模一樣的。

頂級(jí)頁目錄項(xiàng) p4d 中包含了其下一級(jí)頁目錄 PUD 的相關(guān)信息,在內(nèi)核中使用 pud_offset 函數(shù)來定位虛擬內(nèi)存地址 address 對(duì)應(yīng)在 PUD 中的頁目錄項(xiàng) pud_t。

/* Find an entry in the third-level page table.. */
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address)
{
	return (pud_t *)p4d_page_vaddr(*p4d) + pud_index(address);
}

和頂級(jí)頁目錄 PGD 一樣,上層頁目錄 PUD 也可以看做是一個(gè)能夠存放 512 個(gè) pud_t 的數(shù)組 —— pud_t[PTRS_PER_PUD] 。

// 上層頁目錄表 PUD 可以容納的頁目錄項(xiàng) pud_t 的個(gè)數(shù)
#define PTRS_PER_PUD  512

內(nèi)核通過 pud_index 函數(shù)將虛擬內(nèi)存地址右移 PUD_SHIFT(30)位然后用掩碼 PTRS_PER_PUD - 1 將高位全部掩掉,只保留低 9 位得到虛擬內(nèi)存地址在上層頁目錄 PUD 中對(duì)應(yīng)的頁目錄項(xiàng) pud_t 的偏移 —— pud_index。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

static inline unsigned long pud_index(unsigned long address)
{
	return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

#define PUD_SHIFT	30

現(xiàn)在我們有了 pud_index,如果我們還能夠知道上層頁目錄表 PUD 的虛擬內(nèi)存地址,兩者一相加就能得到頁目錄項(xiàng) pud_t 了。而 PUD 的物理內(nèi)存地址恰好保存在剛剛填充好的頂級(jí)頁目錄項(xiàng) p4d 中,我們可以從 p4d 中將 PUD 的物理內(nèi)存地址提取出來,然后通過 __va 轉(zhuǎn)換成虛擬內(nèi)存地址不就行了么。

static inline unsigned long p4d_page_vaddr(p4d_t p4d)
{
	return (unsigned long)__va(p4d_val(p4d) & p4d_pfn_mask(p4d));
}

首先我們通過 p4d_val 將頂級(jí)頁目錄項(xiàng) p4d 的值(8 字節(jié),64 比特)提取出來。

#define p4d_val(x)	native_p4d_val(x)

static inline p4dval_t native_p4d_val(p4d_t p4d)
{
	return p4d.p4d;
}

然后再根據(jù)頁目錄項(xiàng)中的比特位布局,將其下一級(jí)頁目錄表的物理內(nèi)存地址截取出來。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

那么如何截取呢 ? 上圖中展示的頁目錄項(xiàng)比特位布局筆者是按照 36 位物理內(nèi)存地址所畫,事實(shí)上 Linux 內(nèi)核最大可支持 52 位的物理內(nèi)存地址。

#define __PHYSICAL_MASK_SHIFT	52

我們將 1 左移 __PHYSICAL_MASK_SHIFT 位然后再減 1 得到 __PHYSICAL_MASK(低 52 位全部為 1)。

#define __PHYSICAL_MASK		((phys_addr_t)((1ULL << __PHYSICAL_MASK_SHIFT) - 1))

然后拿 p4d_val & __PHYSICAL_MASK 就可以將 p4d_val 的高位截取掉,只保留低 52 位。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

這低 52 位中包含了兩個(gè)部分,一個(gè)是我們想要提取的下一級(jí)頁目錄表的物理內(nèi)存地址,另一個(gè)則是低 12 位的權(quán)限標(biāo)記位。

如果我們?cè)倌軌虬堰@低 12 位的權(quán)限標(biāo)記位用掩碼掩掉,就可以得到下一級(jí)頁目錄表的物理內(nèi)存地址了。

#define PAGE_SHIFT  12
#define PAGE_SIZE   (_AC(1,UL) << PAGE_SHIFT)      
#define PAGE_MASK   (~(PAGE_SIZE-1))     // 0xFFFFFFFFFFFFF000

上面的 PAGE_MASK 掩碼就是用于將頁目錄項(xiàng) p4d 的低 12 位掩掉的,我們接著在 p4d_val & __PHYSICAL_MASK 的基礎(chǔ)上再與上 PAGE_MASK,就可以將 p4d 中保存的下一級(jí)頁目錄表 PUD 的物理內(nèi)存地址截取出來了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

雖然我們是按照 52 位的物理內(nèi)存地址截取的,但是對(duì)于 36 位的物理內(nèi)存地址來說,頁目錄項(xiàng)中的低 36 位到 51 位之間的比特位都是 0 值,所以也不影響。

static inline unsigned long p4d_page_vaddr(p4d_t p4d)
{
    return (unsigned long)__va(p4d_val(p4d) & p4d_pfn_mask(p4d));
}

static inline p4dval_t p4d_pfn_mask(p4d_t p4d)
{
	/* No 512 GiB huge pages yet */
	return PTE_PFN_MASK;
}

/* Extracts the PFN from a (pte|pmd|pud|pgd)val_t of a 4KB page */
#define PTE_PFN_MASK		((pteval_t)PHYSICAL_PAGE_MASK)

#define PHYSICAL_PAGE_MASK	(((signed long)PAGE_MASK) & __PHYSICAL_MASK)

現(xiàn)在我們已經(jīng)得到 PUD 的物理內(nèi)存地址了,隨后通過 __va 轉(zhuǎn)換成虛擬內(nèi)存地址,然后在加上 pud_index 就得到缺頁虛擬內(nèi)存地址在進(jìn)程頁表上層頁目錄 PUD 中對(duì)應(yīng)的頁目錄項(xiàng) pud_t 了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在得到 pud_t 之后,內(nèi)核還是需要通過 pud_none 來判斷下該上層頁目錄項(xiàng) pud_t 是否是空的,如果是空的話,就需要通過 __pmd_alloc 函數(shù)重新分配一張中間頁目錄表 PMD 出來,然后填充這個(gè)空的 pud_t,這里的邏輯和前面處理 p4d_t 的邏輯一模一樣。

// 同 p4d_none 的邏輯一樣
static inline int pud_none(pud_t pud)
{
	return (native_pud_val(pud) & ~(_PAGE_KNL_ERRATUM_MASK)) == 0;
}

由于這個(gè) PUD 是之前為了填充頂級(jí)頁目錄項(xiàng) p4d_t 而新創(chuàng)建出來的,所以 PUD 這張頁目錄表里還全是 0 值,缺頁虛擬內(nèi)存地址在 PUD 中對(duì)應(yīng)的目錄項(xiàng) pud_t 自然也是 0 值,通過 pud_none 判斷自然是返回 true 。

隨后內(nèi)核會(huì)調(diào)用 __pmd_alloc 函數(shù)新分配一張 4K 大小的物理內(nèi)存頁作為 PMD , 然后用 PMD 的物理內(nèi)存地址去填充這個(gè)空的 pud_t。這里的邏輯和 __pud_alloc 還是一模一樣。

/*
 * Allocate page middle directory.
 * We've already handled the fast-path in-line.
 */
int __pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)
{
    // 調(diào)用 alloc_pages 從伙伴系統(tǒng)申請(qǐng)一個(gè) 4K 大小的物理內(nèi)存頁,作為新的 PMD
    pmd_t *new = pmd_alloc_one(mm, address);
    if (!new)
        return -ENOMEM;
    // 如果 pud 還未指向其下一級(jí)頁目錄 PMD,則需要初始化填充 pud
    if (!pud_present(*pud)) {
        mm_inc_nr_pmds(mm);
        // 將 new 指向的新分配出來的 PMD 物理內(nèi)存地址以及相關(guān)屬性填充到上層頁目錄項(xiàng) pud 中
        pud_populate(mm, pud, new);
    } else  /* Another has populated it */
        pmd_free(mm, new);

    return 0;
}

填充上層頁目錄項(xiàng) pud_t 的邏輯和之前填充頂級(jí)頁目錄項(xiàng) p4d_t 的邏輯也是一樣的。

static inline void pud_populate(struct mm_struct *mm, pud_t *pud, pmd_t *pmd)
{
	set_pud(pud, __pud(_PAGE_TABLE | __pa(pmd)));
}

都是通過 PMD 的物理內(nèi)存地址 __pa(pmd) 以及頁目錄的初始權(quán)限標(biāo)記位集合 _PAGE_TABLE 來構(gòu)造一個(gè) unsigned long 類型的整數(shù)。

通過 __pud 將這個(gè)剛剛構(gòu)造出來的 unsigned long 整數(shù)轉(zhuǎn)換成 pud_t 類型:

#define __pud(x)	native_make_pud(x)

static inline pud_t native_make_pud(pmdval_t val)
{
	return (pud_t) { val };
}

最后將 __pud 的返回值通過 set_pud 賦值給原始的上層頁目錄項(xiàng) pud 。這樣就算完成了 pud 的填充。

# define set_pud(pudp, pud)		native_set_pud(pudp, pud)

static inline void native_set_pud(pud_t *pudp, pud_t pud)
{
	WRITE_ONCE(*pudp, pud);
}

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

中間頁目錄表 PMD 有了,接下來的任務(wù)就該定位缺頁虛擬內(nèi)存地址在進(jìn)程頁表 PMD 中對(duì)應(yīng)的頁目錄項(xiàng) pmd_t 了。

和前面的 PGD ,PUD 一樣, PMD 也可以看做是一個(gè)能夠存放 512 個(gè) pmd_t 的數(shù)組 —— pmd_t[PTRS_PER_PMD] 。

// 中間頁目錄表 PMD 可以容納的頁目錄項(xiàng) pmd_t 的個(gè)數(shù)
#define PTRS_PER_PMD  512

內(nèi)核通過 pmd_offset 函數(shù)來定位虛擬內(nèi)存地址 address 對(duì)應(yīng)在 PMD 中的頁目錄項(xiàng) pmd_t。

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
	return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}

還是之前的套路,首先需要通過 pud_page_vaddr 從上層頁目錄 PUD 中的頁目錄項(xiàng) pud_t 中提取出其下一級(jí)頁目錄表 PMD 的起始虛擬內(nèi)存地址。

static inline unsigned long pud_page_vaddr(pud_t pud)
{
	return (unsigned long)__va(pud_val(pud) & pud_pfn_mask(pud));
}

然后通過 pmd_index 獲取缺頁虛擬內(nèi)存地址在 PMD 中的偏移,和之前的處理方式一樣,首先將缺頁虛擬內(nèi)存地址 address 右移 PMD_SHIFT(21)位,然后和掩碼 PTRS_PER_PMD - 1 相與,只保留低 9 位。

static inline unsigned long pmd_index(unsigned long address)
{
	return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}

#define PMD_SHIFT	21
#define PTRS_PER_PMD	512

最后用剛剛提取出的 PMD 起始虛擬內(nèi)存地址 pud_page_vaddr 與 pmd_index 相加就得到我們尋找的中間頁目錄項(xiàng) pmd_t 了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在我們獲取到 pmd_t 之后,接下來就該處理頁表了,而頁表是直接與物理內(nèi)存頁進(jìn)行映射的,后續(xù)我們需要到頁表項(xiàng)中,根據(jù)權(quán)限位的設(shè)置來解析出具體的缺頁原因,然后進(jìn)行針對(duì)性的缺頁處理,這一部分的內(nèi)容封裝在 handle_pte_fault 函數(shù)中,這是我們下一小節(jié)中要介紹的內(nèi)容。

而本小節(jié)中介紹的 __handle_mm_fault 的主要工作是將進(jìn)程頁表中的三級(jí)頁目錄表 PGD,PUD,PMD 補(bǔ)齊,然后獲取到 pmd_t 就完成了,隨后會(huì)把 pmd_t 送到 handle_pte_fault 函數(shù)中進(jìn)行頁表的處理。

在我們理解了以上內(nèi)容之后,再回頭來看 __handle_mm_fault 源碼實(shí)現(xiàn)就很清晰了:

static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
        unsigned long address, unsigned int flags)
{
    // vm_fault 結(jié)構(gòu)用于封裝后續(xù)缺頁處理用到的相關(guān)參數(shù)
    struct vm_fault vmf = {
        // 發(fā)生缺頁的 vma
        .vma = vma,
        // 引起缺頁的虛擬內(nèi)存地址
        .address = address & PAGE_MASK,
        // 處理缺頁的相關(guān)標(biāo)記 FAULT_FLAG_xxx
        .flags = flags,
        // address 在 vma 中的偏移,單位也頁
        .pgoff = linear_page_index(vma, address),
        // 后續(xù)用于分配物理內(nèi)存使用的相關(guān)掩碼 gfp_mask
        .gfp_mask = __get_fault_gfp_mask(vma),
    };
    // 獲取進(jìn)程虛擬內(nèi)存空間
    struct mm_struct *mm = vma->vm_mm;
    // 進(jìn)程頁表的頂級(jí)頁表地址
    pgd_t *pgd;
    // 五級(jí)頁表下會(huì)使用,在四級(jí)頁表下 p4d 與 pgd 的值一樣
    p4d_t *p4d;
    vm_fault_t ret;
    // 獲取 address 在全局頁目錄表 PGD 中對(duì)應(yīng)的目錄項(xiàng) pgd
    pgd = pgd_offset(mm, address);
    // 在四級(jí)頁表下,這里只是將 pgd 賦值給 p4d,后續(xù)均已 p4d 作為全局頁目錄項(xiàng)
    p4d = p4d_alloc(mm, pgd, address);
    if (!p4d)
        return VM_FAULT_OOM;
    // 首先 p4d_none 判斷全局頁目錄項(xiàng) p4d 是否是空的
    // 如果 p4d 是空的,則調(diào)用 __pud_alloc 分配一個(gè)新的上層頁目錄表 PUD,然后填充 p4d
    // 如果 p4d 不是空的,則調(diào)用 pud_offset 獲取 address 在上層頁目錄 PUD 中的目錄項(xiàng) pud
    vmf.pud = pud_alloc(mm, p4d, address);
    if (!vmf.pud)
        return VM_FAULT_OOM;
  
      ........ 省略 1G 大頁缺頁處理 ..........
    
    // 首先 pud_none 判斷上層頁目錄項(xiàng) pud 是不是空的
    // 如果 pud 是空的,則調(diào)用 __pmd_alloc 分配一個(gè)新的中間頁目錄表 PMD,然后填充 pud
    // 如果 pud 不是空的,則調(diào)用 pmd_offset 獲取 address 在中間頁目錄 PMD 中的目錄項(xiàng) pmd
    vmf.pmd = pmd_alloc(mm, vmf.pud, address);
    if (!vmf.pmd)
        return VM_FAULT_OOM;

      ........ 省略 2M 大頁缺頁處理 ..........

    // 進(jìn)行頁表的相關(guān)處理以及解析具體的缺頁原因,后續(xù)針對(duì)性的進(jìn)行缺頁處理
    return handle_pte_fault(&vmf);
}

6. handle_pte_fault

在上一小節(jié)的開頭,筆者列舉了引起缺頁異常主要的三種原因,要么缺頁的虛擬內(nèi)存地址從來還沒有被映射過,要么是雖然之前映射過,但是物理內(nèi)存頁被 swap 到磁盤上了,要么是因?yàn)樵L問權(quán)限不夠的原因引起的缺頁。

從總體上來講引起缺頁中斷的原因分為兩大類,一類是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁不在內(nèi)存中,另一類是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁在內(nèi)存中。

而每一類下邊又包含若干種缺頁的場(chǎng)景,在本小節(jié)中筆者會(huì)帶著大家一一把這些場(chǎng)景梳理清楚,下面我們來看第一類,其中分為了三種缺頁場(chǎng)景。

第一種場(chǎng)景是,缺頁虛擬內(nèi)存地址 address 在進(jìn)程頁表中間頁目錄對(duì)應(yīng)的頁目錄項(xiàng) pmd_t 是空的,我們可以通過 pmd_none 方法來判斷。

static inline int pmd_none(pmd_t pmd)
{
	unsigned long val = native_pmd_val(pmd);
	return (val & ~_PAGE_KNL_ERRATUM_MASK) == 0;
}

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

這種情況表示缺頁地址 address 對(duì)應(yīng)的 pmd 目前還沒有對(duì)應(yīng)的頁表,連頁表都還沒有,那么自然 pte 也是空的,物理內(nèi)存頁就更不用說了,肯定還沒有。

第二種場(chǎng)景是,缺頁地址 address 對(duì)應(yīng)的 pmd_t 雖然不是空的,頁表也存在,但是 address 對(duì)應(yīng)在頁表中的 pte 是空的。內(nèi)核中通過 pte_offset_map 定位 address 在頁表中的 pte 。這個(gè)過程和前面介紹的定位頁目錄項(xiàng)的過程一模一樣。

#define pte_offset_map(dir, address) pte_offset_kernel((dir), (address))

static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
	return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

static inline unsigned long pte_index(unsigned long address)
{
	return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}

#define PAGE_SHIFT   12
// 頁表可以容納的頁表項(xiàng) pte_t 的個(gè)數(shù)
#define PTRS_PER_PTE  512

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

這種情況下,雖然頁表是存在的,但是奈何 address 在頁表中的 pte 是空的,和第一種場(chǎng)景一樣,都說明了該 address 之前從來還沒有被映射過。

既然之前都沒有被映射,那么現(xiàn)在就該把這塊內(nèi)容補(bǔ)齊,筆者在之前的文章 《從內(nèi)核世界透視 mmap 內(nèi)存映射的本質(zhì)(原理篇)》 中曾為大家介紹了四種內(nèi)存映射方式,它們分別為:私有匿名映射,私有文件映射,共享文件映射,共享匿名映射。這四種內(nèi)存映射方式從總體上來說分為兩類:一類是匿名映射,另一類是文件映射。

所以在處理虛擬內(nèi)存映射區(qū) vma 中的缺頁時(shí),也需要分為匿名映射區(qū)的缺頁處理以及文件映射區(qū)的缺頁處理。那么在這里,我們?cè)撊绾螀^(qū)分這個(gè)缺頁的 vma 到底是屬于匿名映射區(qū)還是文件映射區(qū)呢 ?

還記得筆者之前在 《從內(nèi)核世界透視 mmap 內(nèi)存映射的本質(zhì)(源碼實(shí)現(xiàn)篇)》 一文中介紹的內(nèi)存映射核心函數(shù) mmap_region 嗎?關(guān)于文件映射和匿名映射,有這樣的兩段代碼:

unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
                  ........ 省略 ........
    // 文件映射
    if (file) {
        // 將文件與虛擬內(nèi)存映射起來
        vma->vm_file = get_file(file);
        // 這一步中將虛擬內(nèi)存區(qū)域 vma 的操作函數(shù) vm_ops 映射成文件的操作函數(shù)(和具體文件系統(tǒng)有關(guān))
        // ext4 文件系統(tǒng)中的操作函數(shù)為 ext4_file_vm_ops
        // 從這一刻開始,讀寫內(nèi)存就和讀寫文件是一樣的了
        error = call_mmap(file, vma);
        if (error)
            goto unmap_and_free_vma;

        addr = vma->vm_start;
        vm_flags = vma->vm_flags;
    }  else {
        // 這里處理私有匿名映射
        // 將  vma->vm_ops 設(shè)置為 null,只有文件映射才需要 vm_ops 這樣才能將內(nèi)存與文件映射起來
        vma_set_anonymous(vma);
    }
}

在處理文件映射的代碼中,內(nèi)核調(diào)用了一個(gè)叫 call_mmap 的函數(shù),內(nèi)核在該函數(shù)中將虛擬內(nèi)存的相關(guān)操作函數(shù) vma->vm_ops 映射成了文件相關(guān)的操作函數(shù) ext4_file_vm_ops。正因?yàn)槿绱?,后續(xù)進(jìn)程讀寫這塊虛擬內(nèi)存就相當(dāng)于讀寫文件了。

static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
        ........ 省略 ........
        
      vma->vm_ops = &ext4_file_vm_ops;
      
        ........ 省略 ........    
}

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

而在處理匿名映射的代碼中,內(nèi)核調(diào)用了一個(gè)叫做 vma_set_anonymous 的函數(shù),在這里會(huì)將 vma->vm_ops 設(shè)置為 null ,因?yàn)檫@里映射的匿名內(nèi)存頁,背后并沒有文件來支撐。

static inline void vma_set_anonymous(struct vm_area_struct *vma)
{
	vma->vm_ops = NULL;
}

所以判斷一個(gè)虛擬內(nèi)存區(qū)域 vma 到底是文件映射區(qū)還是匿名映射區(qū)就是要看這個(gè) vma 的 vm_ops 是否為 null。

static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
	return !vma->vm_ops;
}

如果 vma_is_anonymous 返回 true,那么內(nèi)核就會(huì)在 handle_pte_fault 函數(shù)中調(diào)用 do_anonymous_page 進(jìn)行匿名映射區(qū)的缺頁處理。

如果 vma_is_anonymous 返回 false,那么內(nèi)核就調(diào)用 do_fault 進(jìn)行文件映射區(qū)的缺頁處理。

    // pte 是空的,表示缺頁地址 address 還從來沒有被映射過,接下來就要處理物理內(nèi)存的映射
    if (!vmf->pte) {
        // 判斷缺頁的虛擬內(nèi)存地址 address 所在的虛擬內(nèi)存區(qū)域 vma 是否是匿名映射區(qū)
        if (vma_is_anonymous(vmf->vma))
            // 處理匿名映射區(qū)發(fā)生的缺頁
            return do_anonymous_page(vmf);
        else
            // 處理文件映射區(qū)發(fā)生的缺頁
            return do_fault(vmf);
    }

第三種缺頁場(chǎng)景是,虛擬內(nèi)存地址 address 在進(jìn)程頁表中的頁表項(xiàng) pte 不是空的,但是其背后映射的物理內(nèi)存頁被內(nèi)核 swap out 到磁盤上了,CPU 訪問的時(shí)候依然會(huì)產(chǎn)生缺頁。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

那么我們?nèi)绾沃?pte 背后映射的物理內(nèi)存頁在不在內(nèi)存中呢 ?

筆者在之前的文章《一步一圖帶你構(gòu)建 Linux 頁表體系》 中介紹了頁表項(xiàng) pte 的比特位布局如下圖所示:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

其中 pte 的第 0 個(gè)比特位表示該 pte 映射的物理內(nèi)存頁是否在內(nèi)存中,值為 1 表示物理內(nèi)存頁在內(nèi)存中駐留,值為 0 表示物理內(nèi)存頁不在內(nèi)存中,可能被 swap 到磁盤上了。

#define _PAGE_BIT_PRESENT 0 /* is present */

#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)

如果我們可以把 pte 中的相關(guān)權(quán)限位提取出來,然后判斷權(quán)限位第 0 個(gè)比特位是否為 1 ,是不是就能知道 pte 映射的物理內(nèi)存頁到底在不在內(nèi)存中了,這個(gè)邏輯封裝在 pte_present 方法中:

static inline int pte_present(pte_t a)
{
	return pte_flags(a) & (_PAGE_PRESENT | _PAGE_PROTNONE);
}

pte_flags 函數(shù)用于從 pte 中提取相關(guān)的權(quán)限位,如何提取呢 ?可還記得我們?cè)谏闲」?jié)中介紹的從頁目錄項(xiàng)中提取其下一級(jí)頁目錄表的物理內(nèi)存地址時(shí)使用到的掩碼 PTE_PFN_MASK 嗎 ?

static inline unsigned long p4d_page_vaddr(p4d_t p4d)
{
    return (unsigned long)__va(p4d_val(p4d) & PTE_PFN_MASK;
}

/* Extracts the PFN from a (pte|pmd|pud|pgd)val_t of a 4KB page */
#define PTE_PFN_MASK        ((pteval_t)PHYSICAL_PAGE_MASK)

#define PHYSICAL_PAGE_MASK  (((signed long)PAGE_MASK) & __PHYSICAL_MASK)

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

如果我們把掩碼 PTE_PFN_MASK 取反,然后在和 pte 做與運(yùn)算,這樣 pte 中的相關(guān)權(quán)限標(biāo)記位不就提取出來么。

#define PTE_FLAGS_MASK		(~PTE_PFN_MASK)

static inline pteval_t pte_flags(pte_t pte)
{
	return native_pte_val(pte) & PTE_FLAGS_MASK;
}

static inline pteval_t native_pte_val(pte_t pte)
{
	return pte.pte;
}

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

然后用權(quán)限標(biāo)記位 pte_flags 和 _PAGE_PRESENT 做 & 運(yùn)算就可以知道 pte 背后映射的物理內(nèi)存頁是否在內(nèi)存中了。

如果我們通過 pte_present 判斷映射的物理內(nèi)存頁不在內(nèi)存中了,說明它已經(jīng)被內(nèi)核 swap out 到磁盤上了,這種情況下的缺頁處理就需要調(diào)用 do_swap_page 函數(shù),將磁盤上的物理內(nèi)存頁重新 swap in 到內(nèi)存中來。

   if (!pte_present(vmf->orig_pte))
        // 將之前映射的物理內(nèi)存頁從磁盤中重新 swap in 到內(nèi)存中
        return do_swap_page(vmf);

以上介紹的這三種缺頁場(chǎng)景都是屬于缺頁內(nèi)存地址 address 背后映射的物理內(nèi)存頁不在內(nèi)存中的類別。

下面我們來看下另一類別,也就是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁在內(nèi)存中的情況 ,這里又會(huì)近一步分為兩種缺頁場(chǎng)景。

筆者曾在?《深入理解 Linux 物理內(nèi)存管理》一文中為大家介紹了 Linux 內(nèi)核在 NUMA 架構(gòu)下物理內(nèi)存管理的相關(guān)內(nèi)容。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在 NUMA 架構(gòu)下,CPU 訪問自己的本地內(nèi)存節(jié)點(diǎn)是最快的,但訪問其他內(nèi)存節(jié)點(diǎn)就會(huì)慢很多,這就導(dǎo)致了 CPU 訪問內(nèi)存的速度不一致。

回到我們?nèi)表撎幚淼膱?chǎng)景中就是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁雖然在內(nèi)存中,但是它可能是進(jìn)程所在 CPU 中的本地 NUMA 節(jié)點(diǎn)上的內(nèi)存,也可能是其他 NUMA 節(jié)點(diǎn)上的內(nèi)存。

因?yàn)?CPU 對(duì)不同 NUMA 節(jié)點(diǎn)上的內(nèi)存有訪問速度上的差異,所以內(nèi)核通常傾向于讓 CPU 盡量訪問本地 NUMA 節(jié)點(diǎn)上的內(nèi)存。NUMA Balancing 機(jī)制就是用來解決這個(gè)問題的。

通俗來講,NUMA Balancing 主要干兩件事情,一件事是讓內(nèi)存跟著 CPU 走,另一件事是讓 CPU 跟著內(nèi)存走。

進(jìn)程申請(qǐng)到的物理內(nèi)存頁可能在當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上,也可能在其他 NUMA 節(jié)點(diǎn)上。

所謂讓內(nèi)存跟著 CPU 走的意思就是,當(dāng)進(jìn)程訪問的物理內(nèi)存頁不在當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上時(shí),NUMA Balancing 就會(huì)嘗試將遠(yuǎn)程 NUMA 節(jié)點(diǎn)上的物理內(nèi)存頁遷移到本地 NUMA 節(jié)點(diǎn)上,加快進(jìn)程訪問內(nèi)存的速度。

所謂讓 CPU 跟著內(nèi)存走的意思就是,當(dāng)進(jìn)程經(jīng)常訪問的大部分物理內(nèi)存頁均不在當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上時(shí),NUMA Balancing 干脆就把進(jìn)程重新調(diào)度到這些物理內(nèi)存頁所在的 NUMA 節(jié)點(diǎn)上。當(dāng)然整個(gè) NUMA Balancing 的過程會(huì)根據(jù)我們?cè)O(shè)置的 NUMA policy 以及各個(gè) NUMA 節(jié)點(diǎn)上缺頁的次數(shù)來綜合考慮是否遷移內(nèi)存頁。這里涉及到的細(xì)節(jié)很多,筆者就不一一展開了。

NUMA Balancing 會(huì)周期性掃描進(jìn)程虛擬內(nèi)存地址空間,如果發(fā)現(xiàn)虛擬內(nèi)存背后映射的物理內(nèi)存頁不在當(dāng)前 CPU 本地 NUMA 節(jié)點(diǎn)的時(shí)候,就會(huì)把對(duì)應(yīng)的頁表項(xiàng) pte 標(biāo)記為 _PAGE_PROTNONE,也就是將 pte 的第 8 個(gè) 比特位置為 1,隨后會(huì)將 pte 的 Present 位置為 0 。

#define _PAGE_PROTNONE	(_AT(pteval_t, 1) << _PAGE_BIT_PROTNONE)

#define _PAGE_BIT_PROTNONE	_PAGE_BIT_GLOBAL

#define _PAGE_BIT_GLOBAL	8

這種情況下調(diào)用 pte_present 依然很返回 true ,因?yàn)楫?dāng)前的物理內(nèi)存頁畢竟是在內(nèi)存中的,只不過不在當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上而已。

當(dāng) pte 被標(biāo)記為 _PAGE_PROTNONE 之后,這意味著該 pte 背后映射的物理內(nèi)存頁進(jìn)程對(duì)其沒有讀寫權(quán)限,也沒有可執(zhí)行的權(quán)限。進(jìn)程在訪問這段虛擬內(nèi)存地址的時(shí)候就會(huì)發(fā)生缺頁。

當(dāng)進(jìn)入缺頁異常的處理程序之后,內(nèi)核會(huì)在 handle_pte_fault 函數(shù)中通過 pte_protnone 函數(shù)判斷,缺頁的 pte 是否被標(biāo)記了 _PAGE_PROTNONE 標(biāo)識(shí)。

static inline int pte_protnone(pte_t pte)
{
	return (pte_flags(pte) & (_PAGE_PROTNONE | _PAGE_PRESENT))
		== _PAGE_PROTNONE;
}

如果 pte 被標(biāo)記了 _PAGE_PROTNONE,并且對(duì)應(yīng)的虛擬內(nèi)存區(qū)域是一個(gè)具有讀寫,可執(zhí)行權(quán)限的 vma。這就說明該 vma 背后映射的物理內(nèi)存頁不在當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上。

static inline bool vma_is_accessible(struct vm_area_struct *vma)
{
	return vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE);
}

這里需要調(diào)用 do_numa_page,將這個(gè)遠(yuǎn)程 NUMA 節(jié)點(diǎn)上的物理內(nèi)存頁遷移到當(dāng)前 CPU 的本地 NUMA 節(jié)點(diǎn)上,從而加快進(jìn)程訪問內(nèi)存的速度。

  if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
        return do_numa_page(vmf);

NUMA Balancing 機(jī)制看起來非常好,但是同時(shí)也會(huì)為系統(tǒng)引入很多開銷,比如,掃描進(jìn)程地址空間的開銷,缺頁的開銷,更主要的是頁面遷移的開銷會(huì)很大,這也會(huì)引起 CPU 有時(shí)候莫名其妙的飆到 100 %。因此筆者建議在一般情況下還是將 NUMA Balancing 關(guān)閉為好,除非你有明確的理由開啟。

我們可以將內(nèi)核參數(shù) /proc/sys/kernel/numa_balancing 設(shè)置為 0 或者通過 sysctl 命令來關(guān)閉 NUMA Balancing。

echo 0 > /proc/sys/kernel/numa_balancing

sysctl -w kernel.numa_balancing=0

第二種場(chǎng)景就是寫時(shí)復(fù)制了(Copy On Write, COW),這種場(chǎng)景和 NUMA Balancing 一樣,都屬于缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁在內(nèi)存中而引起的缺頁中斷。

COW 在內(nèi)核的內(nèi)存管理子系統(tǒng)中很常見了,比如,父進(jìn)程通過 fork 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程之后,父子進(jìn)程的虛擬內(nèi)存空間完全是一模一樣的,包括父子進(jìn)程的頁表內(nèi)容都是一樣的,父子進(jìn)程頁表中的 PTE 均指向同一物理內(nèi)存頁面,此時(shí)內(nèi)核會(huì)將父子進(jìn)程頁表中的 PTE 均改為只讀的,并將父子進(jìn)程共同映射的這個(gè)物理頁面引用計(jì)數(shù) + 1。

static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
        pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
        unsigned long addr, int *rss)
{
    /*
     * If it's a COW mapping, write protect it both
     * in the parent and the child
     */
    if (is_cow_mapping(vm_flags) && pte_write(pte)) {
        // 設(shè)置父進(jìn)程的 pte 為只讀
        ptep_set_wrprotect(src_mm, addr, src_pte);
        // 設(shè)置子進(jìn)程的 pte 為只讀
        pte = pte_wrprotect(pte);
    }
    // 獲取 pte 中映射的物理內(nèi)存頁(此時(shí)父子進(jìn)程共享該頁)
    page = vm_normal_page(vma, addr, pte);
    // 物理內(nèi)存頁的引用計(jì)數(shù) + 1
    get_page(page);
}

當(dāng)父進(jìn)程或者子進(jìn)程對(duì)該頁面發(fā)生寫操作的時(shí)候,我們現(xiàn)在假設(shè)子進(jìn)程先對(duì)頁面發(fā)生寫操作,隨后子進(jìn)程發(fā)現(xiàn)自己頁表中的 PTE 是只讀的,于是產(chǎn)生缺頁中斷,子進(jìn)程進(jìn)入內(nèi)核態(tài),內(nèi)核會(huì)在本小節(jié)介紹的缺頁中斷處理程序中發(fā)現(xiàn),訪問的這個(gè)物理頁面引用計(jì)數(shù)大于 1,說明此時(shí)該物理內(nèi)存頁面存在多進(jìn)程共享的情況,于是發(fā)生寫時(shí)復(fù)制(Copy On Write, COW),內(nèi)核為子進(jìn)程重新分配一個(gè)新的物理頁面,然后將原來物理頁中的內(nèi)容拷貝到新的頁面中,最后子進(jìn)程頁表中的 PTE 指向新的物理頁面并將 PTE 的 R/W 位設(shè)置為 1,原來物理頁面的引用計(jì)數(shù) - 1。

后面父進(jìn)程在對(duì)頁面進(jìn)行寫操作的時(shí)候,同樣也會(huì)發(fā)現(xiàn)父進(jìn)程的頁表中 PTE 是只讀的,也會(huì)產(chǎn)生缺頁中斷,但是在內(nèi)核的缺頁中斷處理程序中,發(fā)現(xiàn)訪問的這個(gè)物理頁面引用計(jì)數(shù)為 1 了,那么就只需要將父進(jìn)程頁表中的 PTE 的 R/W 位設(shè)置為 1 就可以了。

還有筆者在之前的文章?《從內(nèi)核世界透視 mmap 內(nèi)存映射的本質(zhì)(原理篇)》中介紹的私有文件映射,也用到了 COW,當(dāng)多個(gè)進(jìn)程采用私有文件映射的方式對(duì)同一文件的同一部分進(jìn)行映射的時(shí)候,后續(xù)產(chǎn)生的 pte 也都是只讀的。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)任意進(jìn)程開始對(duì)它的私有文件映射區(qū)進(jìn)行寫操作時(shí),就會(huì)發(fā)生寫時(shí)復(fù)制,隨后內(nèi)核會(huì)在這里介紹的缺頁中斷程序中重新申請(qǐng)一個(gè)內(nèi)存頁,然后將 page cache 中的內(nèi)容拷貝到這個(gè)新的內(nèi)存頁中,進(jìn)程頁表中對(duì)應(yīng)的 pte 會(huì)重新關(guān)聯(lián)到這個(gè)新的內(nèi)存頁上,此時(shí) pte 的權(quán)限變?yōu)榭蓪憽?/p>

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在以上介紹的兩種寫時(shí)復(fù)制應(yīng)用場(chǎng)景中,他們都有一個(gè)共同的特點(diǎn),就是進(jìn)程的虛擬內(nèi)存區(qū)域 vma 的權(quán)限是可寫的,但是其對(duì)應(yīng)在頁表中的 pte 卻是只讀的,而 pte 映射的物理內(nèi)存頁也在內(nèi)存中。

內(nèi)核正是利用這個(gè)特點(diǎn)來判斷本次缺頁中斷是否是由寫時(shí)復(fù)制引起的。如果是,則調(diào)用 do_wp_page 進(jìn)行寫時(shí)復(fù)制的缺頁處理。

    // 判斷本次缺頁是否為寫時(shí)復(fù)制引起的
    if (vmf->flags & FAULT_FLAG_WRITE) {
        // 這里說明 vma 是可寫的,但是 pte 被標(biāo)記為不可寫,說明是寫保護(hù)類型的中斷
        if (!pte_write(entry))
            // 進(jìn)行寫時(shí)復(fù)制處理,cow 就發(fā)生在這里
            return do_wp_page(vmf);
    }

在我們清楚了以上背景知識(shí)之后,再來看 handle_pte_fault 的缺頁處理邏輯就很清晰了:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
    pte_t entry;

    if (unlikely(pmd_none(*vmf->pmd))) {
        // 如果 pmd 是空的,說明現(xiàn)在連頁表都沒有,頁表項(xiàng) pte 自然是空的
        vmf->pte = NULL;
    } else {
        // vmf->pte 表示缺頁虛擬內(nèi)存地址在頁表中對(duì)應(yīng)的頁表項(xiàng) pte
        // 通過 pte_offset_map 定位到虛擬內(nèi)存地址 address 對(duì)應(yīng)在頁表中的 pte
        // 這里根據(jù) address 獲取 pte_index,然后從 pmd 中提取頁表起始虛擬內(nèi)存地址相加獲取 pte
        vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
        //  vmf->orig_pte 表示發(fā)生缺頁時(shí),address 對(duì)應(yīng)的 pte 值
        vmf->orig_pte = *vmf->pte;

        // 這里 pmd 不是空的,表示現(xiàn)在是有頁表存在的,但缺頁虛擬內(nèi)存地址在頁表中的 pte 是空值
        if (pte_none(vmf->orig_pte)) {
            pte_unmap(vmf->pte);
            vmf->pte = NULL;
        }
    }

    // pte 是空的,表示缺頁地址 address 還從來沒有被映射過,接下來就要處理物理內(nèi)存的映射
    if (!vmf->pte) {
        // 判斷缺頁的虛擬內(nèi)存地址 address 所在的虛擬內(nèi)存區(qū)域 vma 是否是匿名映射區(qū)
        if (vma_is_anonymous(vmf->vma))
            // 處理匿名映射區(qū)發(fā)生的缺頁
            return do_anonymous_page(vmf);
        else
            // 處理文件映射區(qū)發(fā)生的缺頁
            return do_fault(vmf);
    }

    // 走到這里表示 pte 不是空的,但是 pte 中的 p 比特位是 0 值,表示之前映射的物理內(nèi)存頁已不在內(nèi)存中(swap out)
    if (!pte_present(vmf->orig_pte))
        // 將之前映射的物理內(nèi)存頁從磁盤中重新 swap in 到內(nèi)存中
        return do_swap_page(vmf);

    // 這里表示 pte 背后映射的物理內(nèi)存頁在內(nèi)存中,但是 NUMA Balancing 發(fā)現(xiàn)該內(nèi)存頁不在當(dāng)前進(jìn)程運(yùn)行的 numa 節(jié)點(diǎn)上
    // 所以將該 pte 標(biāo)記為 _PAGE_PROTNONE(無讀寫,可執(zhí)行權(quán)限)
    // 進(jìn)程訪問該內(nèi)存頁時(shí)發(fā)生缺頁中斷,在這里的 do_numa_page 中,內(nèi)核將該 page 遷移到進(jìn)程運(yùn)行的 numa 節(jié)點(diǎn)上。
    if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
        return do_numa_page(vmf);

    entry = vmf->orig_pte;
    // 如果本次缺頁中斷是由寫操作引起的
    if (vmf->flags & FAULT_FLAG_WRITE) {
        // 這里說明 vma 是可寫的,但是 pte 被標(biāo)記為不可寫,說明是寫保護(hù)類型的中斷
        if (!pte_write(entry))
            // 進(jìn)行寫時(shí)復(fù)制處理,cow 就發(fā)生在這里
            return do_wp_page(vmf);
        // 如果 pte 是可寫的,就將 pte 標(biāo)記為臟頁
        entry = pte_mkdirty(entry);
    }
    // 將 pte 的 access 比特位置 1 ,表示該 page 是活躍的。避免被 swap 出去
    entry = pte_mkyoung(entry);

    // 經(jīng)過上面的缺頁處理,這里會(huì)判斷原來的頁表項(xiàng) entry(orig_pte) 值是否發(fā)生了變化
    // 如果發(fā)生了變化,就把 entry 更新到 vmf->pte 中。
    if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
                vmf->flags & FAULT_FLAG_WRITE)) {
        // pte 既然變化了,則刷新 mmu (體系結(jié)構(gòu)相關(guān))
        update_mmu_cache(vmf->vma, vmf->address, vmf->pte);
    } else {
        // 如果 pte 內(nèi)容本身沒有變化,則不需要刷新任何東西
        // 但是有個(gè)特殊情況就是寫保護(hù)類型中斷,產(chǎn)生的寫時(shí)復(fù)制,產(chǎn)生了新的映射關(guān)系,需要刷新一下 tlb
		/*
		 * This is needed only for protection faults but the arch code
		 * is not yet telling us if this is a protection fault or not.
		 * This still avoids useless tlb flushes for .text page faults
		 * with threads.
		 */
        if (vmf->flags & FAULT_FLAG_WRITE)
            flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
    }

    return 0;
}

7. do_anonymous_page 處理匿名頁缺頁

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在本文的第五小節(jié)中,我們完成了各級(jí)頁目錄的補(bǔ)齊填充工作,但是現(xiàn)在最后一級(jí)頁表還沒有著落,所以在處理缺頁之前,我們需要調(diào)用 pte_alloc 繼續(xù)把頁表補(bǔ)齊了。

#define pte_alloc(mm, pmd) (unlikely(pmd_none(*(pmd))) && __pte_alloc(mm, pmd))

首先我們通過 pmd_none 判斷缺頁地址 address 在進(jìn)程頁表中間頁目錄 PMD 中對(duì)應(yīng)的頁目錄項(xiàng) pmd 是否是空的,如果 pmd 是空的,說明此時(shí)還不存在一級(jí)頁表,這樣一來,就需要調(diào)用 __pte_alloc 來分配一張頁表,然后用頁表的 pfn 以及初始權(quán)限位 _PAGE_TABLE 來填充 pmd。

static inline void pmd_populate(struct mm_struct *mm, pmd_t *pmd,
                struct page *pte)
{
    // 通過頁表 page 獲取對(duì)應(yīng)的 pfn
    unsigned long pfn = page_to_pfn(pte);
    // 將頁表 page 的 pfn 以及初始權(quán)限位 _PAGE_TABLE 填充到 pmd 中
    set_pmd(pmd, __pmd(((pteval_t)pfn << PAGE_SHIFT) | _PAGE_TABLE));
}

這里 __pte_alloc 的流程邏輯和前面我們介紹的__pud_alloc,__pmd_alloc 可以說是一模一樣,都是創(chuàng)建其下一級(jí)頁目錄或者頁表,然后填充對(duì)應(yīng)的頁目錄項(xiàng),這里就不做過多的介紹了。

int __pte_alloc(struct mm_struct *mm, pmd_t *pmd)
{
    spinlock_t *ptl;
    // 調(diào)用 get_zeroed_page 申請(qǐng)一個(gè) 4k 物理內(nèi)存頁并初始化為 0 值作為新的 頁表
    // new 指向新分配的 頁表 起始內(nèi)存地址
    pgtable_t new = pte_alloc_one(mm);
    if (!new)
        return -ENOMEM;
    // 鎖定中間頁目錄項(xiàng) pmd
    ptl = pmd_lock(mm, pmd);
    // 如果 pmd 是空的,說明此時(shí) pmd 并未指向頁表,下面就需要用新頁表 new 來填充 pmd 
    if (likely(pmd_none(*pmd))) {  
        // 更新 mm->pgtables_bytes 計(jì)數(shù),該字段用于統(tǒng)計(jì)進(jìn)程頁表所占用的字節(jié)數(shù)
        // 由于這里新增了一張頁表,所以計(jì)數(shù)需要增加 PTRS_PER_PTE * sizeof(pte_t)
        mm_inc_nr_ptes(mm);
        // 將 new 指向的新分配出來的頁表 page 的 pfn 以及相關(guān)初始權(quán)限位填充到 pmd 中
        pmd_populate(mm, pmd, new);
        new = NULL;
    }
    spin_unlock(ptl);
    return 0;
}

// 頁表可以容納的頁表項(xiàng) pte_t 的個(gè)數(shù)
#define PTRS_PER_PTE  512

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在我們已經(jīng)有了一級(jí)頁表,但是頁表中的 pte 還都是空的,接下來就該用這個(gè)空的 pte 來映射物理內(nèi)存頁了。

首先我們通過 alloc_zeroed_user_highpage_movable 來分配一個(gè)物理內(nèi)存頁出來,關(guān)于物理內(nèi)存詳細(xì)的分配過程,感興趣的讀者可以看下筆者的這篇文章——《深入理解 Linux 物理內(nèi)存分配全鏈路實(shí)現(xiàn)》。

這個(gè)物理內(nèi)存頁就是為缺頁地址 address 映射的物理內(nèi)存了,隨后我們通過 mk_pte 利用物理內(nèi)存頁 page 的 pfn 以及缺頁內(nèi)存區(qū)域 vma 中記錄的頁屬性 vma->vm_page_prot 填充一個(gè)新的頁表項(xiàng) entry 出來。

entry 這里只是一個(gè)臨時(shí)的值,后續(xù)會(huì)將 entry 的值設(shè)置到真正的 pte 中。

#define mk_pte(page, pgprot)   pfn_pte(page_to_pfn(page), (pgprot))

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

如果缺頁內(nèi)存地址 address 所在的虛擬內(nèi)存區(qū)域 vma 是可寫的,那么我們就通過 pte_mkwrite 和 pte_mkdirty 將臨時(shí)頁表項(xiàng) entry 的 R/W(1) 比特位和D(6) 比特位置為 1 。表示該頁表項(xiàng)背后映射的物理內(nèi)存頁 page 是可寫的,并且標(biāo)記為臟頁。

  if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry));

注意,此時(shí)缺頁內(nèi)存地址 address 在頁表中的 pte 還是空的,我們還沒有設(shè)置呢,目前只是先將值初始化到臨時(shí)的頁表項(xiàng) entry 中,下面才到設(shè)置真正的 pte 的時(shí)候。

調(diào)用 pte_offset_map_lock,首先獲取 address 在一級(jí)頁表中的真正 pte,然后將一級(jí)頁表鎖定。

#define pte_offset_map_lock(mm, pmd, address, ptlp) \
({                          \
    // 獲取 pmd 映射的一級(jí)頁表鎖
    spinlock_t *__ptl = pte_lockptr(mm, pmd);   \
    // 獲取 pte
    pte_t *__pte = pte_offset_map(pmd, address);    \
    *(ptlp) = __ptl;                \
    // 鎖定一級(jí)頁表
    spin_lock(__ptl);               \
    __pte;                      \
})

按理說此時(shí)獲取到的 pte 應(yīng)該是空的,如果 pte 不為空,說明已經(jīng)有其他線程把缺頁處理好了,pte 已經(jīng)被填充了,那么本次缺頁處理就該停止,不能在往下走了,直接跳轉(zhuǎn)到 release 處,釋放頁表鎖,釋放新分配的物理內(nèi)存頁 page。

    if (!pte_none(*vmf->pte))
        goto release;

如果 pte 為空,說明此時(shí)沒有其他線程對(duì)缺頁進(jìn)行并發(fā)處理,我們可以接著處理缺頁。

進(jìn)程使用到的常駐內(nèi)存等相關(guān)統(tǒng)計(jì)信息保存在 task->rss_stat 字段中:

struct task_struct {
    // 統(tǒng)計(jì)進(jìn)程常駐內(nèi)存信息
    struct task_rss_stat rss_stat;
}

由于這里我們新分配一個(gè)匿名內(nèi)存頁用于缺頁處理,所以相關(guān) rss_stat 統(tǒng)計(jì)信息 —— task->rss_stat.count[MM_ANONPAGES] 要加 1 。

// MM_ANONPAGES —— Resident anonymous pages 
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);

#define inc_mm_counter_fast(mm, member) add_mm_counter_fast(mm, member, 1)

static void add_mm_counter_fast(struct mm_struct *mm, int member, int val)
{
	struct task_struct *task = current;

	if (likely(task->mm == mm))
		task->rss_stat.count[member] += val;
	else
		add_mm_counter(mm, member, val);
}

隨后調(diào)用 page_add_new_anon_rmap 建立匿名頁的反向映射關(guān)系,關(guān)于匿名頁的反向映射筆者已經(jīng)在之前的文章 —— ?《深入理解 Linux 物理內(nèi)存管理》 中詳細(xì)介紹過了,感興趣的朋友可以回看下。

反向映射建立好之后,調(diào)用 lru_cache_add_active_or_unevictable 將匿名內(nèi)存頁加入到 LRU 活躍鏈表中。

最后調(diào)用 set_pte_at 將之間我們臨時(shí)填充的頁表項(xiàng) entry 賦值給缺頁 address 真正對(duì)應(yīng)的 pte。

set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

#define set_pte_at(mm, addr, ptep, pte)	native_set_pte_at(mm, addr, ptep, pte)

static inline void native_set_pte_at(struct mm_struct *mm, unsigned long addr,
				     pte_t *ptep , pte_t pte)
{
	native_set_pte(ptep, pte);
}

static inline void native_set_pte(pte_t *ptep, pte_t pte)
{
	WRITE_ONCE(*ptep, pte);
}

到這里我們才算是真正把進(jìn)程的頁表體系給補(bǔ)齊了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

在明白以上內(nèi)容之后,我們回過頭來看在 do_anonymous_page 匿名頁缺頁處理的邏輯就很清晰了:

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    // 缺頁地址 address 所在的虛擬內(nèi)存區(qū)域 vma
    struct vm_area_struct *vma = vmf->vma;
    // 指向分配的物理內(nèi)存頁,后面與虛擬內(nèi)存進(jìn)行映射
    struct page *page;
    vm_fault_t ret = 0;
    // 臨時(shí)的 pte 用于構(gòu)建 pte 中的值,后續(xù)會(huì)賦值給 address 在頁表中對(duì)應(yīng)的真正 pte
    pte_t entry;

    // 如果 pmd 是空的,表示現(xiàn)在還沒有一級(jí)頁表
    // pte_alloc 這里會(huì)創(chuàng)建一級(jí)頁表,并填充 pmd 中的內(nèi)容
    if (pte_alloc(vma->vm_mm, vmf->pmd))
        return VM_FAULT_OOM;
  
    // 頁表創(chuàng)建好之后,這里從伙伴系統(tǒng)中分配一個(gè) 4K 物理內(nèi)存頁出來
    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
    if (!page)
        goto oom;
    // 將 page 的 pfn 以及相關(guān)權(quán)限標(biāo)記位 vm_page_prot 初始化一個(gè)臨時(shí) pte 出來 
    entry = mk_pte(page, vma->vm_page_prot);
    // 如果 vma 是可寫的,則將 pte 標(biāo)記為可寫,臟頁。
    if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry));
    // 鎖定一級(jí)頁表,并獲取 address 在頁表中對(duì)應(yīng)的真實(shí) pte
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
            &vmf->ptl);
    // 是否有其他線程在并發(fā)處理缺頁
    if (!pte_none(*vmf->pte))
        goto release;
    // 增加 進(jìn)程 rss 相關(guān)計(jì)數(shù),匿名內(nèi)存頁計(jì)數(shù) + 1
    inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
    // 建立匿名頁反向映射關(guān)系
    page_add_new_anon_rmap(page, vma, vmf->address, false);
    // 將匿名頁添加到 LRU 鏈表中
    lru_cache_add_active_or_unevictable(page, vma);
setpte:
    // 將 entry 賦值給真正的 pte,這里 pte 就算被填充好了,進(jìn)程頁表體系也就補(bǔ)齊了
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    // 刷新 mmu 
    update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
    // 解除 pte 的映射
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return ret;
release:
    // 釋放 page 
    put_page(page);
    goto unlock;
oom:
    return VM_FAULT_OOM;
}

8. do_fault 處理文件頁缺頁

筆者在之前的文章《從內(nèi)核世界透視 mmap 內(nèi)存映射的本質(zhì)(源碼實(shí)現(xiàn)篇)》?中,在為大家介紹到 mmap 文件映射的源碼實(shí)現(xiàn)時(shí),特別強(qiáng)調(diào)了一下,mmap 內(nèi)存文件映射的本質(zhì)其實(shí)就是將虛擬映射區(qū) vma 的相關(guān)操作 vma->vm_ops 映射成文件的相關(guān)操作 ext4_file_vm_ops。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
                  ........ 省略 ........
    // 文件映射
    if (file) {
        // 將文件與虛擬內(nèi)存映射起來
        vma->vm_file = get_file(file);
        // 這一步中將虛擬內(nèi)存區(qū)域 vma 的操作函數(shù) vm_ops 映射成文件的操作函數(shù)(和具體文件系統(tǒng)有關(guān))
        // ext4 文件系統(tǒng)中的操作函數(shù)為 ext4_file_vm_ops
        // 從這一刻開始,讀寫內(nèi)存就和讀寫文件是一樣的了
        error = call_mmap(file, vma);
    } 
}

static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{     
      vma->vm_ops = &ext4_file_vm_ops;
}

在 vma->vm_ops 中有個(gè)重要的函數(shù) fault,在 ext4 文件系統(tǒng)中的實(shí)現(xiàn)是:ext4_filemap_fault 函數(shù)。

static const struct vm_operations_struct ext4_file_vm_ops = {
    .fault      = ext4_filemap_fault,
    .map_pages  = filemap_map_pages,
    .page_mkwrite   = ext4_page_mkwrite,
};

vma->vm_ops->fault 函數(shù)就是專門用于處理文件映射區(qū)缺頁的,本小節(jié)要介紹的文件頁的缺頁處理的核心就是依賴這個(gè)函數(shù)完成的。

我們知道 mmap 進(jìn)行文件映射的時(shí)候只是單純地建立了虛擬內(nèi)存與文件之間的映射關(guān)系,此時(shí)并沒有物理內(nèi)存分配。當(dāng)進(jìn)程對(duì)這段文件映射區(qū)進(jìn)行讀取操作的時(shí)候,會(huì)觸發(fā)缺頁,然后分配物理內(nèi)存(文件頁),這一部分邏輯在下面的 do_read_fault 函數(shù)中完成,它主要處理的是由于對(duì)文件映射區(qū)的讀取操作而引起的缺頁情況。

而 mmap 文件映射又分為私有文件映射與共享文件映射兩種映射方式,而私有文件映射的核心特點(diǎn)是讀共享的,當(dāng)任意進(jìn)程對(duì)私有文件映射區(qū)發(fā)生寫入操作時(shí)候,就會(huì)發(fā)生寫時(shí)復(fù)制 COW,這一部分邏輯在下面的 do_cow_fault 函數(shù)中完成。

對(duì)共享文件映射區(qū)進(jìn)行的寫入操作而引起的缺頁,內(nèi)核放在 do_shared_fault 函數(shù)中進(jìn)行處理。

static vm_fault_t do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct mm_struct *vm_mm = vma->vm_mm;
    vm_fault_t ret;

    // 處理 vm_ops->fault 為 null 的異常情況
    if (!vma->vm_ops->fault) {
        // 如果中間頁目錄 pmd 指向的一級(jí)頁表不在內(nèi)存中,則返回 SIGBUS 錯(cuò)誤
        if (unlikely(!pmd_present(*vmf->pmd)))
            ret = VM_FAULT_SIGBUS;
        else {
            // 獲取缺頁的頁表項(xiàng) pte
            vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
                               vmf->pmd,
                               vmf->address,
                               &vmf->ptl);
            // pte 為空,則返回 SIGBUS 錯(cuò)誤
            if (unlikely(pte_none(*vmf->pte)))
                ret = VM_FAULT_SIGBUS;
            else
                // pte 不為空,返回 NOPAGE,即本次缺頁處理不會(huì)分配物理內(nèi)存頁
                ret = VM_FAULT_NOPAGE;

            pte_unmap_unlock(vmf->pte, vmf->ptl);
        }
    } else if (!(vmf->flags & FAULT_FLAG_WRITE))
        // 缺頁如果是讀操作引起的,進(jìn)入 do_read_fault 處理
        ret = do_read_fault(vmf);
    else if (!(vma->vm_flags & VM_SHARED))
        // 缺頁是由私有映射區(qū)的寫入操作引起的,則進(jìn)入 do_cow_fault 處理寫時(shí)復(fù)制
        ret = do_cow_fault(vmf);
    else
        // 處理共享映射區(qū)的寫入缺頁
        ret = do_shared_fault(vmf);

    return ret;
}

8.1 do_read_fault 處理讀操作引起的缺頁

當(dāng)我們調(diào)用 mmap 對(duì)文件進(jìn)行映射的時(shí)候,無論是采用私有文件映射的方式還是共享文件映射的方式,內(nèi)核都只是會(huì)在進(jìn)程的地址空間中為本次映射創(chuàng)建出一段虛擬映射區(qū) vma 出來,然后將這段虛擬映射區(qū) vma 與映射文件關(guān)聯(lián)起來就結(jié)束了,整個(gè)映射過程并未涉及到物理內(nèi)存的分配。

下面是多進(jìn)程對(duì)同一文件中的同一段文件區(qū)域進(jìn)行私有映射后,內(nèi)核中的結(jié)構(gòu)圖:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)任意進(jìn)程開始訪問其地址空間中的這段虛擬內(nèi)存區(qū)域 vma 時(shí),由于背后沒有對(duì)應(yīng)文件頁進(jìn)行映射,所以會(huì)發(fā)生缺頁中斷,在缺頁中斷中內(nèi)核會(huì)首先分配一個(gè)物理內(nèi)存頁并加入到 page cache 中,隨后將映射的文件內(nèi)容讀取到剛剛創(chuàng)建出來的物理內(nèi)存頁中,然后將這個(gè)物理內(nèi)存頁映射到缺頁虛擬內(nèi)存地址 address 對(duì)應(yīng)在進(jìn)程頁表中的 pte 中。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

除此之外,內(nèi)核還會(huì)考慮到進(jìn)程訪問內(nèi)存的空間局部性,所以內(nèi)核除了會(huì)映射本次缺頁需要的文件頁之外,還會(huì)將其相鄰的文件頁讀取到 page cache 中,然后將這些相鄰的文件頁映射到對(duì)應(yīng)的 pte 中。這一部分預(yù)先提前映射的邏輯在 map_pages 函數(shù)中實(shí)現(xiàn)。

static const struct vm_operations_struct ext4_file_vm_ops = {
    .fault      = ext4_filemap_fault,
    .map_pages  = filemap_map_pages,
    .page_mkwrite   = ext4_page_mkwrite,
};

如果不滿足預(yù)先提前映射的條件,那么內(nèi)核就只會(huì)專注處理映射本次缺頁所需要的文件頁。

首先通過上面的 fault 函數(shù),當(dāng)映射文件所在文件系統(tǒng)是 ext4 時(shí),該函數(shù)的實(shí)現(xiàn)為 ext4_filemap_fault,該函數(shù)只負(fù)責(zé)獲取本次缺頁所需要的文件頁。

當(dāng)獲取到文件頁之后,內(nèi)核會(huì)調(diào)用 finish_fault 函數(shù),將文件頁映射到缺頁地址 address 在進(jìn)程頁表中對(duì)應(yīng)的 pte 中,do_read_fault 函數(shù)處理就完成了,不過需要注意的是,對(duì)于私有文件映射的話,此時(shí)的這個(gè) pte 還是只讀的,多進(jìn)程之間讀共享,當(dāng)任意進(jìn)程嘗試寫入的時(shí)候,會(huì)發(fā)生寫時(shí)復(fù)制。

static unsigned long fault_around_bytes __read_mostly =
	rounddown_pow_of_two(65536);

static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret = 0;

    // map_pages 用于提前預(yù)先映射文件頁相鄰的若干文件頁到相關(guān) pte 中,從而減少缺頁次數(shù)
    // fault_around_bytes 控制預(yù)先映射的的字節(jié)數(shù)默認(rèn)初始值為 65536(16個(gè)物理內(nèi)存頁)
    if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
        // 這里會(huì)嘗試使用 map_pages 將缺頁地址 address 附近的文件頁預(yù)讀進(jìn) page cache
        // 然后填充相關(guān)的 pte,目的是減少缺頁次數(shù)
        ret = do_fault_around(vmf);
        if (ret)
            return ret;
    }

    // 如果不滿足預(yù)先映射的條件,則只映射本次需要的文件頁
    // 首先會(huì)從 page cache 中讀取文件頁,如果 page cache 中不存在則從磁盤中讀取,并預(yù)讀若干文件頁到 page cache 中
    ret = __do_fault(vmf);     // 這里需要負(fù)責(zé)獲取文件頁,并不映射
    // 將本次缺頁所需要的文件頁映射到 pte 中。
    ret |= finish_fault(vmf);
    unlock_page(vmf->page);
    return ret;
}

__do_fault 函數(shù)底層會(huì)調(diào)用到 vma->vm_ops->fault,在 ext4 文件系統(tǒng)中對(duì)應(yīng)的實(shí)現(xiàn)是 ext4_filemap_fault。

static vm_fault_t __do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret;
          ...... 省略 ......
    ret = vma->vm_ops->fault(vmf);
          ...... 省略 ......
    return ret;
}

vm_fault_t ext4_filemap_fault(struct vm_fault *vmf)
{
    ret = filemap_fault(vmf);
    return ret;
}

filemap_fault 主要的任務(wù)就是先把缺頁所需要的文件頁獲取出來,為后面的映射做準(zhǔn)備。

以下內(nèi)容涉及到文件以及 page cache 的相關(guān)操作,對(duì)細(xì)節(jié)感興趣的讀者可以回看下筆者之前的文章 —— 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》

內(nèi)核在這里首先會(huì)調(diào)用 find_get_page 從 page cache 中嘗試獲取文件頁,如果文件頁存在,則繼續(xù)調(diào)用 do_async_mmap_readahead 啟動(dòng)異步預(yù)讀機(jī)制,將相鄰的若干文件頁一起預(yù)讀進(jìn) page cache 中。

如果文件頁不在 page cache 中,內(nèi)核則會(huì)調(diào)用 do_sync_mmap_readahead 來同步預(yù)讀,這里首先會(huì)分配一個(gè)物理內(nèi)存頁出來,然后將新分配的內(nèi)存頁加入到 page cache 中,并增加頁引用計(jì)數(shù)。

隨后會(huì)通過 address_space_operations 中定義的 readpage 激活塊設(shè)備驅(qū)動(dòng)從磁盤中讀取映射的文件內(nèi)容,然后將讀取到的內(nèi)容填充新分配的內(nèi)存頁中。并同步預(yù)讀若干相鄰的文件頁到 page cache 中。

static const struct address_space_operations ext4_aops = {
    .readpage       = ext4_readpage
}
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    int error;
    // 獲取映射文件
    struct file *file = vmf->vma->vm_file;
    // 獲取 page cache
    struct address_space *mapping = file->f_mapping;    
    // 獲取映射文件的 inode
    struct inode *inode = mapping->host;
    // 獲取映射文件內(nèi)容在文件中的偏移
    pgoff_t offset = vmf->pgoff;
    // 從 page cache 讀取到的文件頁,存放在 vmf->page 中返回
    struct page *page;
    vm_fault_t ret = 0;

    // 根據(jù)文件偏移 offset,到 page cache 中查找對(duì)應(yīng)的文件頁
    page = find_get_page(mapping, offset);
    if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
        // 如果文件頁在 page cache 中,則啟動(dòng)異步預(yù)讀,預(yù)讀后面的若干文件頁到 page cache 中
        fpin = do_async_mmap_readahead(vmf, page);
    } else if (!page) {
        // 如果文件頁不在 page cache,那么就需要啟動(dòng) io 從文件中讀取內(nèi)容到 page cahe
        // 由于涉及到了磁盤 io ,所以本次缺頁類型為 VM_FAULT_MAJOR
        count_vm_event(PGMAJFAULT);
        count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
        ret = VM_FAULT_MAJOR;
        // 啟動(dòng)同步預(yù)讀,將所需的文件數(shù)據(jù)讀取進(jìn) page cache 中并同步預(yù)讀若干相鄰的文件數(shù)據(jù)到 page cache 
        fpin = do_sync_mmap_readahead(vmf);
retry_find:
        // 嘗試到 page cache 中重新讀取文件頁,這一次就可以讀到了
        page = pagecache_get_page(mapping, offset,
                      FGP_CREAT|FGP_FOR_MMAP,
                      vmf->gfp_mask);
        }
    }

    ..... 省略 ......
}
EXPORT_SYMBOL(filemap_fault);

文件頁現(xiàn)在有了,接下來內(nèi)核就會(huì)調(diào)用 finish_fault 將文件頁映射到 pte 中。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

vm_fault_t finish_fault(struct vm_fault *vmf)
{
    // 為本次缺頁準(zhǔn)備好的物理內(nèi)存頁,即后續(xù)需要用 pte 映射的內(nèi)存頁
    struct page *page;
    vm_fault_t ret = 0;

    if ((vmf->flags & FAULT_FLAG_WRITE) &&
        !(vmf->vma->vm_flags & VM_SHARED))
        // 如果是寫時(shí)復(fù)制場(chǎng)景,那么 pte 要映射的是這個(gè) cow 復(fù)制過來的內(nèi)存頁
        page = vmf->cow_page;
    else
        // 在 filemap_fault 函數(shù)中讀取到的文件頁,后面需要將文件頁映射到 pte 中
        page = vmf->page;

    // 對(duì)于私有映射來說,這里需要檢查進(jìn)程地址空間是否被標(biāo)記了 MMF_UNSTABLE
    // 如果是,那么 oom 后續(xù)會(huì)回收這塊地址空間,這會(huì)導(dǎo)致私有映射的文件頁丟失
    // 所以在為私有映射建立 pte 映射之前,需要檢查一下
    if (!(vmf->vma->vm_flags & VM_SHARED))
        // 地址空間沒有被標(biāo)記 MMF_UNSTABLE 則會(huì)返回 o
        ret = check_stable_address_space(vmf->vma->vm_mm);
    if (!ret)
        // 將創(chuàng)建出來的物理內(nèi)存頁映射到 address 對(duì)應(yīng)在頁表中的 pte 中
        ret = alloc_set_pte(vmf, vmf->memcg, page);
    if (vmf->pte)
        // 釋放頁表鎖
        pte_unmap_unlock(vmf->pte, vmf->ptl);
    return ret;
}

alloc_set_pte 將之前我們準(zhǔn)備好的文件頁,映射到缺頁地址 address 在進(jìn)程頁表對(duì)應(yīng)的 pte 中。

vm_fault_t alloc_set_pte(struct vm_fault *vmf, struct mem_cgroup *memcg,
        struct page *page)
{
    struct vm_area_struct *vma = vmf->vma;
    // 判斷本次缺頁是否是 寫時(shí)復(fù)制
    bool write = vmf->flags & FAULT_FLAG_WRITE;
    pte_t entry;
    vm_fault_t ret;
    // 如果頁表還不存在,需要先創(chuàng)建一個(gè)頁表出來
    if (!vmf->pte) {
        // 如果 pmd 為空,則創(chuàng)建一個(gè)頁表出來,并填充 pmd
        // 如果頁表存在,則獲取 address 在頁表中對(duì)應(yīng)的 pte 保存在 vmf->pte 中
        ret = pte_alloc_one_map(vmf);
        if (ret)
            return ret;
    }
    // 根據(jù)之前分配出來的內(nèi)存頁 pfn 以及相關(guān)頁屬性 vma->vm_page_prot 構(gòu)造一個(gè) pte 出來
    // 對(duì)于私有文件映射來說,這里的 pte 是只讀的
    entry = mk_pte(page, vma->vm_page_prot);
    // 如果是寫時(shí)復(fù)制,這里才會(huì)將 pte 改為可寫的
    if (write) 
        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
    // 將構(gòu)造出來的 pte (entry)賦值給 address 在頁表中真正對(duì)應(yīng)的 vmf->pte
    // 現(xiàn)在進(jìn)程頁表體系就全部被構(gòu)建出來了,文件頁缺頁處理到此結(jié)束
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    // 刷新 mmu
    update_mmu_cache(vma, vmf->address, vmf->pte);

    return 0;
}

8.2 do_cow_fault 處理私有文件映射的寫時(shí)復(fù)制

上小節(jié) do_read_fault 函數(shù)處理的場(chǎng)景是,進(jìn)程在調(diào)用 mmap 對(duì)文件進(jìn)行私有映射或者共享映射之后,立馬進(jìn)行讀取的缺頁場(chǎng)景。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

但是如果當(dāng)我們采用的是 mmap 進(jìn)行私有文件映射時(shí),在映射之后,立馬進(jìn)行寫入操作時(shí),就會(huì)發(fā)生寫時(shí)復(fù)制,寫時(shí)復(fù)制的缺頁處理流程內(nèi)核封裝在 do_cow_fault 函數(shù)中。

由于我們這里要進(jìn)行寫時(shí)復(fù)制,所以首先要調(diào)用 alloc_page_vma 從伙伴系統(tǒng)中重新申請(qǐng)一個(gè)物理內(nèi)存頁出來,我們先把這個(gè)剛剛新申請(qǐng)出來用于寫時(shí)復(fù)制的內(nèi)存頁稱為 cow_page

然后調(diào)用上小節(jié)中介紹的 __do_fault 函數(shù),將原來的文件頁從 page cache 中讀取出來,我們把原來的文件頁稱為 page 。

最后調(diào)用 copy_user_highpage 將原來文件頁 page 中的內(nèi)容拷貝到剛剛新申請(qǐng)的內(nèi)存頁 cow_page 中,完成寫時(shí)復(fù)制之后,接著調(diào)用 finish_fault 將 cow_page 映射到缺頁地址 address 在進(jìn)程頁表中的 pte 上。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

這樣一來,進(jìn)程的這段虛擬文件映射區(qū)就映射到了專屬的物理內(nèi)存頁 cow_page 上,而且內(nèi)容和原來文件頁 page 中的內(nèi)容一模一樣,進(jìn)程對(duì)各自虛擬內(nèi)存區(qū)的修改只能反應(yīng)到各自對(duì)應(yīng)的 cow_page上,而且各自的修改在進(jìn)程之間是互不可見的。

由于 cow_page 已經(jīng)脫離了 page cache,所以這些修改也都不會(huì)回寫到磁盤文件中,這就是私有文件映射的核心特點(diǎn)。

static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret;
    // 從伙伴系統(tǒng)重新申請(qǐng)一個(gè)用于寫時(shí)復(fù)制的物理內(nèi)存頁 cow_page
    vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
    // 從  page cache 讀取原來的文件頁
    ret = __do_fault(vmf);
    // 將原來文件頁中的內(nèi)容拷貝到 cow_page 中完成寫時(shí)復(fù)制
    copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
    // 將 cow_page 重新映射到缺頁地址 address 對(duì)應(yīng)在頁表中的 pte 上。
    ret |= finish_fault(vmf);
    unlock_page(vmf->page);
    // 原來的文件頁引用計(jì)數(shù) - 1
    put_page(vmf->page);
    return ret;
}

8.3 do_shared_fault 處理對(duì)共享文件映射區(qū)寫入引起的缺頁

上小節(jié)我們介紹的 do_cow_fault 函數(shù)處理的場(chǎng)景是,當(dāng)我們采用 mmap 進(jìn)行私有文件映射之后,立即對(duì)虛擬映射區(qū)進(jìn)行寫入操作之后的缺頁處理邏輯。

如果我們調(diào)用 mmap 對(duì)文件進(jìn)行共享文件映射之后,然后立即對(duì)虛擬映射區(qū)進(jìn)行寫入操作,這背后的缺頁處理邏輯又是怎樣的呢 ?

其實(shí)和之前的文件缺頁處理邏輯的核心流程都差不多,不同的是由于這里我們進(jìn)行的共享文件映射,所以多個(gè)進(jìn)程中的虛擬文件映射區(qū)都會(huì)映射到 page cache 中的文件頁上,由于沒有寫時(shí)復(fù)制,所以進(jìn)程對(duì)文件頁的修改都會(huì)直接反映到 page cache 中,近而后續(xù)會(huì)回寫到磁盤文件上。

由于共享文件映射涉及到臟頁回寫,所以在共享文件映射的缺頁處理場(chǎng)景中,為了防止數(shù)據(jù)的丟失會(huì)額外有一些文件系統(tǒng)日志的記錄工作。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret, tmp;
    // 從 page cache 中讀取文件頁
    ret = __do_fault(vmf);
   
    if (vma->vm_ops->page_mkwrite) {
        unlock_page(vmf->page);
        // 將文件頁變?yōu)榭蓪憼顟B(tài),并為后續(xù)記錄文件日志做一些準(zhǔn)備工作
        tmp = do_page_mkwrite(vmf);
    }

    // 將文件頁映射到缺頁 address 在頁表中對(duì)應(yīng)的 pte 上
    ret |= finish_fault(vmf);

    // 將 page 標(biāo)記為臟頁,記錄相關(guān)文件系統(tǒng)的日志,防止數(shù)據(jù)丟失
    // 判斷是否將臟頁回寫
    fault_dirty_shared_page(vma, vmf->page);
    return ret;
}

9. do_wp_page 進(jìn)行寫時(shí)復(fù)制

本小節(jié)即將要介紹的 do_wp_page 函數(shù)和之前介紹的 do_cow_fault 函數(shù)都是用于處理寫時(shí)復(fù)制的,其最為核心的邏輯都是差不多的,只是在觸發(fā)場(chǎng)景上會(huì)略有不同。

do_cow_fault 函數(shù)主要處理的寫時(shí)復(fù)制場(chǎng)景是,當(dāng)我們使用 mmap 進(jìn)行私有文件映射時(shí),在剛映射完之后,此時(shí)進(jìn)程的頁表或者相關(guān)頁表項(xiàng) pte 還是空的,就立即進(jìn)行寫入操作。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

do_wp_page 函數(shù)主要處理的寫時(shí)復(fù)制場(chǎng)景是,訪問的這塊虛擬內(nèi)存背后是有物理內(nèi)存頁映射的,對(duì)應(yīng)的 pte 不為空,只不過相關(guān) pte 的權(quán)限是只讀的,而虛擬內(nèi)存區(qū)域 vma 是有寫權(quán)限的,在這種類型的虛擬內(nèi)存進(jìn)行寫入操作的時(shí)候,觸發(fā)的寫時(shí)復(fù)制就在 do_wp_page 函數(shù)中處理。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

比如,我們使用 mmap 進(jìn)行私有文件映射之后,此時(shí)只是分配了虛擬內(nèi)存,進(jìn)程頁表或者相關(guān) pte 還是空的,這時(shí)對(duì)這塊映射的虛擬內(nèi)存進(jìn)行訪問的時(shí)候就會(huì)觸發(fā)缺頁中斷,最后在之前介紹的 do_read_fault 函數(shù)中將映射的文件內(nèi)容加載到 page cache 中,pte 指向 page cache 中的文件頁。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

但此時(shí)的 pte 是只讀的,如果我們對(duì)這塊映射的虛擬內(nèi)存進(jìn)行寫入操作,就會(huì)發(fā)生寫時(shí)復(fù)制,由于現(xiàn)在 pte 不為空,背后也映射著文件頁,所以會(huì)在 do_wp_page 函數(shù)中進(jìn)行處理。

除了私有映射的文件頁之外,do_wp_page 還會(huì)對(duì)匿名頁相關(guān)的寫時(shí)復(fù)制進(jìn)行處理。

比如,我們通過 fork 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程的時(shí)候,內(nèi)核會(huì)拷貝父進(jìn)程占用的所有資源到子進(jìn)程中,其中也包括了父進(jìn)程的地址空間以及父進(jìn)程的頁表。

一個(gè)進(jìn)程中申請(qǐng)的物理內(nèi)存頁既會(huì)有文件頁也會(huì)有匿名頁,而這些文件頁和匿名頁既可以是私有的也可以是共享的,當(dāng)內(nèi)核在拷貝父進(jìn)程的頁表時(shí),如果遇到私有的匿名頁或者文件頁,就會(huì)將其對(duì)應(yīng)在父子進(jìn)程頁表中的 pte 設(shè)置為只讀,進(jìn)行寫保護(hù)。并將父子進(jìn)程共同引用的匿名頁或者文件頁的引用計(jì)數(shù)加 1。

static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
        pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
        unsigned long addr, int *rss)
{
    /*
     * If it's a COW mapping, write protect it both
     * in the parent and the child
     */
    if (is_cow_mapping(vm_flags) && pte_write(pte)) {
        // 設(shè)置父進(jìn)程的 pte 為只讀
        ptep_set_wrprotect(src_mm, addr, src_pte);
        // 設(shè)置子進(jìn)程的 pte 為只讀
        pte = pte_wrprotect(pte);
    }
    // 獲取 pte 中映射的物理內(nèi)存頁(此時(shí)父子進(jìn)程共享該頁)
    page = vm_normal_page(vma, addr, pte);
    // 物理內(nèi)存頁的引用技術(shù) + 1
    get_page(page);
}

static inline bool is_cow_mapping(vm_flags_t flags)
{
        // vma 是私有可寫的
	return (flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE;
}

現(xiàn)在父子進(jìn)程擁有了一模一樣的地址空間,頁表是一樣的,頁表中的 pte 均指向同一個(gè)物理內(nèi)存頁面,對(duì)于私有的物理內(nèi)存頁來說,父子進(jìn)程的相關(guān) pte 此時(shí)均變?yōu)榱酥蛔x的,私有物理內(nèi)存頁的引用計(jì)數(shù)為 2 。而對(duì)于共享的物理內(nèi)存頁來說,內(nèi)核就只是簡單的將父進(jìn)程的 pte 拷貝到子進(jìn)程頁表中即可,然后將子進(jìn)程 pte 中的臟頁標(biāo)記清除,其他的不做改變。

當(dāng)父進(jìn)程或者子進(jìn)程對(duì)該頁面發(fā)生寫操作的時(shí)候,我們現(xiàn)在假設(shè)子進(jìn)程先對(duì)頁面發(fā)生寫操作,隨后子進(jìn)程發(fā)現(xiàn)自己頁表中的 pte 是只讀的,于是就會(huì)產(chǎn)生寫保護(hù)類型的缺頁中斷,由于子進(jìn)程頁表中的 pte 不為空,所以會(huì)進(jìn)入到 do_wp_page 函數(shù)中處理。

由于現(xiàn)在子進(jìn)程和父子進(jìn)程頁表中的相關(guān) pte 指向的均是同一個(gè)物理內(nèi)存頁,內(nèi)核在 do_wp_page 函數(shù)中會(huì)發(fā)現(xiàn)這個(gè)物理內(nèi)存頁的引用計(jì)數(shù)大于 1,存在多進(jìn)程共享的情況,所以就會(huì)觸發(fā)寫時(shí)復(fù)制,這一過程在 wp_page_copy 函數(shù)中處理。

在 wp_page_copy 函數(shù)中,內(nèi)核會(huì)首先為子進(jìn)程分配一個(gè)新的物理內(nèi)存頁 new_page,然后調(diào)用 cow_user_page 將原有內(nèi)存頁 old_page 中的內(nèi)容全部拷貝到新內(nèi)存頁中。

創(chuàng)建一個(gè)臨時(shí)的頁表項(xiàng) entry,然后讓 entry 指向新的內(nèi)存頁,將 entry 重新設(shè)置為可寫,通過 set_pte_at_notify 將 entry 值設(shè)置到子進(jìn)程頁表中的 pte 上。最后將原有內(nèi)存頁 old_page 的引用計(jì)數(shù)減 1 。

static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
    // 缺頁地址 address 所在 vma
    struct vm_area_struct *vma = vmf->vma;
    // 當(dāng)前進(jìn)程地址空間
    struct mm_struct *mm = vma->vm_mm;
    // 原來映射的物理內(nèi)存頁,pte 為只讀
    struct page *old_page = vmf->page;
    // 用于寫時(shí)復(fù)制的新內(nèi)存頁
    struct page *new_page = NULL;
    // 寫時(shí)復(fù)制之后,需要修改原來的 pte,這里是臨時(shí)構(gòu)造的一個(gè) pte 值
    pte_t entry;
    // 是否發(fā)生寫時(shí)復(fù)制
    int page_copied = 0;

    // 如果 pte 原來映射的是一個(gè)零頁
    if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {
        // 新申請(qǐng)一個(gè)零頁出來,內(nèi)存頁中的內(nèi)容被零初始化
        new_page = alloc_zeroed_user_highpage_movable(vma,
                                  vmf->address);
        if (!new_page)
            goto oom;
    } else {
        // 新申請(qǐng)一個(gè)物理內(nèi)存頁
        new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
                vmf->address);
        if (!new_page)
            goto oom;
        // 將原來內(nèi)存頁 old page 中的內(nèi)容拷貝到新內(nèi)存頁 new page 中
        cow_user_page(new_page, old_page, vmf->address, vma);
    }

    // 給頁表加鎖,并重新獲取 address 在頁表中對(duì)應(yīng)的 pte
    vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
    // 判斷加鎖前的 pte (orig_pte)與加鎖后的 pte (vmf->pte)是否相同
    // 目的是判斷此時(shí)是否有其他線程正在并發(fā)修改 pte
    if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {
        if (old_page) {
            // 更新進(jìn)程常駐內(nèi)存信息 rss_state
            if (!PageAnon(old_page)) {
                // 減少 MM_FILEPAGES 計(jì)數(shù)
                dec_mm_counter_fast(mm,
                        mm_counter_file(old_page));
                // 由于發(fā)生寫時(shí)復(fù)制,這里匿名頁個(gè)數(shù)加 1 
                inc_mm_counter_fast(mm, MM_ANONPAGES);
            }
        } else {
            inc_mm_counter_fast(mm, MM_ANONPAGES);
        }
        // 將舊的 tlb 緩存刷出
        flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
        // 創(chuàng)建一個(gè)臨時(shí)的 pte 映射到新內(nèi)存頁 new page 上
        entry = mk_pte(new_page, vma->vm_page_prot);
        // 設(shè)置 entry 為可寫的,正是這里, pte 的權(quán)限由只讀變?yōu)榱丝蓪?        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
        // 為新的內(nèi)存頁建立反向映射關(guān)系
        page_add_new_anon_rmap(new_page, vma, vmf->address, false);
        // 將新的內(nèi)存頁加入到 LRU active 鏈表中
        lru_cache_add_active_or_unevictable(new_page, vma);
        // 將 entry 值重新設(shè)置到子進(jìn)程頁表 pte 中
        set_pte_at_notify(mm, vmf->address, vmf->pte, entry);
        // 更新 mmu
        update_mmu_cache(vma, vmf->address, vmf->pte);
        if (old_page) {
            // 將原來的內(nèi)存頁從當(dāng)前進(jìn)程的反向映射關(guān)系中解除
            page_remove_rmap(old_page, false);
        }

        /* Free the old page.. */
        new_page = old_page;
        page_copied = 1;
    } else {
        mem_cgroup_cancel_charge(new_page, memcg, false);
    }
    // 釋放頁表鎖
    pte_unmap_unlock(vmf->pte, vmf->ptl);

    if (old_page) {
        // 舊內(nèi)存頁的引用計(jì)數(shù)減 1
        put_page(old_page);
    }
    return page_copied ? VM_FAULT_WRITE : 0;
}

現(xiàn)在子進(jìn)程處理完了,下面我們?cè)賮砜串?dāng)父進(jìn)程發(fā)生寫入操作的時(shí)候會(huì)發(fā)生什么 ?

首先和子進(jìn)程一樣,現(xiàn)在父進(jìn)程頁表中的相關(guān) pte 仍然是只讀的,訪問這段虛擬內(nèi)存地址依然會(huì)產(chǎn)生寫保護(hù)類型的缺頁中斷,和子進(jìn)程不同的是,此時(shí)父進(jìn)程 pte 中指向的原有物理內(nèi)存頁 old_page 的引用計(jì)數(shù)已經(jīng)變?yōu)?1 了,說明父進(jìn)程是獨(dú)占的,復(fù)用原來的 old_page 即可,不必進(jìn)行寫時(shí)復(fù)制,只是簡單的將父進(jìn)程頁表中的相關(guān) pte 改為可寫就行了。

static inline void wp_page_reuse(struct vm_fault *vmf)
    __releases(vmf->ptl)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page = vmf->page;
    pte_t entry;
    // 先將 tlb cache 中緩存的 address 對(duì)應(yīng)的 pte 刷出緩存
    flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
    // 將原來 pte 的 access 位置 1 ,表示該 pte 映射的物理內(nèi)存頁是活躍的
    entry = pte_mkyoung(vmf->orig_pte);
    // 將原來只讀的 pte 改為可寫的,并標(biāo)記為臟頁
    entry = maybe_mkwrite(pte_mkdirty(entry), vma);
    // 將更新后的 entry 值設(shè)置到頁表 pte 中
    if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
        // 更新 mmu 
        update_mmu_cache(vma, vmf->address, vmf->pte);
    pte_unmap_unlock(vmf->pte, vmf->ptl);
}

理解了上面的核心內(nèi)容,我們?cè)賮砜?do_wp_page 的處理邏輯就很清晰了:

static vm_fault_t do_wp_page(struct vm_fault *vmf)
    __releases(vmf->ptl)
{
    struct vm_area_struct *vma = vmf->vma;
    // 獲取 pte 映射的物理內(nèi)存頁
    vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);

         ...... 省略處理特殊映射相關(guān)邏輯 ....
    // 物理內(nèi)存頁為匿名頁的情況
    if (PageAnon(vmf->page)) {

         ...... 省略處理 ksm page 相關(guān)邏輯 ....
        // reuse_swap_page 判斷匿名頁的引用計(jì)數(shù)是否為 1
        if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
            // 如果當(dāng)前物理內(nèi)存頁的引用計(jì)數(shù)為 1 ,并且只有當(dāng)前進(jìn)程在引用該物理內(nèi)存頁
            // 則不做寫時(shí)復(fù)制處理,而是復(fù)用當(dāng)前物理內(nèi)存頁,只是將 pte 改為可寫即可 
            wp_page_reuse(vmf);
            return VM_FAULT_WRITE;
        }
        unlock_page(vmf->page);
    } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                    (VM_WRITE|VM_SHARED))) {
        // 處理共享可寫的內(nèi)存頁
        // 由于大家都可寫,所以這里也只是調(diào)用 wp_page_reuse 復(fù)用當(dāng)前內(nèi)存頁即可,不做寫時(shí)復(fù)制處理
        // 由于是共享的,對(duì)于文件頁來說是可以回寫到磁盤上的,所以會(huì)額外調(diào)用一次 fault_dirty_shared_page 判斷是否進(jìn)行臟頁的回寫
        return wp_page_shared(vmf);
    }
copy:
    // 走到這里表示當(dāng)前物理內(nèi)存頁的引用計(jì)數(shù)大于 1 被多個(gè)進(jìn)程引用
    // 對(duì)于私有可寫的虛擬內(nèi)存區(qū)域來說,就要發(fā)生寫時(shí)復(fù)制
    // 而對(duì)于私有文件頁的情況來說,不必判斷內(nèi)存頁的引用計(jì)數(shù)
    // 因?yàn)槭撬接形募?,不管文件頁的引用?jì)數(shù)是不是 1 ,都要進(jìn)行寫時(shí)復(fù)制
    return wp_page_copy(vmf);
}

10. do_swap_page 處理 swap 缺頁異常

如果在遍歷進(jìn)程頁表的時(shí)候發(fā)現(xiàn),虛擬內(nèi)存地址 address 對(duì)應(yīng)的頁表項(xiàng) pte 不為空,但是 pte 中第 0 個(gè)比特位置為 0 ,則表示該 pte 之前是被物理內(nèi)存映射過的,只不過后來被內(nèi)核 swap out 出去了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

我們需要的物理內(nèi)存頁不在內(nèi)存中反而在磁盤中,現(xiàn)在我們就需要將物理內(nèi)存頁從磁盤中 swap in 進(jìn)來。但在 swap in 之前內(nèi)核需要知道該物理內(nèi)存頁的內(nèi)容被保存在磁盤的什么位置上。

筆者在之前文章《一步一圖帶你構(gòu)建 Linux 頁表體系》?中的第 4.2.1 小節(jié)中詳細(xì)介紹了 64 位頁表項(xiàng) pte 的比特位布局,以及各個(gè)比特位的含義。

typedef unsigned long   pteval_t;
typedef struct { pteval_t pte; } pte_t;

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

64 位的 pte 主要用來表示物理內(nèi)存頁的地址以及相關(guān)的權(quán)限標(biāo)識(shí)位,但是當(dāng)物理內(nèi)存頁不在內(nèi)存中的時(shí)候,這些比特位就沒有了任何意義。我們何不將這些已經(jīng)沒有任何意義的比特位利用起來,在物理內(nèi)存頁被 swap out 到磁盤上的時(shí)候,將物理內(nèi)存頁在磁盤上的位置保存在這些比特位中。本質(zhì)上還利用的是之前 pte 中的那 64 個(gè)比特,為了區(qū)別 swap 的場(chǎng)景,內(nèi)核使用了一個(gè)新的結(jié)構(gòu)體 swp_entry_t 來包裝。

typedef struct {
	unsigned long val;
} swp_entry_t;

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

swap in 的首要任務(wù)就是先要從進(jìn)程頁表中將這個(gè) swp_entry_t 讀取出來,然后從 swp_entry_t 中解析出內(nèi)存頁在 swap 交換區(qū)中的位置,根據(jù)磁盤位置信息將內(nèi)存頁的內(nèi)容讀取到內(nèi)存中。由于產(chǎn)生了新的物理內(nèi)存頁,所以就要?jiǎng)?chuàng)建新的 pte 來映射這個(gè)物理內(nèi)存頁,然后將新的 pte 設(shè)置到頁表中,替換原來的 swp_entry_t。

這里筆者需要為大家解釋的第一個(gè)問題就是 —— 這個(gè) swp_entry_t 究竟是長什么樣子 的,它是如何保存 swap 交換區(qū)相關(guān)位置信息的 ?

10.1 交換區(qū)的布局及其組織結(jié)構(gòu)

要明白這個(gè),我們就需要先了解一下 swap 交換區(qū)(swap area)的布局,swap 交換區(qū)共有兩種類型,一種是 swap 分區(qū)(swap partition),另一種是 swap 文件(swap file)。

swap partition 可以認(rèn)為是一個(gè)沒有文件系統(tǒng)的裸磁盤分區(qū),分區(qū)中的磁盤塊在磁盤中是連續(xù)分布的。

swap file 可以認(rèn)為是在某個(gè)現(xiàn)有的文件系統(tǒng)上,創(chuàng)建的一個(gè)定長的普通文件,專門用于保存匿名頁被 swap 出來的內(nèi)容。背后的磁盤塊是不連續(xù)的。

Linux 系統(tǒng)中可以允許多個(gè)這樣的 swap 交換區(qū)存在,我們可以同時(shí)使用多個(gè)交換區(qū),也可以為這些交換區(qū)指定優(yōu)先級(jí),優(yōu)先級(jí)高的會(huì)被內(nèi)核優(yōu)先使用。這些交換區(qū)都可以被靈活地添加,刪除,而不需要重啟系統(tǒng)。多個(gè)交換區(qū)可以分散在不同的磁盤設(shè)備上,這樣可以實(shí)現(xiàn)硬件的并行訪問。

在使用交換區(qū)之前,我們可以通過 mkswap 首先創(chuàng)建一個(gè)交換區(qū)出來,如果我們創(chuàng)建的是 swap partition,則在 mkswap 命令后面直接指定分區(qū)的設(shè)備文件名稱即可。

mkswap /dev/sdb7

如果我們創(chuàng)建的是 swap file,則需要額外先使用 dd 命令在現(xiàn)有文件系統(tǒng)中創(chuàng)建出一個(gè)定長的文件出來。比如下面通過 dd 命令從 /dev/zero 中拷貝創(chuàng)建一個(gè) /swapfile 文件,大小為 4G。

dd if=/dev/zero of=/swapfile bs=1M count=4096

然后使用 mkswap 命令創(chuàng)建 swap file :

mkswap /swapfile

當(dāng) swap partition 或者 swap file 創(chuàng)建好之后,我們通過 swapon 命令來初始化并激活這個(gè)交換區(qū)。

swapon /swapfile

當(dāng)前系統(tǒng)中各個(gè)交換區(qū)的情況,我們可以通過 cat /proc/swaps 或者 swapon -s 命令產(chǎn)看:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

交換區(qū)在內(nèi)核中使用 struct swap_info_struct 結(jié)構(gòu)體來表示,系統(tǒng)中眾多的交換區(qū)被組織在一個(gè)叫做 swap_info 的數(shù)組中,數(shù)組中的最大長度為 MAX_SWAPFILES,MAX_SWAPFILES 在內(nèi)核中是一個(gè)常量,一般指定為 32,也就是說,系統(tǒng)中最大允許 32 個(gè)交換區(qū)存在。

struct swap_info_struct *swap_info[MAX_SWAPFILES];

由于交換區(qū)是有優(yōu)先級(jí)的,所以內(nèi)核又會(huì)按照優(yōu)先級(jí)高低,將交換區(qū)組織在一個(gè)叫做 swap_avail_heads 的雙向鏈表中。

static struct plist_head *swap_avail_heads;

swap_info_struct 結(jié)構(gòu)用于描述單個(gè)交換區(qū)中的各種信息:

/*
 * The in-memory structure used to track swap areas.
 */
struct swap_info_struct {
    // 用于表示該交換區(qū)的狀態(tài),比如 SWP_USED 表示正在使用狀態(tài),SWP_WRITEOK 表示交換區(qū)是可寫的狀態(tài)
    unsigned long   flags;      /* SWP_USED etc: see above */
    // 交換區(qū)的優(yōu)先級(jí)
    signed short    prio;       /* swap priority of this type */
    // 指向該交換區(qū)在 swap_avail_heads 鏈表中的位置
    struct plist_node list;     /* entry in swap_active_head */
    // 該交換區(qū)在 swap_info 數(shù)組中的索引
    signed char type;       /* strange name for an index */
    // 該交換區(qū)可以容納 swap 的匿名頁總數(shù)
    unsigned int pages;     /* total of usable pages of swap */
    // 已經(jīng) swap 到該交換區(qū)的匿名頁總數(shù)
    unsigned int inuse_pages;   /* number of those currently in use */
    // 如果該交換區(qū)是 swap partition 則指向該磁盤分區(qū)的塊設(shè)備結(jié)構(gòu) block_device
    // 如果該交換區(qū)是 swap file 則指向文件底層依賴的塊設(shè)備結(jié)構(gòu) block_device
    struct block_device *bdev;  /* swap device or bdev of swap file */
    // 指向 swap file 的 file 結(jié)構(gòu)
    struct file *swap_file;     /* seldom referenced */
};

而在每個(gè)交換區(qū) swap area 內(nèi)部又會(huì)分為很多連續(xù)的 slot (槽),每個(gè) slot 的大小剛好和一個(gè)物理內(nèi)存頁的大小相同都是 4K,物理內(nèi)存頁在被 swap out 到交換區(qū)時(shí),就會(huì)存放在 slot 中。

交換區(qū)中的這些 slot 會(huì)被組織在一個(gè)叫做 swap_map 的數(shù)組中,數(shù)組中的索引就是 slot 在交換區(qū)中的 offset (這個(gè)位置信息很重要),數(shù)組中的值表示該 slot 總共被多少個(gè)進(jìn)程同時(shí)引用。

什么意思呢 ? 比如現(xiàn)在系統(tǒng)中一共有三個(gè)進(jìn)程同時(shí)共享一個(gè)物理內(nèi)存頁(內(nèi)存中的概念),當(dāng)這個(gè)物理內(nèi)存頁被 swap out 到交換區(qū)上時(shí),就變成了 slot (內(nèi)存頁在交換區(qū)中的概念),現(xiàn)在物理內(nèi)存頁沒了,這三個(gè)共享進(jìn)程就只能在各自的頁表中指向這個(gè) slot,因此該 slot 的引用計(jì)數(shù)就是 3,對(duì)應(yīng)在數(shù)組 swap_map 中的值也是 3 。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

交換區(qū)中的第一個(gè) slot 用于存儲(chǔ)交換區(qū)的元信息,比如交換區(qū)對(duì)應(yīng)底層各個(gè)磁盤塊的壞塊列表。因此筆者將其標(biāo)注了紅色,表示不能使用。

swap_map 數(shù)組中的值表示的就是對(duì)應(yīng) slot 被多少個(gè)進(jìn)程同時(shí)引用,值為 0 表示該 slot 是空閑的,下次 swap out 的時(shí)候首先查找的就是空閑 slot 。 查找范圍就是 lowest_bit 到 highest_bit 之間的 slot。當(dāng)查找到空閑 slot 之后,就會(huì)將整個(gè)物理內(nèi)存頁回寫到這個(gè) slot 中。

struct swap_info_struct {
	unsigned char *swap_map;	/* vmalloc'ed array of usage counts */
	unsigned int lowest_bit;	/* index of first free in swap_map */
	unsigned int highest_bit;	/* index of last free in swap_map */

但是這里會(huì)有一個(gè)問題就是交換區(qū)面向的是整個(gè)系統(tǒng),而系統(tǒng)中會(huì)有很多進(jìn)程,如果多個(gè)進(jìn)程并發(fā)進(jìn)行 swap 的時(shí)候,swap_map 數(shù)組就會(huì)面臨并發(fā)操作的問題,這樣一來就不得不需要一個(gè)全局鎖來保護(hù),但是這也導(dǎo)致了多個(gè) CPU 只能串行訪問,大大降低了并發(fā)度。

那怎么辦呢 ? 想想 JDK 中的 ConcurrentHashMap,將鎖分段唄,這樣可以將鎖競(jìng)爭分散開來,大大提升并發(fā)度。

內(nèi)核會(huì)將 swap_map 數(shù)組中的這些 slot,按照常量 SWAPFILE_CLUSTER 指定的個(gè)數(shù),256 個(gè) slot 分為一個(gè) cluster。

#define SWAPFILE_CLUSTER	256

每個(gè) cluster 中包含一把 spinlock_t 鎖,如果 cluster 是空閑的,那么 swap_cluster_info 結(jié)構(gòu)中的 data 指向下一個(gè)空閑的 cluster,如果 cluster 不是空閑的,那么 data 保存的是該 cluster 中已經(jīng)分配的 slot 個(gè)數(shù)。

struct swap_cluster_info {
    spinlock_t lock;    /*
                 * Protect swap_cluster_info fields
                 * and swap_info_struct->swap_map
                 * elements correspond to the swap
                 * cluster
                 */
    unsigned int data:24;
    unsigned int flags:8;
};
#define CLUSTER_FLAG_FREE 1 /* This cluster is free */
#define CLUSTER_FLAG_NEXT_NULL 2 /* This cluster has no next cluster */
#define CLUSTER_FLAG_HUGE 4 /* This cluster is backing a transparent huge page */

這樣一來 swap_map 數(shù)組中的這些獨(dú)立的 slot,就被按照以 cluster 為單位重新組織了起來,這些 cluster 被串聯(lián)在 cluster_info 鏈表中。

為了進(jìn)一步利用 cpu cache,以及實(shí)現(xiàn)無鎖化查找 slot,內(nèi)核會(huì)給每個(gè) cpu 分配一個(gè) cluster —— percpu_cluster,cpu 直接從自己的 cluster 中查找空閑 slot,近一步提高了 swap out 的吞吐。

當(dāng) cpu 自己的 percpu_cluster 用盡之后,內(nèi)核則會(huì)調(diào)用 swap_alloc_cluster 函數(shù)從 free_clusters 中獲取一個(gè)新的 cluster。

struct swap_info_struct {
    struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
    struct swap_cluster_list free_clusters; /* free clusters list */

    struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
}

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在交換區(qū)的整體布局筆者就為大家介紹完了,可能大家這里有一點(diǎn)還是會(huì)比較困惑 —— 你說來說去,這個(gè) slot 到底是個(gè)啥 ?

哈哈,大家先別急,我們現(xiàn)在已經(jīng)對(duì)進(jìn)程的虛擬內(nèi)存空間非常熟悉了,這里我們把交換區(qū) swap_info_struct 與進(jìn)程的內(nèi)存空間 mm_struct 放到一起一對(duì)比就很清楚了。

首先進(jìn)程虛擬內(nèi)存空間中的虛擬內(nèi)存別管說的如何天花亂墜,說到底還是要保存在真實(shí)的物理內(nèi)存中的,虛擬內(nèi)存與物理內(nèi)存通過頁表來關(guān)聯(lián)起來。

同樣的道理,別管交換區(qū)布局的如何天花亂墜,swap out 出來的數(shù)據(jù)說到底還是要保存在真實(shí)的磁盤中的,而交換區(qū)中是按照 slot 為單位進(jìn)行組織管理的,磁盤中是按照磁盤塊來組織管理的,大小都是 4K 。

交換區(qū)中的 slot 就好比于虛擬內(nèi)存空間中的虛擬內(nèi)存,都是虛擬的概念,物理內(nèi)存頁與磁盤塊才是真實(shí)本質(zhì)的東西。

虛擬內(nèi)存是連續(xù)的,但其背后映射的物理內(nèi)存可能是不連續(xù),交換區(qū)中的 slot 也都是連續(xù)的,但磁盤中磁盤塊的扇區(qū)地址卻不一定是連續(xù)的。頁表可以將不連續(xù)的物理內(nèi)存映射到連續(xù)的虛擬內(nèi)存上,內(nèi)核也需要一種機(jī)制,將不連續(xù)的磁盤塊映射到連續(xù)的 slot 中。

當(dāng)我們使用 swapon 命令來初始化激活交換區(qū)時(shí),內(nèi)核會(huì)掃描交換區(qū)中各個(gè)磁盤塊的扇區(qū)地址,以確定磁盤塊與扇區(qū)的對(duì)應(yīng)關(guān)系,然后搜集扇區(qū)地址連續(xù)的磁盤塊,將這些連續(xù)的磁盤塊組成一個(gè)塊組,slot 就會(huì)一個(gè)一個(gè)的映射到這些塊組上,塊組之間的扇區(qū)地址是不連續(xù)的,但是 slot 是連續(xù)的。

slot 與連續(xù)的磁盤塊組的映射關(guān)系保存在 swap_extent 結(jié)構(gòu)中:

/*
 * A swap extent maps a range of a swapfile's PAGE_SIZE pages onto a range of
 * disk blocks.  A list of swap extents maps the entire swapfile.  (Where the
 * term `swapfile' refers to either a blockdevice or an IS_REG file.  Apart
 * from setup, they're handled identically.
 *
 * We always assume that blocks are of size PAGE_SIZE.
 */
struct swap_extent {
    // 紅黑樹節(jié)點(diǎn)
    struct rb_node rb_node;
    // 塊組內(nèi),第一個(gè)映射的 slot 編號(hào)
    pgoff_t start_page;
    // 映射的 slot 個(gè)數(shù)
    pgoff_t nr_pages;
    // 塊組內(nèi)第一個(gè)磁盤塊
    sector_t start_block;
};

由于一個(gè)塊組內(nèi)的磁盤塊都是連續(xù)的,slot 本來又是連續(xù)的,所以 swap_extent 結(jié)構(gòu)中只需要保存映射到該塊組內(nèi)第一個(gè) slot 的編號(hào) (start_page),塊組內(nèi)第一個(gè)磁盤塊在磁盤上的塊號(hào),以及磁盤塊個(gè)數(shù)就可以了。

虛擬內(nèi)存頁類比 slot,物理內(nèi)存頁類比磁盤塊,這里的 swap_extent 可以看做是虛擬內(nèi)存區(qū)域 vma,進(jìn)程的虛擬內(nèi)存空間正是由一段一段的 vma 組成,這些 vma 被組織在一顆紅黑樹上。

交換區(qū)也是一樣,它是由一段一段的 swap_extent 組成,同樣也會(huì)被組織在一顆紅黑樹上。我們可以通過 slot 在交換區(qū)中的 offset,在這顆紅黑樹中快速查找出 slot 背后對(duì)應(yīng)的磁盤塊。

struct swap_info_struct {
	struct rb_root swap_extent_root;/* root of the swap extent rbtree */

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在交換區(qū)內(nèi)部的樣子,我們已經(jīng)非常清楚了,有了這些背景知識(shí)之后,我們?cè)诨剡^頭來看本小節(jié)最開始提出的問題 —— swp_entry_t 到底長什么樣子。

10.2 一睹 swp_entry_t 真容

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

匿名內(nèi)存頁在被內(nèi)核 swap out 到磁盤上之后,內(nèi)存頁中的內(nèi)容保存在交換區(qū)的 slot 中,在 swap in 的場(chǎng)景中,內(nèi)核需要根據(jù) swp_entry_t 里的信息找到這個(gè) slot,進(jìn)而找到其對(duì)應(yīng)的磁盤塊,然后從磁盤塊中讀取出被 swap out 出去的內(nèi)容。

這個(gè)就和交換區(qū)的布局有很大的關(guān)系,首先系統(tǒng)中存在多個(gè)交換區(qū),這些交換區(qū)被內(nèi)核組織在 swap_info 數(shù)組中。

struct swap_info_struct *swap_info[MAX_SWAPFILES];

我們首先需要知道匿名內(nèi)存頁到底被 swap out 到哪個(gè)交換區(qū)里了,所以 swp_entry_t 里必須包含交換區(qū)在 swap_info 數(shù)組中的索引,而這個(gè)索引正是 swap_info_struct 結(jié)構(gòu)中的 type 字段。

struct swap_info_struct {
    // 該交換區(qū)在 swap_info 數(shù)組中的索引
    signed char type;  
}

在確定了交換區(qū)的位置后,我們需要知道匿名頁被 swap out 到交換區(qū)中的哪個(gè) slot 中,所以 swp_entry_t 中也必須包含 slot 在交換區(qū)中的 offset,這個(gè) offset 就是 swap_info_struct 結(jié)構(gòu)里 slot 所在 swap_map 數(shù)組中的下標(biāo)。

struct swap_info_struct {
    unsigned char *swap_map; 
}

所以總結(jié)下來 swp_entry_t 中需要包含以下三種信息:

第一, swp_entry_t 需要標(biāo)識(shí)該頁表項(xiàng)是一個(gè) pte 還是 swp_entry_t,因?yàn)樗鼈z本質(zhì)上是一樣的,都是 unsigned long 類型的無符號(hào)整數(shù),是可以相互轉(zhuǎn)換的。

#define __pte_to_swp_entry(pte)	((swp_entry_t) { pte_val(pte) })
#define __swp_entry_to_pte(swp)	((pte_t) { (swp).val })

第 0 個(gè)比特位置 1 表示是一個(gè) pte,背后映射的物理內(nèi)存頁存在于內(nèi)存中。如果第 0 個(gè)比特位置 0 則表示該 pte 背后映射的物理內(nèi)存頁已經(jīng)被 swap out 出去了,那么它就是一個(gè) swp_entry_t,指向內(nèi)存頁在交換區(qū)中的位置。

第二,swp_entry_t 需要包含被 swap 出去的匿名頁所在交換區(qū)的索引 type,第 2 個(gè)比特位到第 7 個(gè)比特位,總共使用 6 個(gè)比特來表示匿名頁所在交換區(qū)的索引。

第三,swp_entry_t 需要包含匿名頁所在 slot 的位置 offset,第 8 個(gè)比特位到第 57 個(gè)比特位,總共 50 個(gè)比特來表示匿名頁對(duì)應(yīng)的 slot 在交換區(qū)的 offset 。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

/*
 * Encode and decode a swap entry:
 *	bits 0-1:	present (must be zero)
 *	bits 2-7:	swap type
 *	bits 8-57:	swap offset
 *	bit  58:	PTE_PROT_NONE (must be zero)
 */
#define __SWP_TYPE_SHIFT	2
#define __SWP_TYPE_BITS		6
#define __SWP_OFFSET_BITS	50
#define __SWP_OFFSET_SHIFT	(__SWP_TYPE_BITS + __SWP_TYPE_SHIFT)

內(nèi)核提供了宏 __swp_type 用于從 swp_entry_t 中將匿名頁所在交換區(qū)編號(hào)提取出來,還提供了宏 __swp_offset 用于從 swp_entry_t 中將匿名頁所在 slot 的 offset 提取出來。

#define __swp_type(x)		(((x).val >> __SWP_TYPE_SHIFT) & __SWP_TYPE_MASK)
#define __swp_offset(x)		(((x).val >> __SWP_OFFSET_SHIFT) & __SWP_OFFSET_MASK)

#define __SWP_TYPE_MASK		((1 << __SWP_TYPE_BITS) - 1)
#define __SWP_OFFSET_MASK	((1UL << __SWP_OFFSET_BITS) - 1)

有了這兩個(gè)宏之后,我們就可以根據(jù) swp_entry_t 輕松地定位到匿名頁在交換區(qū)中的位置了。

內(nèi)核首先會(huì)通過 swp_type 從 swp_entry_t 提取出匿名頁所在的交換區(qū)索引 type,根據(jù) type 就可以從 swap_info 數(shù)組中定位到交換區(qū)數(shù)據(jù)結(jié)構(gòu) swap_info_struct 。

內(nèi)核將定位交換區(qū) swap_info_struct 結(jié)構(gòu)的邏輯封裝在 swp_swap_info 函數(shù)中:

struct swap_info_struct *swp_swap_info(swp_entry_t entry)
{
	return swap_type_to_swap_info(swp_type(entry));
}

static struct swap_info_struct *swap_type_to_swap_info(int type)
{
	return READ_ONCE(swap_info[type]);
}

得到了交換區(qū)的 swap_info_struct 結(jié)構(gòu),我們就可以獲取交換區(qū)所在磁盤分區(qū)底層的塊設(shè)備 —— swap_info_struct->bdev。

struct swap_info_struct {
    // 如果該交換區(qū)是 swap partition 則指向該磁盤分區(qū)的塊設(shè)備結(jié)構(gòu) block_device
    // 如果該交換區(qū)是 swap file 則指向文件底層依賴的塊設(shè)備結(jié)構(gòu) block_device
    struct block_device *bdev;  /* swap device or bdev of swap file */
}

最后通過 swp_offset 定位匿名頁所在 slot 在交換區(qū)中的 offset, 然后利用 offset 在紅黑樹 swap_extent_root 中查找其對(duì)應(yīng)的 swap_extent。

struct swap_info_struct {
    struct rb_root swap_extent_root;/* root of the swap extent rbtree */
}

前面我們提到過 swap file 背后所在的磁盤塊不一定是連續(xù)的,而 swap file 中的 slot 卻是連續(xù)的,內(nèi)核需要用 swap_extent 結(jié)構(gòu)來描述 slot 與磁盤塊的映射關(guān)系。

所以對(duì)于 swap file 來說,我們找到了 swap_extent 也就確定了 slot 對(duì)應(yīng)的磁盤塊了。

static sector_t map_swap_entry(swp_entry_t entry, struct block_device **bdev)
{
    struct swap_info_struct *sis;
    struct swap_extent *se;
    pgoff_t offset;
    // 通過 swap_info[swp_type(entry)]  獲取交換區(qū) swap_info_struct 結(jié)構(gòu)
    sis = swp_swap_info(entry);
    // 獲取交換區(qū)所在磁盤分區(qū)塊設(shè)備
    *bdev = sis->bdev;
    // 獲取匿名頁在交換區(qū)的偏移 
    offset = swp_offset(entry);
    // 通過 offset 到紅黑樹 swap_extent_root 中查找對(duì)應(yīng)的 swap_extent
    se = offset_to_swap_extent(sis, offset);
    // 獲取 slot 對(duì)應(yīng)的磁盤塊
    return se->start_block + (offset - se->start_page);
}

而 swap partition 是一個(gè)沒有文件系統(tǒng)的裸磁盤分區(qū),其背后的磁盤塊都是連續(xù)分布的,所以對(duì)于 swap partition 來說,slot 與磁盤塊是直接映射的,我們獲取到 slot 的 offset 之后,在乘以一個(gè)固定的偏移 2 ^ PAGE_SHIFT - 9 跳過用于存儲(chǔ)交換區(qū)元信息的 swap header ,就可以直接獲得磁盤塊了。

這里有點(diǎn)像?《深入理解 Linux 虛擬內(nèi)存管理》?一文中提到的內(nèi)核虛擬內(nèi)存空間中的直接映射區(qū),虛擬內(nèi)存與物理內(nèi)存都是直接映射的,通過虛擬內(nèi)存地址減去一個(gè)固定的偏移直接就可以獲得物理內(nèi)存地址了。

static sector_t swap_page_sector(struct page *page)
{
    return (sector_t)__page_file_index(page) << (PAGE_SHIFT - 9);
}

pgoff_t __page_file_index(struct page *page)
{
    // 在 swap 場(chǎng)景中,swp_entry_t 的值會(huì)設(shè)置到 page 結(jié)構(gòu)中的 private 字段中
    // 具體什么時(shí)候設(shè)置的,我們這里先不管,后面會(huì)說
    swp_entry_t swap = { .val = page_private(page) };
    return swp_offset(swap);
}

以上介紹的就是內(nèi)核在 swap file 和 swap partition 場(chǎng)景下,如何獲取 slot 對(duì)應(yīng)的磁盤塊 sector_t 的邏輯與實(shí)現(xiàn)。

有了 sector_t,內(nèi)核接著就會(huì)利用 bdev_read_page 函數(shù)將 slot 對(duì)應(yīng)在 sector 中的內(nèi)容讀取到物理內(nèi)存頁 page 中,這就是整個(gè) swap in 的過程。

/**
 * bdev_read_page() - Start reading a page from a block device
 * @bdev: The device to read the page from
 * @sector: The offset on the device to read the page to (need not be aligned)
 * @page: The page to read
 */
int bdev_read_page(struct block_device *bdev, sector_t sector,
			struct page *page)

swap_readpage 函數(shù)負(fù)責(zé)將匿名頁中的內(nèi)容從交換區(qū)中讀取到物理內(nèi)存頁中來,這里也是 swap in 的核心實(shí)現(xiàn):

int swap_readpage(struct page *page, bool synchronous)
{
    struct bio *bio;
    int ret = 0;
    struct swap_info_struct *sis = page_swap_info(page);
    blk_qc_t qc;
    struct gendisk *disk;
    // 處理交換區(qū)是 swap file 的情況
    if (sis->flags & SWP_FS) {
        // 從交換區(qū)中獲取交換文件 swap_file
        struct file *swap_file = sis->swap_file;
        // swap_file 本質(zhì)上還是文件系統(tǒng)中的一個(gè)文件,所以它也會(huì)有 page cache
        struct address_space *mapping = swap_file->f_mapping;
        // 利用 page cache 中的 readpage 方法,從 swap_file 所在的文件系統(tǒng)中讀取匿名頁內(nèi)容到 page 中。
        // 注意這里只是利用 page cache 的 readpage 方法從文件系統(tǒng)中讀取數(shù)據(jù),內(nèi)核并不會(huì)把 page 加入到 page cache 中
        // 這里 swap_file 和普通文件的讀取過程是不一樣的,page cache 不緩存內(nèi)存頁。
        // 對(duì)于 swap out 的場(chǎng)景來說,內(nèi)核也只是利用 page cache 的 writepage 方法將匿名頁的內(nèi)容寫入到 swap_file 中。
        ret = mapping->a_ops->readpage(swap_file, page);
        if (!ret)
            count_vm_event(PSWPIN);
        return ret;
    }

    // 如果交換區(qū)是 swap partition,則直接從磁盤塊中讀取
    // 對(duì)于 swap out 的場(chǎng)景,內(nèi)核調(diào)用 bdev_write_page,直接將匿名頁的內(nèi)容寫入到磁盤塊中
    ret = bdev_read_page(sis->bdev, swap_page_sector(page), page);

out:
    return ret;
}

swap_readpage 是內(nèi)核 swap 機(jī)制的最底層實(shí)現(xiàn),直接和磁盤打交道,負(fù)責(zé)搭建磁盤與內(nèi)存之間的橋梁。雖然直接調(diào)用 swap_readpage 可以基本完成 swap in 的目的,但在某些特殊情況下會(huì)導(dǎo)致 swap 的性能非常糟糕。

比如下圖所示,假設(shè)當(dāng)前系統(tǒng)中存在三個(gè)進(jìn)程,它們共享引用了同一個(gè)物理內(nèi)存頁 page。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)這個(gè)被共享的 page 被內(nèi)核 swap out 到交換區(qū)之后,三個(gè)共享進(jìn)程的頁表會(huì)發(fā)生如下變化:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng) 進(jìn)程1 開始讀取這個(gè)共享 page 的時(shí)候,由于 page 已經(jīng) swap out 到交換區(qū)了,所以會(huì)發(fā)生 swap 缺頁異常,進(jìn)入內(nèi)核通過 swap_readpage 將共享 page 的內(nèi)容從磁盤中讀取進(jìn)內(nèi)存,此時(shí)三個(gè)進(jìn)程的頁表結(jié)構(gòu)變?yōu)橄聢D所示:

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在共享 page 已經(jīng)被 進(jìn)程1 swap in 進(jìn)來了,但是 進(jìn)程2 和 進(jìn)程 3 是不知道的,它們的頁表中還儲(chǔ)存的是 swp_entry_t,依然指向 page 所在交換區(qū)的位置。

按照之前的邏輯,當(dāng) 進(jìn)程2 以及 進(jìn)程3 開始讀取這個(gè)共享 page 的時(shí)候,其實(shí) page 已經(jīng)在內(nèi)存了,但是它們此刻感知不到,因?yàn)?進(jìn)程2 和 進(jìn)程3 的頁表中存儲(chǔ)的依然是 swp_entry_t,還是會(huì)產(chǎn)生 swap 缺頁中斷,重新通過 swap_readpage 讀取交換區(qū)中的內(nèi)容,這樣一來就產(chǎn)生了額外重復(fù)的磁盤 IO。

除此之外,更加嚴(yán)重的是,由于 進(jìn)程2 和 進(jìn)程3 的 swap 缺頁,又會(huì)產(chǎn)生兩個(gè)新的內(nèi)存頁用來存放從 swap_readpage 中讀取進(jìn)來的交換區(qū)數(shù)據(jù)。

產(chǎn)生了重復(fù)的磁盤 IO 不說,還產(chǎn)生了額外的內(nèi)存消耗,并且這樣一來,三個(gè)進(jìn)程對(duì)內(nèi)存頁就不是共享的了。

還有一種極端場(chǎng)景是一個(gè)進(jìn)程試圖讀取一個(gè)正在被 swap out 的 page ,由于 page 正在被內(nèi)核 swap out,此時(shí)進(jìn)程頁表指向該 page 的 pte 已經(jīng)變成了 swp_entry_t。

進(jìn)程在這個(gè)時(shí)候訪問 page 的時(shí)候,還是會(huì)產(chǎn)生 swap 缺頁異常,進(jìn)程試圖 swap in 這個(gè)正在被內(nèi)核 swap out 的 page,但是此時(shí) page 仍然還在內(nèi)存中,只不過是正在被內(nèi)核刷盤。

而按照之前的 swap in 邏輯,進(jìn)程這里會(huì)調(diào)用 swap_readpage 從磁盤中讀取,產(chǎn)生額外的磁盤 IO 以及內(nèi)存消耗不說,關(guān)鍵是此刻 swap_readpage 出來的數(shù)據(jù)都不是完整的,這肯定是個(gè)大問題。

內(nèi)核為了解決上面提到的這些問題,因此引入了一個(gè)新的結(jié)構(gòu) —— swap cache 。

10.3 swap cache

有了 swap cache 之后,情況就會(huì)變得大不相同,我們?cè)诨剡^頭來看第一個(gè)問題 —— 多進(jìn)程共享內(nèi)存頁。

進(jìn)程1 在 swap in 的時(shí)候首先會(huì)到 swap cache 中去查找,看看是否有其他進(jìn)程已經(jīng)把內(nèi)存頁 swap in 進(jìn)來了,如果 swap cache 中沒有才會(huì)調(diào)用 swap_readpage 從磁盤中去讀取。

當(dāng)內(nèi)核通過 swap_readpage 將內(nèi)存頁中的內(nèi)容從磁盤中讀取進(jìn)內(nèi)存之后,內(nèi)核會(huì)把這個(gè)匿名頁先放入 swap cache 中。進(jìn)程 1 的頁表將原來的 swp_entry_t 填充為 pte 并指向 swap cache 中的這個(gè)內(nèi)存頁。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

由于進(jìn)程1 頁表中對(duì)應(yīng)的頁表項(xiàng)現(xiàn)在已經(jīng)從 swp_entry_t 變?yōu)?pte 了,指向的是 swap cache 中的內(nèi)存頁而不是 swap 交換區(qū),所以對(duì)應(yīng) slot 的引用計(jì)數(shù)就要減 1 。

還記得我們之前介紹的 swap_map 數(shù)組嗎 ?slot 被進(jìn)程引用的計(jì)數(shù)就保存在這里,現(xiàn)在這個(gè) slot 在 swap_map 數(shù)組中保存的引用計(jì)數(shù)從 3 變成了 2 。表示還有兩個(gè)進(jìn)程也就是 進(jìn)程2 和 進(jìn)程3 仍在繼續(xù)引用這個(gè) slot 。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)進(jìn)程2 發(fā)生 swap 缺頁中斷的時(shí)候進(jìn)入內(nèi)核之后,也是首先會(huì)到 swap cache 中查找是否現(xiàn)在已經(jīng)有其他進(jìn)程把共享的內(nèi)存頁 swap in 進(jìn)來了,內(nèi)存頁 page 在 swap cache 的索引就是頁表中的 swp_entry_t。由于這三個(gè)進(jìn)程共享的同一個(gè)內(nèi)存頁,所以三個(gè)進(jìn)程頁表中的 swp_entry_t 都是相同的,都是指向交換區(qū)的同一位置。

由于共享內(nèi)存頁現(xiàn)在已經(jīng)被 進(jìn)程1 swap in 進(jìn)來了,并存放在 swap cache 中,所以 進(jìn)程2 通過 swp_entry_t 一下就在 swap cache 中找到了,同理,進(jìn)程 2 的頁表也會(huì)將原來的 swp_entry_t 填充為 pte 并指向 swap cache 中的這個(gè)內(nèi)存頁。slot 的引用計(jì)數(shù)減 1。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在這個(gè) slot 在 swap_map 數(shù)組中保存的引用計(jì)數(shù)從 2 變成了 1 。表示只有 進(jìn)程3 在引用這個(gè) slot 了。

當(dāng) 進(jìn)程3 發(fā)生 swap 缺頁中斷的之后,內(nèi)核還是先通過 swp_entry_t 到 swap cache 中去查找,找到之后,將 進(jìn)程 3 頁表原來的 swp_entry_t 填充為 pte 并指向 swap cache 中的這個(gè)內(nèi)存頁,slot 的引用計(jì)數(shù)減 1。

現(xiàn)在 slot 的引用計(jì)數(shù)已經(jīng)變?yōu)?0 了,這意味著所有共享該內(nèi)存頁的進(jìn)程已經(jīng)全部知道了新內(nèi)存頁的地址,它們的 pte 已經(jīng)全部指向了新內(nèi)存頁,不在指向 slot 了,此時(shí)內(nèi)核便將這個(gè)內(nèi)存頁從 swap cache 中移除。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

針對(duì)第二個(gè)問題 —— 進(jìn)程試圖 swap in 這個(gè)正在被內(nèi)核 swap out 的 page,內(nèi)核的處理方法也是一樣,內(nèi)核在 swap out 的時(shí)候首先會(huì)在交換區(qū)中為這個(gè) page 分配 slot 確定其在交換區(qū)的位置,然后通過之前文章 《深入理解 Linux 物理內(nèi)存管理》 中
介紹的匿名頁反向映射機(jī)制找到所有引用該內(nèi)存頁的進(jìn)程,將它們頁表中的 pte 修改為指向 slot 的 swp_entry_t。

然后將匿名頁 page 先是放入到 swap cache 中,慢慢地通過 swap_writepage 回寫。當(dāng)匿名頁被完全回寫到交換區(qū)中時(shí),內(nèi)核才會(huì)將 page 從 swap cache 中移除。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

如果當(dāng)內(nèi)核正在回寫的過程中,不巧有一個(gè)進(jìn)程又要訪問該內(nèi)存頁,同樣也會(huì)發(fā)生 swap 缺頁中斷,但是由于此時(shí)沒有回寫完成,內(nèi)存頁還保存在 swap cache 中,內(nèi)核通過進(jìn)程頁表中的 swp_entry_t 一下就在 swap cache 中找到了,避免了再次發(fā)生磁盤 IO,后面的過程就和第一個(gè)問題一樣了。

上述查找 swap cache 的過程。內(nèi)核封裝在 __read_swap_cache_async 函數(shù)里,在 swap in 的過程中,內(nèi)核會(huì)首先調(diào)用這里查看 swap cache 是否已經(jīng)緩存了內(nèi)存頁,如果沒有,則新分配一個(gè)內(nèi)存頁并加入到 swap cache 中,最后才會(huì)調(diào)用 swap_readpage 從磁盤中將所需內(nèi)容讀取到新內(nèi)存頁中。

struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
            struct vm_area_struct *vma, unsigned long addr,
            bool *new_page_allocated)
{
    struct page *found_page = NULL, *new_page = NULL;
    struct swap_info_struct *si;
    int err;
    // 是否分配新的內(nèi)存頁,如果內(nèi)存頁已經(jīng)在 swap cache 中則無需分配
    *new_page_allocated = false;

    do {
        // 獲取交換區(qū)結(jié)構(gòu) swap_info_struct
        si = get_swap_device(entry);
        // 首先根據(jù) swp_entry_t 到 swap cache 中查找,內(nèi)存頁是否已經(jīng)被其他進(jìn)程 swap in 進(jìn)來了
        found_page = find_get_page(swap_address_space(entry),
                       swp_offset(entry));
        // swap cache 已經(jīng)緩存了,就直接返回,不必啟動(dòng)磁盤 IO
        if (found_page)
            break;
        // 如果 swap cache 中沒有,則需要新分配一個(gè)內(nèi)存頁
        // 用來存儲(chǔ)從交換區(qū)中 swap in 進(jìn)來的內(nèi)容
        if (!new_page) {
            new_page = alloc_page_vma(gfp_mask, vma, addr);
            if (!new_page)
                break;      /* Out of memory */
        }
        // swap 沒有完成時(shí),內(nèi)存頁需要加鎖,禁止訪問
        __SetPageLocked(new_page);
        __SetPageSwapBacked(new_page);
        // 將新的內(nèi)存頁先放入 swap cache 中
        // 在這里會(huì)將 swp_entry_t 設(shè)置到 page 結(jié)構(gòu)的 private 屬性中
        err = add_to_swap_cache(new_page, entry, gfp_mask & GFP_KERNEL);
    } while (err != -ENOMEM);

    return found_page;
}

前面我們提到,Linux 系統(tǒng)中同時(shí)允許多個(gè)交換區(qū)存在,內(nèi)核將這些交換區(qū)組織在 swap_info 數(shù)組中。

struct swap_info_struct *swap_info[MAX_SWAPFILES];

內(nèi)核會(huì)為系統(tǒng)中每一個(gè)交換區(qū)分配一個(gè) swap cache,被內(nèi)核組織在一個(gè)叫做 swapper_spaces 的數(shù)組中。交換區(qū)的 swap cache 在 swapper_spaces 數(shù)組中的索引也是 swp_entry_t 中存儲(chǔ)的 type 信息,通過 swp_type 來提取。

// 一個(gè)交換區(qū)對(duì)應(yīng)一個(gè) swap cache
struct address_space *swapper_spaces[MAX_SWAPFILES] __read_mostly;

這里我們可以看到,交換區(qū)的 swap cache 和文件的 page cache 一樣,都是 address_space 結(jié)構(gòu)來描述的,而對(duì)于 swap file 來說,因?yàn)樗举|(zhì)上是文件系統(tǒng)里的一個(gè)文件,所以 swap file 既有 swap cache 也有 page cache 。

這里大家需要區(qū)分 swap file 的 swap cache 和 page cache,前面在介紹 swap_readpage 函數(shù)的時(shí)候,筆者也提過,swap file 的 page cache 在 swap 的場(chǎng)景中是不會(huì)緩存內(nèi)存頁的,內(nèi)核只是利用 page cache 相關(guān)的操作函數(shù) —— address_space->a_ops ,從 swap file 所在的文件系統(tǒng)中讀取或者寫入匿名頁,匿名頁是不會(huì)加入到 page cache 中的。

而交換區(qū)是針對(duì)整個(gè)系統(tǒng)來說的,系統(tǒng)中會(huì)存在很多進(jìn)程,當(dāng)發(fā)生 swap 的時(shí)候,系統(tǒng)中的這些進(jìn)程會(huì)對(duì)同一個(gè) swap cache 進(jìn)行爭搶,所以為了近一步提高 swap 的并行度,內(nèi)核會(huì)將一個(gè)交換區(qū)中的 swap cache 分裂多個(gè)出來,將競(jìng)爭的壓力分散開來。

這樣一來,一個(gè)交換就演變出多個(gè) swap cache 出來,swapper_spaces 數(shù)組其實(shí)是一個(gè) address_space 結(jié)構(gòu)的二維數(shù)組。每個(gè) swap cache 能夠管理的匿名頁個(gè)數(shù)為 2^SWAP_ADDRESS_SPACE_SHIFT 個(gè),涉及到的內(nèi)存大小為 4K * SWAP_ADDRESS_SPACE_PAGES —— 64M。

/* One swap address space for each 64M swap space */
#define SWAP_ADDRESS_SPACE_SHIFT	14
#define SWAP_ADDRESS_SPACE_PAGES	(1 << SWAP_ADDRESS_SPACE_SHIFT)

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

通過一個(gè)給定的 swp_entry_t 查找對(duì)應(yīng)的 swap cache 的邏輯,內(nèi)核定義在 swap_address_space 宏中。

  1. 首先內(nèi)核通過 swp_type 提取交換區(qū)在 swapper_spaces 數(shù)組中的索引(一維索引)。

  2. 通過 swp_offset >> SWAP_ADDRESS_SPACE_SHIFT(二維索引),定位 slot 具體歸哪一個(gè) swap cache 管理。

#define swap_address_space(entry)			    \
	(&swapper_spaces[swp_type(entry)][swp_offset(entry) \
		>> SWAP_ADDRESS_SPACE_SHIFT])

struct page * lookup_swap_cache(swp_entry_t entry)  
{          
    struct swap_info_struct *si = get_swap_device(entry);
    // 通過 swp_entry_t 定位 swap cache
    // 根據(jù) swp_offset 在 swap cache 中查找內(nèi)存頁
    page = find_get_page(swap_address_space(entry), swp_offset(entry));        
    return page;  
}

當(dāng)我們通過 swapon 命令來初始化并激活一個(gè)交換區(qū)的時(shí)候,內(nèi)核會(huì)在 init_swap_address_space 函數(shù)中為交換區(qū)初始化 swap cache。

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
    struct address_space *spaces, *space;
    unsigned int i, nr;
    // 計(jì)算交換區(qū)包含的 swap cache 個(gè)數(shù)
    nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES);
    // 為交換區(qū)分配 address_space 數(shù)組,用于存放多個(gè) swap cache
    spaces = kvcalloc(nr, sizeof(struct address_space), GFP_KERNEL);
    // 挨個(gè)初始化交換區(qū)中的 swap cache
    for (i = 0; i < nr; i++) {
        space = spaces + i;
        // 將 a_ops 指定為 swap_aops
        space->a_ops = &swap_aops;
        /* swap cache doesn't use writeback related tags */
        // swap cache 不會(huì)回寫
        mapping_set_no_writeback_tags(space);
    }
    // 保存交換區(qū)中的 swap cache 個(gè)數(shù)
    nr_swapper_spaces[type] = nr;
    // 將初始化好的 address_space 數(shù)組放入 swapper_spaces 數(shù)組中(二維數(shù)組)
    swapper_spaces[type] = spaces;

    return 0;
}

// 交換區(qū)中的 swap cache 個(gè)數(shù)
static unsigned int nr_swapper_spaces[MAX_SWAPFILES] __read_mostly;

struct address_space *swapper_spaces[MAX_SWAPFILES] __read_mostly;

這里我們可以看到,對(duì)于 swap cache 來說,內(nèi)核會(huì)將 address_space-> a_ops 初始化為 swap_aops。

static const struct address_space_operations swap_aops = {
	.writepage	= swap_writepage,
	.set_page_dirty	= swap_set_page_dirty,
#ifdef CONFIG_MIGRATION
	.migratepage	= migrate_page,
#endif
};

10.4 swap 預(yù)讀

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

現(xiàn)在我們已經(jīng)清楚了當(dāng)進(jìn)程虛擬內(nèi)存空間中的某一段 vma 發(fā)生 swap 缺頁異常之后,內(nèi)核的 swap in 核心處理流程。但是整個(gè)完整的 swap 流程還沒有結(jié)束,內(nèi)核還需要考慮內(nèi)存訪問的空間局部性原理。

當(dāng)進(jìn)程訪問某一段內(nèi)存的時(shí)候,在不久之后,其附近的內(nèi)存地址也將被訪問。對(duì)應(yīng)于本小節(jié)的 swap 場(chǎng)景來說,當(dāng)進(jìn)程地址空間中的某一個(gè)虛擬內(nèi)存地址 address 被訪問之后,那么其周圍的虛擬內(nèi)存地址在不久之后,也會(huì)被進(jìn)程訪問。

而那些相鄰的虛擬內(nèi)存地址,在進(jìn)程頁表中對(duì)應(yīng)的頁表項(xiàng)也都是相鄰的,當(dāng)我們處理完了缺頁地址 address 的 swap 缺頁異常之后,如果其相鄰的頁表項(xiàng)均是 swp_entry_t,那么這些相鄰的 swp_entry_t 所指向交換區(qū)的內(nèi)容也需要被內(nèi)核預(yù)讀進(jìn)內(nèi)存中。

這樣一來,當(dāng) address 附近的虛擬內(nèi)存地址發(fā)生 swap 缺頁的時(shí)候,內(nèi)核就可以直接從 swap cache 中讀到了,避免了磁盤 IO,使得 swap in 可以快速完成,這里和文件的預(yù)讀機(jī)制有點(diǎn)類似。

swap 預(yù)讀在 Linux 內(nèi)核中由 swapin_readahead 函數(shù)負(fù)責(zé),它有兩種實(shí)現(xiàn)方式:

第一種是根據(jù)缺頁地址 address 周圍的虛擬內(nèi)存地址進(jìn)行預(yù)讀,但前提是它們必須屬于同一個(gè) vma,這個(gè)邏輯在 swap_vma_readahead 函數(shù)中完成。

第二種是根據(jù)內(nèi)存頁在交換區(qū)中周圍的磁盤地址進(jìn)行預(yù)讀,但前提是它們必須屬于同一個(gè)交換區(qū),這個(gè)邏輯在 swap_cluster_readahead 函數(shù)中完成。

struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                struct vm_fault *vmf)
{
    return swap_use_vma_readahead() ?
            swap_vma_readahead(entry, gfp_mask, vmf) :
            swap_cluster_readahead(entry, gfp_mask, vmf);
}

在本小節(jié)介紹的 swap 缺頁場(chǎng)景中,內(nèi)核是按照缺頁地址周圍的虛擬內(nèi)存地址進(jìn)行預(yù)讀的。在函數(shù) swap_vma_readahead 的開始,內(nèi)核首先調(diào)用 swap_ra_info 方法來計(jì)算本次需要預(yù)讀的頁表項(xiàng)集合。

預(yù)讀的最大頁表項(xiàng)個(gè)數(shù)由 page_cluster 決定,但最大不能超過 2 ^ SWAP_RA_ORDER_CEILING

#ifdef CONFIG_64BIT
#define SWAP_RA_ORDER_CEILING	5
// 最大預(yù)讀窗口
max_win = 1 << min_t(unsigned int, READ_ONCE(page_cluster),
			     SWAP_RA_ORDER_CEILING);

page_cluster 的值可以通過內(nèi)核參數(shù) /proc/sys/vm/page-cluster 來調(diào)整,默認(rèn)值為 3,我們可以通過設(shè)置 page_cluster = 0來禁止 swap 預(yù)讀。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)要 swap in 的內(nèi)存頁在交換區(qū)的位置已經(jīng)接近末尾了,則需要減少預(yù)讀頁的個(gè)數(shù),防止預(yù)讀超出交換區(qū)的邊界。

如果預(yù)讀的頁表項(xiàng)不是 swp_entry_t,則說明該頁表項(xiàng)是一個(gè)空的還沒有進(jìn)行過映射或者頁表項(xiàng)指向的內(nèi)存頁還在內(nèi)存中,這種情況下則跳過,繼續(xù)預(yù)讀后面的 swp_entry_t。

/**
 * swap_vma_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vmf: fault information
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * Primitive swap readahead code. We simply read in a few pages whoes
 * virtual addresses are around the fault address in the same vma.
 *
 * Caller must hold read mmap_sem if vmf->vma is not NULL.
 *
 */
static struct page *swap_vma_readahead(swp_entry_t fentry, gfp_t gfp_mask,
                       struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct vma_swap_readahead ra_info = {0,};
    // 獲取本次要進(jìn)行預(yù)讀的頁表項(xiàng)
    swap_ra_info(vmf, &ra_info);
    // 遍歷預(yù)讀窗口 ra_info 中的頁表項(xiàng),挨個(gè)進(jìn)行預(yù)讀
    for (i = 0, pte = ra_info.ptes; i < ra_info.nr_pte;
         i++, pte++) {
        // 獲取要進(jìn)行預(yù)讀的頁表項(xiàng)
        pentry = *pte;
        // 頁表項(xiàng)為空,表示還未進(jìn)行內(nèi)存映射,直接跳過
        if (pte_none(pentry))
            continue;
        // 頁表項(xiàng)指向的內(nèi)存頁仍然在內(nèi)存中,跳過
        if (pte_present(pentry))
            continue;
        // 將 pte 轉(zhuǎn)換為 swp_entry_t
        entry = pte_to_swp_entry(pentry);
        if (unlikely(non_swap_entry(entry)))
            continue;
        // 利用 swp_entry_t 先到 swap cache 中去查找
        // 如果沒有,則新分配一個(gè)內(nèi)存頁并添加到 swap cache 中,這種情況下 page_allocated = true
        // 如果有,則直接從swap cache 中獲取內(nèi)存頁,也就不需要預(yù)讀了,page_allocated = false
        page = __read_swap_cache_async(entry, gfp_mask, vma,
                           vmf->address, &page_allocated);

        if (page_allocated) {
            // 發(fā)生磁盤 IO,從交換區(qū)中讀取內(nèi)存頁的內(nèi)容到新分配的 page 中
            swap_readpage(page, false);
        }
    }
}

這樣一來,經(jīng)過 swap_vma_readahead 預(yù)讀之后,缺頁內(nèi)存地址 address 周圍的頁表項(xiàng)所指向的內(nèi)存頁就全部被加載到 swap cache 中了。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)進(jìn)程下次訪問 address 周圍的內(nèi)存地址時(shí),雖然也會(huì)發(fā)生 swap 缺頁異常,但是內(nèi)核直接從 swap cache 中就可以讀取到了,避免了磁盤 IO。

10.5 還原 do_swap_page 完整面貌

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

當(dāng)我們明白了前面介紹的這些背景知識(shí)之后,再回過頭來看內(nèi)核完整的 swap in 過程就很清晰了

  1. 首先內(nèi)核會(huì)通過 pte_to_swp_entry 將進(jìn)程頁表中的 pte 轉(zhuǎn)換為 swp_entry_t

  2. 通過 lookup_swap_cache 根據(jù) swp_entry_t 到 swap cache 中查找是否已經(jīng)有其他進(jìn)程將內(nèi)存頁 swap 進(jìn)來了。

  3. 如果 swap cache 沒有對(duì)應(yīng)的內(nèi)存頁,則調(diào)用 swapin_readahead 啟動(dòng)預(yù)讀,在這個(gè)過程中,內(nèi)核會(huì)重新分配物理內(nèi)存頁,并將這個(gè)物理內(nèi)存頁加入到 swap cache 中,隨后通過 swap_readpage 將交換區(qū)的內(nèi)容讀取到這個(gè)內(nèi)存頁中。

  4. 現(xiàn)在我們需要的內(nèi)存頁已經(jīng) swap in 到內(nèi)存中了,后面的流程就和普通的缺頁處理一樣了,根據(jù) swap in 進(jìn)來的內(nèi)存頁地址重新創(chuàng)建初始化一個(gè)新的 pte,然后用這個(gè)新的 pte,將進(jìn)程頁表中原來的 swp_entry_t 替換掉。

  5. 為新的內(nèi)存頁建立反向映射關(guān)系,加入 lru active list 中,最后 swap_free 釋放交換區(qū)中的資源。

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

vm_fault_t do_swap_page(struct vm_fault *vmf)
{
    // 將缺頁內(nèi)存地址 address 對(duì)應(yīng)的 pte 轉(zhuǎn)換為 swp_entry_t
    entry = pte_to_swp_entry(vmf->orig_pte);  
    // 首先利用 swp_entry_t 到 swap cache 查找,看內(nèi)存頁已經(jīng)其他進(jìn)程被 swap in 進(jìn)來
    page = lookup_swap_cache(entry, vma, vmf->address);
    swapcache = page;
    // 處理匿名頁不在 swap cache 的情況
    if (!page) {
        // 通過 swp_entry_t 獲取對(duì)應(yīng)的交換區(qū)結(jié)構(gòu)
        struct swap_info_struct *si = swp_swap_info(entry);
        // 針對(duì) fast swap storage 比如 zram 等 swap 的性能優(yōu)化,跳過 swap cache
        if (si->flags & SWP_SYNCHRONOUS_IO &&
                __swap_count(entry) == 1) {
            /* skip swapcache */
            // 當(dāng)只有單進(jìn)程引用這個(gè)匿名頁的時(shí)候,直接跳過 swap cache
            // 從伙伴系統(tǒng)中申請(qǐng)內(nèi)存頁 page,注意這里的 page 并不會(huì)加入到 swap cache 中
            page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
                            vmf->address);
            if (page) {
                __SetPageLocked(page);
                __SetPageSwapBacked(page);
                set_page_private(page, entry.val);
                // 加入 lru 鏈表
                lru_cache_add_anon(page);
                // 直接從 fast storage device 中讀取被換出的內(nèi)容到 page 中
                swap_readpage(page, true);
            }
        } else {
            // 啟動(dòng) swap 預(yù)讀
            page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
                        vmf);
            swapcache = page;
        }

        // 因?yàn)樯婕暗搅舜疟P IO,所以本次缺頁異常屬于 FAULT_MAJOR 類型
        ret = VM_FAULT_MAJOR;
        count_vm_event(PGMAJFAULT);
        count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
    } 

    // 現(xiàn)在之前被換出的內(nèi)存頁已經(jīng)被內(nèi)核重新 swap in 到內(nèi)存中了。
    // 下面就是重新設(shè)置 pte,將原來頁表中的 swp_entry_t 替換掉
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
            &vmf->ptl);
    // 增加匿名頁的統(tǒng)計(jì)計(jì)數(shù)
    inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
    // 減少 swap entries 計(jì)數(shù)
    dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);
    // 根據(jù)被 swap in 進(jìn)來的新內(nèi)存頁重新創(chuàng)建 pte
    pte = mk_pte(page, vma->vm_page_prot);
    // 用新的 pte 替換掉頁表中的 swp_entry_t
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
    vmf->orig_pte = pte;

    // 建立新內(nèi)存頁的反向映射關(guān)系
    do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
    // 將內(nèi)存頁添加到 lru 的 active list 中
    activate_page(page);
    // 釋放交換區(qū)中的資源
    swap_free(entry);
    // 刷新 mmu cache
    update_mmu_cache(vma, vmf->address, vmf->pte);
    return ret;
}

總結(jié)

一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults

本文我們介紹了 Linux 內(nèi)核如何通過缺頁中斷將進(jìn)程頁表從 0 到 1 一步一步的完整構(gòu)建出來。從進(jìn)程虛擬內(nèi)存空間布局的角度來講,缺頁中斷主要分為兩個(gè)方面:

  • 內(nèi)核態(tài)缺頁異常處理 —— do_kern_addr_fault,這里主要是處理 vmalloc 虛擬內(nèi)存區(qū)域的缺頁異常,其中涉及到主內(nèi)核頁表與進(jìn)程頁表內(nèi)核部分的同步問題。

  • 用戶態(tài)缺頁異常處理 —— do_user_addr_fault,其中涉及到的主內(nèi)容是如何從 0 到 1 一步一步構(gòu)建完善進(jìn)程頁表體系。

總體上來講引起缺頁中斷的原因分為兩大類:

  • 第一類是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁不在內(nèi)存中

  • 第二類是缺頁虛擬內(nèi)存地址背后映射的物理內(nèi)存頁在內(nèi)存中。

第一類缺頁中斷的原因涉及到三種場(chǎng)景:

  1. 缺頁虛擬內(nèi)存地址 address 在進(jìn)程頁表中間頁目錄對(duì)應(yīng)的頁目錄項(xiàng) pmd_t 是空的。

  2. 缺頁地址 address 對(duì)應(yīng)的 pmd_t 雖然不是空的,頁表也存在,但是 address 對(duì)應(yīng)在頁表中的 pte 是空的。

  3. 虛擬內(nèi)存地址 address 在進(jìn)程頁表中的頁表項(xiàng) pte 不是空的,但是其背后映射的物理內(nèi)存頁被內(nèi)核 swap out 到磁盤上了。

第二類缺頁中斷的原因涉及到兩種場(chǎng)景:

  1. NUMA Balancing。

  2. 寫時(shí)復(fù)制了(Copy On Write, COW)。

最后我們介紹了內(nèi)核整個(gè) swap in 的完整過程,其中涉及到的重要內(nèi)容包括交換區(qū)的布局以及在內(nèi)核中的組織結(jié)構(gòu),swap cache 與 page cache 之間的區(qū)別,swap 預(yù)讀機(jī)制。

好了,今天的內(nèi)容到這里就結(jié)束了,感謝大家的收看,我們下篇文章見~~~~文章來源地址http://www.zghlxwxcb.cn/news/detail-760766.html

到了這里,關(guān)于一文聊透 Linux 缺頁異常的處理 —— 圖解 Page Faults的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • linux異常情況,排查處理中

    linux異常情況,排查處理中

    登錄客戶環(huán)境后,發(fā)現(xiàn)一個(gè)奇怪情況如下圖,之前也遇到過,直接fuser -ck /backup操作的話,主機(jī)將會(huì)重啟,因數(shù)據(jù)庫運(yùn)行中,等待停機(jī)維護(hù)時(shí)間,同時(shí)也在想辦法不重啟的情況下解決該問題 [root@db ~]#? fuser -cu /backup Specified filename /backup does not exist. [root@db ~]#? umount -l /bacup umo

    2024年01月21日
    瀏覽(18)
  • 微信小程序異常:navigateTo:fail can not navigateTo a tabbar page

    微信小程序利用路徑 wx.navigateTo 跳轉(zhuǎn)界面時(shí)發(fā)送異常 跳轉(zhuǎn)代碼 異常代碼 原因分析 在 app.json 中的 tabBar 關(guān)聯(lián)了 details 詳情界面產(chǎn)生沖突,而官方文檔要求 wx.navigateTo 無法跳轉(zhuǎn)到 tabBar 中定義的界面 ,只需要把 tabBar 換成其他界面就可以! app.json代碼 保留當(dāng)前頁面,跳轉(zhuǎn)到應(yīng)用

    2024年02月08日
    瀏覽(37)
  • vm.min_free_kbytes調(diào)整導(dǎo)致GI異常,kernel: oracle: page allocation failure

    vm.min_free_kbytes調(diào)整導(dǎo)致GI異常,kernel: oracle: page allocation failure

    有個(gè)11204 rac的測(cè)試環(huán)境,客戶反饋凌晨rman全備時(shí)偶爾會(huì)有內(nèi)存耗盡導(dǎo)致數(shù)據(jù)庫重啟的情況,不是合同內(nèi)的維護(hù)環(huán)境,請(qǐng)我們幫忙處理。我估計(jì)是沒配置vm.min_free_kbytes,之前也調(diào)整多次每次都成功完成,就沒有多想,直接白天調(diào)整了 ?機(jī)器內(nèi)存有370G多, 實(shí)例sga+pga=260G,我計(jì)劃

    2024年02月09日
    瀏覽(13)
  • vue-pdf多頁預(yù)覽異常,Rendering cancelled, page 1 Error at BaseExceptionClosure xxx

    vue-pdf多頁預(yù)覽異常,Rendering cancelled, page 1 Error at BaseExceptionClosure xxx

    項(xiàng)目開發(fā)使用vue-pdf,單頁情況預(yù)覽正常,多頁vue-pdf預(yù)覽異常,第一次預(yù)覽時(shí),會(huì)先彈出異常模態(tài)窗口,關(guān)閉模態(tài)窗口,pdf又是正常顯示,報(bào)錯(cuò)信息及異常截圖如下: 異常截圖,點(diǎn)擊右上角關(guān)閉X,pdf是正常預(yù)覽,再次打開后也能正常預(yù)覽,僅第一次打開預(yù)覽有異常。 1.vue-pdf預(yù)

    2024年02月07日
    瀏覽(23)
  • 七大排序算法一文通(易懂圖解+優(yōu)化代碼)

    七大排序算法一文通(易懂圖解+優(yōu)化代碼)

    目錄 1.直接插入排序 2.希爾排序 3.選擇排序 4.堆排序 5.冒泡排序 6.快速排序 6.1 遞歸實(shí)現(xiàn)——Hoare版 6.2?遞歸實(shí)現(xiàn)——挖坑法 6.3?非遞歸實(shí)現(xiàn) 6.4?優(yōu)化 7.歸并排序 7.1 歸并排序——遞歸實(shí)現(xiàn) 7.2 歸并排序——非遞歸實(shí)現(xiàn) 8.復(fù)雜度以及穩(wěn)定性 ??基本思路 從待排序數(shù)組的第 i (初始

    2024年02月07日
    瀏覽(23)
  • Linux下安裝ElasticSearch-analysis-ik中文分詞器插件,以及可能出現(xiàn)的異常處理

    Linux下安裝ElasticSearch-analysis-ik中文分詞器插件,以及可能出現(xiàn)的異常處理

    ? ? ? ? 注意:安裝可以采用在線方式、離線方式,但是不建議在線安裝,速度超級(jí)慢,本文只介紹離線安裝方式 ? ? ? ? ????????下載地址:https://github.com/medcl/elasticsearch-analysis-ik ? ? ? ? ? ? ? ? 切記選擇版本需要跟ElasticSearch保持一致,否則可能會(huì)出現(xiàn)一些未知的異

    2024年02月07日
    瀏覽(37)
  • 一文讀懂TCP的三次握手(詳細(xì)圖解)

    一文讀懂TCP的三次握手(詳細(xì)圖解)

    在學(xué)習(xí)TCP三次握手的過程前,首先熟悉幾個(gè)縮寫簡稱: TCB 傳輸控制塊,打開后服務(wù)器 / 客戶端進(jìn)入監(jiān)聽( LISTEN )狀態(tài) SYN TCP報(bào)文標(biāo)志位,該位為 1 時(shí)表示發(fā)起一個(gè)新連接 ACK TCP 報(bào)文標(biāo)志位,該位為1時(shí),確認(rèn)序號(hào)有效 ,確認(rèn)接收到消息。 TCP 規(guī)定,在連接建立后所有報(bào)文的傳

    2024年02月09日
    瀏覽(24)
  • 一文徹底搞懂BJT及其放大特性(圖解說明)

    一文徹底搞懂BJT及其放大特性(圖解說明)

    前置知識(shí):PN結(jié) 一文徹底搞懂PN結(jié)及其單向?qū)щ娦裕▓D解說明)-CSDN博客 BJT的基本結(jié)構(gòu)如上圖所示,在左側(cè)是寬度較窄,濃度非常高的N型離子參雜。中間是非常窄的P型離子參雜。而左側(cè)是濃度較低的N型離子參雜。 在N型參雜區(qū)和P型參雜區(qū)之間會(huì)形成PN結(jié),因此BJT實(shí)際上內(nèi)部是

    2024年02月08日
    瀏覽(16)
  • 一文講透TCP/IP協(xié)議 | 圖解+秒懂+史上最全

    一文講透TCP/IP協(xié)議 | 圖解+秒懂+史上最全

    目錄 ???♂? TCP/IP協(xié)議詳解 ???♂? TCP/IP協(xié)議的分層模型 OSI模型的七層框架 TCP/IP協(xié)議與七層ISO模型的對(duì)應(yīng)關(guān)系 (一)TCP/IP協(xié)議的應(yīng)用層 (二)TCP/IP協(xié)議的傳輸層 (三)TCP/IP協(xié)議的網(wǎng)絡(luò)層 (四)TCP/IP協(xié)議的鏈路層 ???♂? 圖解 物理層:使用MAC解決設(shè)備的身份證問題

    2024年02月06日
    瀏覽(19)
  • Linux page migration源碼分析

    Linux page migration源碼分析

    目錄 概述 __unmap_and_move函數(shù) step1:?Lock the page to be migrated step2:??Insure that writeback is complete. step3:?Lock the new page that we want to move to.? step4:??All the page table references to the page are converted to migration entries. step5-step15:?move_to_new_page step 5- step11 step 12- 15 step 16-18 概述 Linux 內(nèi)核page migrat

    2024年02月11日
    瀏覽(18)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包