深入Linux核心(記憶體篇)—頁表對映分頁

2020-10-13 11:01:06

作業系統的核心任務是對系統資源的管理,而重中之重的是對CPU和記憶體的管理。為了使程序擺脫系統記憶體的制約,使用者程序執行在虛擬記憶體之上,每個使用者程序都擁有完整的虛擬地址空間,互不干涉。

而實現虛擬記憶體的關鍵就在於建立虛擬地址(Virtual Address,VA)與實體地址(Physical Address,PA)之間的關係,因為無論如何資料終究要儲存到實體記憶體中才能被記錄下來。
如下圖所示,程序1和程序2擁有完整的虛擬地址空間,虛擬地址空間分為了使用者空間和核心空間,對於不同的程序面對的都是同一個核心,其核心空間的地址對於的實體地址都是一樣的,因而程序1和程序2中核心空間的VA K地址都對映到了實體記憶體的PA K地址。而不同的程序的使用者空間是不同的,程序1和程序2相同的虛擬地址VA 1和VA 2分別對映到了不同的實體地址PA 1和PA 2上。
在這裡插入圖片描述
而虛擬地址到實體地址對映關係的實現可以稱之為地址轉換(Address Translation)
為了實現上述地址轉換,作業系統需要藉助硬體的幫助,即記憶體管理單元(Memory Management Unit,MMU)的幫助。
對於MMU應當有如下功能:

要求說明
特權模式區分核心空間和使用者空間,使用者程序無法直接存取核心地址空間
基址/界限暫存器記錄地址轉換基址的暫存器,用於定址地址轉換對映表
地址轉換完成地址轉換過程
檢查越界完成地址轉換過程中,可以檢查存取是否越界
基址/界限暫存器特權操作指令用於修改地址轉換基址的暫存器,可以保證不同程序存取的對映表不同,從而對映的結果也不同
觸發異常發生越權,越界存取時,可以觸發異常通知作業系統
例外處理特權操作指令作業系統用於處理記憶體存取異常的入口

MMU配合作業系統完成了諸多功能:

  • 使用者空間和核心空間,通過特權模式劃分了核心空間和使用者空間,使用者空間無法直接存取核心空間,必須通過某些手段(系統呼叫,異常,中斷等)切換到特權模式才能間接存取核心。
  • 地址轉換,通過基址/界限暫存器記錄的轉換對映表基址,結合虛擬地址,可以完成地址轉換的功能,從而實現通過虛擬地址存取到實體地址。
  • 程序獨立的虛擬地址空間,通過基址/界限暫存器的存取指令,在程序切換時修改基址/界限暫存器的值,從而使MMU在做地址轉換時找到各個程序對應的地址對映表,從而實現不同程序虛擬地址完全獨立。
  • 缺頁異常,對於程序申請的記憶體,並不需要在其申請記憶體時即建立地址轉換對映表,同時分配對應的物理空間,而是在程序真正存取記憶體地址時,MMU上報缺頁異常再分配對應的物理空間。當然虛擬地址到實體地址對映表中的一些標誌區可以實現更多的缺頁異常型別,例如讀寫許可權錯誤,特權錯誤,越界錯誤等異常。

本文重點關注地址轉換,而地址轉換的核心是頁表對映。

一、分頁

分頁即將記憶體劃分為固定長度的單元,每個單元就是一頁。
對於虛擬地址空間,分頁機制將地址空間分割成固定大小的單元,每個單元稱為一頁。對於實體地址空間,實體記憶體被抽象成固定大小的單元,每個單元稱為頁幀(frame)。通過分頁管理記憶體可以避免分段帶來的記憶體外碎片問題。
分頁管理記憶體的核心問題是虛擬地址頁到實體地址頁幀的對映關係。

虛擬地址到實體地址的轉換可以抽象簡化成下圖,假設地址是32位元的。
在這裡插入圖片描述
為了將虛擬地址轉換成實體地址,將虛擬地址分割成兩部分:

  1. 虛擬頁面號,高31-X位組成,VPN(virtual page number);
  2. 虛擬地址偏移,低X位組成,VA Offset(virtual address offset);

實體地址也抽象成兩部分:

  1. 物理頁幀號,高31-X位組成,PFN(physical frame number);
  2. 實體地址偏移,低X位組成,PA Offset(physical address offset);

虛擬頁面號VPN用於索引物理頁幀號PFN,VPN索引PFN的過程就是地址轉換的核心。VA offset通常就是PA offset,即PFN + VA offset就是最終實體地址。

所以,可以說分頁機制的核心就是VPN到PFN的對映。而VPN到PFN的對映關係是通過頁表記錄的。MMU通過頁表記錄的對映關係完成VPN到PFN的轉換,即找到了頁表就找到了實體地址

1.1 頁表存在哪裡?

以32位元地址空間為例,分頁大小為4KB(最常用的分頁大小),上述抽象例子中的X為12,那麼VPN長度就是20bit,偏移量為12bit。
20bit的VPN意味著作業系統需要2^20個地址轉換對映,假設每個轉換對映需要4Byte空間儲存,那麼所有對映關係需要4MB空間。
開篇我們提到,程序的虛擬地址到實體地址的轉換是不同的,所以每個程序的對映關係也是不同的,就是說每個程序都需要4MB的空間來儲存頁表。如果作業系統執行100個程序,則需要400MB空間。
可見頁表所需要的空間是很大的,所以頁表都儲存在實體記憶體中。即MMU通將虛擬地址轉換為實體地址,需要存取實體記憶體中對應的頁表。
當然頁表佔用實體記憶體大的問題還是需要解決的,這是分頁相對於分段的一個劣勢,解決方案是多級頁表配合缺頁異常的方式,後面再詳細介紹多級頁表的機制。

1.2 頁表長啥樣?

頁表是如何完成VPN到PFN的轉換的,要知道這個問題就得清楚頁表的基本內容,即頁表記錄了什麼資訊。
頁表的作用就是通過VPN找到PFN,那麼頁表最基本的組成部分需要包含如下內容:

  • PFN物理頁幀號;
  • 有效位(valid),用於標記頁面是否有效;
  • 存在位(present),指示該頁是否存在於實體記憶體,用於頁面換入換出(swap);
  • 特權標記,指示頁面存取的特權等級;
  • Dirty位,寫操作時設定該位,表示頁面被寫過,頁面交換時使用;

1.3 分頁機制如何完成程序地址空間切換?

每個程序都擁有自己獨立的地址空間,程序切換時地址空間也會切換。不同程序都擁有自己的一套頁表,因而即使兩個程序虛擬地址相同,對映的實體地址也是不同的。
切換地址空間相當於控制MMU存取不同程序擁有的頁表,MMU找到了頁表就找到了實體地址。
通常CPU會提供若干暫存器供作業系統使用,用於為MMU指示頁表的基地址。
如下圖所示,程序切換時,只需要設定頁表基址暫存器即可完成頁表的切換,也就完成了程序地址空間的切換。
所以CPU會為作業系統提供頁表基址暫存器用於程序地址空間的切換。

X86體系架構提供的暫存器是CR3(Control Register 3);ARM-v7體系架構提供的暫存器是協處理器CP15暫存器TTBR(Tranlation Table Base Register);ARM-v8體系架構提供的暫存器是系統暫存器TTBR(Tranlation Table Base Register)。

在這裡插入圖片描述

1.4 實際使用的分頁機制

考慮到分頁機制佔用記憶體過多的問題,實際的分頁機制是多級分頁。
以二級頁表為例,如下圖所示,MMU通過頁表基址暫存器配合虛擬地址中的PGD index(Page Global Directory)找到一級頁表,通過一級頁表配合虛擬地址中的PTE index(Page Table Entry)找到二級頁表,通過二級頁表配合虛擬地址中Offset找到實體地址。
多級頁表要做到節省記憶體,還需要配合缺頁異常,程序往往只需將一級頁表保持到記憶體中,二級頁表在缺頁異常時再分配。
下圖範例中,一級頁表一共4096項(212),二級頁表一共512項(29)。因此程序頁表可以只使用4096 X 4Byte空間即可。如果使用一級頁表,則需要2097152 X 4Byte空間。因此多級頁錶帶來的最大好處就是降低了記憶體空間的佔用
在這裡插入圖片描述

1.5 多級頁表的缺點

多級頁錶帶來了好處,降低了作業系統程序管理,記憶體管理對記憶體空間的佔用。當然計算機領域總是沒有那麼完美的方案,多級分頁也逃避不了這個宿命,獲得了空間的優勢,也帶來時間上的損失。
多級分頁時間上的損失主要體現在如下幾個方面:

  • 用時下發的耗時:對於子程序寫時複製(COW)技術大家比較熟悉,其實多級頁表也利用了類似的思想。多級頁表的後幾級頁表對映關係沒有存在記憶體中,MMU地址轉換中發現頁表不存在需要向作業系統上報缺頁異常,作業系統需要在缺頁異常中下發頁表到記憶體;
  • 額外的記憶體存取:MMU進行地址轉換需要通過頁表基址暫存器找到一級頁表,再依次找到下級頁表,所有的頁表都存放在記憶體中,存取記憶體是需要額外的時間消耗的,相對於CPU對暫存器的存取,Cache的存取速度而言,記憶體的存取速度是災難性的,何況還是多次存取。當然額外的記憶體存取本身是分頁機制相對分段機制的缺陷,一級頁表對映也存在這樣的缺陷,只是多級頁表對映將這個缺點再次放大。

1.6 Translation Lookside Buffer

Translation Lookside Buffer簡稱TLB,按其真實作用應當翻譯為地址轉換快取
方才抨擊了多級頁表對映基址,提出了它可能導致系統變慢的缺點,那麼如何解決這一問題呢?如果使MMU做頁錶轉換時不存取記憶體,是不是就解決問題了?TLB就是幹這個事的。
TLB之所以可以解決這個問題是因為TLB是Cache,它將CPU存取記憶體替換為CPU存取Cache,也就是說MMU做頁錶轉換時不再存取記憶體的頁表,而是存取快取在TLB中的頁表,因而降低了時間的消耗。
TLB要實現這個替換,其需要實現的基本工作原理是:

  1. 從虛擬地址中提取頁號(VPN),檢查TLB是否有該VPN的轉換對映。
  2. 如果有,則表示TLB命中(TLB hit),意味著從TLB中找到VPN對應的物理頁框號(PFN)。PFN與虛擬地址的偏移量組成成實體地址(PA)。
  3. 如果沒有,表示TLB未命中(TLB miss),則需要處理TBL miss。
  4. TLB miss處理有兩種方法,一種是硬體處理,一種是軟體處理。硬體處理TLB miss會自動更新TLB。軟體處理則是由硬體丟擲一個TLB miss異常,軟體進入例外處理程式,查詢物理頁表中轉換對映,再由指令更新TLB,並從異常中返回。
  5. 軟體處理TLB miss異常與其他異常不同,例外處理返回後,應繼續執行陷入異常之後的那條指令,而TLB miss例外處理返回後,從導致陷入異常的執行開始執行。這樣保證TLB一定命中。

誠然,TLB是好,但是也引入了一些麻煩事(既然是Cache,就有一致性問題):程序切換時TLB如何處理?TLB表項滿了如何處理?mmap對映的記憶體被munmap解除TLB怎麼處理?……
針對這些話題本文不做深入探討,可以閱讀另一篇為其量身定做的博文《深入Linux核心(記憶體篇)—TLB》。

1.7 頁表多大合適?

大頁表的好處:

  • 省記憶體:可以解決分頁機制佔用記憶體的問題,取得和多級頁表一樣節省記憶體的效果;
  • 對TLB友好:大頁表意味著地址轉換時需要更少的頁表對映表項,頁表對映表項少了意味著TLB快取的表項少,這樣就提高了TLB的命中率;

大頁表的壞處:

  • 記憶體內碎片:作業系統申請記憶體時總是申請一大塊記憶體,哪怕實際只需要很小的記憶體,導致大頁記憶體得不到充分利用;而且記憶體很快會被這些大頁侵佔。

顯然小頁表的好處和壞處正好與大頁表對立。
因此頁表不是越大越好,也不是越小越好,找到折中的大小是才最適合。通常作業系統的使用的頁大小是4KB。

各種體系架構的CPU都支援很多種頁大小。因此實際頁表的應用可能會更「聰明」,使用者程序在請求地址空間時,可以因需求選擇合適的頁大小,這樣既可以滿足資料的存放,同時佔用更少的TLB表項。一個典型的例子,DPDK使用了1GB的大頁記憶體,這樣DPDK程序的頁表對映只佔用一個TLB表項,在程序執行過程中杜絕了TLB miss情況的發生,保障了效能。

二、X86中的分頁

X86中定義分頁即將每個線性地址轉換為實體地址,並確定對於每個轉換,允許對線性地址的何種存取(地址的存取許可權)以及用於此類存取的快取型別(地址的記憶體型別)。

X86支援如下四種分頁模式:
在這裡插入圖片描述
分頁模式的選擇主要由control register CR0,control register CR4,IA32_EFER MSR控制。
由上表可以看出:

  • CR0.PG = 0,關閉分頁單元,線性地址被直接解釋成實體地址;
  • CR0.PG = 1 && CR4.PAE = 0,使用32-bit分頁機制,線性地址大小是232,實體地址大小可以達到240,支援的頁大小是4KB和4MB;
  • CR0.PG = 1 && CR4.PAE = 1,使用PAE(Physical Address Extension)分頁機制,線性地址大小是232,實體地址大小可以達到252,支援的頁大小是4KB和2MB;
  • CR0.PG = 1 && CR4.PAE = 1 && IA32_EFER.LME = 1,使用4級分頁機制,線性地址大小是248,實體地址大小可以達到252,支援的頁大小是4KB、2MB和1GB;
  • CR0.PG = 1 && CR4.PAE = 1 && IA32_EFER.LME = 1 && CR4.LA57 = 1,使用5級分頁機制,線性地址大小是257,實體地址大小可以達到252,支援的頁大小是4KB、2MB和1GB;

2.1 32-bit Paging

暫存器狀態CR0.PG = 1 && CR4.PAE = 0 && IA32_EFER.LME = 0 時,X86選擇32-BIT PAGING分頁模式。
32-BIT PAGING分頁模式支援頁大小是4KB和4MB兩種。

以4KB大小頁為例,其分頁機制如下圖所示。
在這裡插入圖片描述
32bit線性地址被劃分為3部分:

  1. Directory[31:22]:最高10bit;
  2. Table[21:12]:中間10bit;
  3. Offset[11:0]:最低12bit。

其原理與第一節所述原理如出一轍。頁表基址暫存器為CR3,用於索引一級頁表Page Directory(PDE),Page Directory用於索引二級頁表Page Table(PTE),Page Table和Offset共同找到Physical Address。

2.2 32-bit Paging頁表長啥樣?

如下圖所示是32-BIT PAGING分頁模式下,CR3暫存器和一級頁表PDE,二級頁表PTE的長相。
在這裡插入圖片描述
CR3暫存器

  • Address of page directory[31:12]:線性地址轉換一級頁表實體地址,用於配合線性地址Page Directory找到一級頁表實體地址;
  • PCD[4]:Page-level cache disable
  • PWT[4]:Page-level write-through

PDE

  • Page size[7]:指定頁大小,為1則頁大小為4MB,為0則頁大小為4KB,頁大小不同則PDE的結構也不同,我們只看PS=0的情況,即4KB頁大小;
  • Present[0]:存在位,為1表示此頁在記憶體中,為0表示此頁不在記憶體中。地址轉換時,發現此位為0,則MMU將該地址放入CR2暫存器,併產生缺頁異常。
  • Read/write[1]:讀寫許可權,為0表示該頁表唯讀;
  • User/supervisor[2]:特權等級,為0則使用者模式無法存取該頁表;
  • Page-level write-through[3]:控制Cache處理頁表方式,指定Cache直寫和回寫策略;
  • Page-level cache disable[4]:控制Cache處理頁表方式,指定Cache是否禁用;
  • Accessed[5]:存取頁框時置位;
  • Address of page table[31:12]:線性地址轉換二級頁表實體地址,用於配合線性地址Page Table找到二級頁表實體地址;

PTE

  • [5:0]:與PDE一樣;
  • Dirty[6]:對頁框進行寫操作時置位;
  • Global[8]:CR4.PGE = 1時有效,防止常用頁從TLB中Flush掉。
  • Address of page table[31:12]:線性地址轉換實體地址,用於配合線性地址Offset找到實體地址;

其長相與第一節所述如出一轍。

2.3 4-Level Paging

再來看一個4級頁表分頁模式,支援的頁大小是4KB、2MB和1GB。
以4KB頁大小為例,如下圖所示,顯然相比於32-bit Paging,4-Level Paging擴充套件了線性地址(48bit)和實體地址(52bit)。

隨著計算機的發展,32bit的地址空間顯很侷促,尤其是物理定址範圍也只有32bit,即4GB實體地址空間,在計算機發展初期4GB空間是天文數位,現在已經淪落到選擇個人PC都看不上4GB記憶體,起碼是8GB記憶體起步。這也是為什麼X86 32位元CPU支援PAE(Physical Address Extension)的原因。同樣的,ARM-v7也支援了LPAE(Large Physical Address Extension),名字雖不同,但困境如出一轍。

在這裡插入圖片描述
4級頁表對映其原理與32bit Paging是一樣的。將47bit線性地址被劃分為5部分:

  1. PML4[47:39]:最高9bit;
  2. Directory Ptr[38:30]:中間9bit;
  3. Directory [29:21]:中間9bit;
  4. Table[20:12]:中間9bit;
  5. Offset[11:0]:最低12bit。

地址轉換過程也是一樣,從CR3開始逐級找到Physical Address,這裡不再贅述了。

其原理真是如出一轍。

三、ARM中的分頁

看完了X86中的分頁,再看ARM中分頁。

3.1 ARMv7 Paging

ARMv7架構支援三種頁大小:1MB,64KB和4KB。同時ARMv7支援LPAE,可以將實體地址範圍擴大到40bit。
以4KB頁大小,未開啟LPAE為例,如下圖所示。
在這裡插入圖片描述
32bit線性地址被劃分為3部分:

  1. L1 Table Index[31:20]:最高12bit;
  2. L2 Table Index[19:12]:中間8bit;
  3. Page Index[11:0]:最低12bit。

地址轉換過程,TTBR暫存器Translation base[31:14]和虛擬地址的L1 Table Index[31:20]索引到一級頁表實體地址,一級頁表Page table base address[31:10]和虛擬地址L2 Table Index[19:12]索引到二級頁表實體地址,二級頁表Small page base address[31:12]和虛擬地址的Page Index[11:0]索引到實體地址。

3.2 ARMv7 4KB Paging頁表長啥樣?

ARMv7 4KB分頁機制採用二級頁表管理,其一級頁表屬性如下圖所示。
在這裡插入圖片描述

  • Page table base address[31:10]:線性地址轉換二級頁表實體地址,用於配合線性地址Page Table找到二級頁表實體地址;
  • NS[3]:Non-secure,非安全模式下忽略;
  • Domain[8:5]:域,記憶體區域的集合,可以定義16個域,劃分其存取許可權,超越域存取許可權時會觸發Permission fault。

二級頁表屬性如下圖所示。
在這裡插入圖片描述

  • Small page base address[31:12]:線性地址轉換實體地址,用於配合線性地址offset找到實體地址;
  • XN[0]:Execute-never;
  • C,B[3:2]:定義內部cache屬性。
  • TEX[8:6]:定義外部cache屬性。
    在這裡插入圖片描述
  • AP[9,5:4]:Access Permissions,當高位設定為1,則頁表為唯讀許可權;
     MMU access permissions
  • S[10]:Shareable;
  • nG[11]:not global,指定頁表是否是全域性的,用於TLB;

3.3 ARMv8 分頁設定

ARMv8架構AArm64支援三種頁大小:64KB,16KB和4KB。
頁大小選擇由系統暫存器TCR控制,如下圖所示為TCR_EL1暫存器。
在這裡插入圖片描述
比較重要的bit位說明:

  • T0SZ[5:0]:The size offset of the memory region addressed by TTBR0_EL1,TTBR0_EL1定址的記憶體區域的大小偏移量,記憶體區域大小計算方法2(64-T0SZ);
  • T1SZ[21:16]:The size offset of the memory region addressed by TTBR1_EL1,TTBR1_EL1定址的記憶體區域的大小偏移量,記憶體區域大小計算方法2(64-T0SZ);
  • IRGN0[9:8]/IRGN1[25:24]:Inner cacheability attribute for memory associated,控制內部Cache存取模式,直寫和回寫;
  • ORGN0[11:10]/ORGN1[27:26]:Outer cacheability attribute for memory associated,控制外部Cache存取模式,直寫和回寫;
  • TG0[15:14]:Granule size for the TTBR0_EL1,頁大小,為0表示4KB,1表示64KB,2表示16KB;
  • TG1[31:30]:Granule size for the TTBR1_EL1,頁大小,為0表示4KB,1表示64KB,3表示16KB;
  • A1[22]:ASID選擇,為0選擇TTBR0_EL1.ASID ,為1選擇TTBR1_EL1.ASID;
  • IPS[34:32]:Intermediate Physical Address Size,中間實體地址大小;
  • AS[36]:ASID Size,ASID大小,為0表示8bit,為1表示16bit;
  • HA[39]:Hardware Access flag update,Access使能位;
  • HD[40]:Hardware management of dirty state,Dirty使能位;

說明:

  • ARM架構提供了兩個頁表基址暫存器TTBR0和TTBR1,可以分別用於使用者態和核心態。
  • ASID用於標識程序,可以根據ASID劃分TLB entry,避免TLB entry頻繁Flush。

顯然系統暫存器TCR控制了頁表對映的引數,其中TCR.TG0/TG1決定了頁大小。

當頁大小為4KB時,分頁單元每級頁表的地址範圍如下,其中TnSZmin和TnSZmax分別表示TCR_ELx.TnSZ的最小最大值,IA表示Input Address,即虛擬地址:
在這裡插入圖片描述

3.4 ARMv8 Paging

以頁大小為4KB,虛擬地址位寬為48bit為例,符合上一節中TCR_ELx.TnSZ為最小值的情況,如下圖所示。
在這裡插入圖片描述
ARMv8對IA(input address)劃分成了五部分:

  1. Index the level 0 translation table[47:39]:最高9bit;
  2. Index the level 1 translation table[38:30]:中間9bit;
  3. Index the level 2 translation table [29:21]:中間9bit;
  4. Index the level 3 translation table[20:12]:中間9bit;
  5. OA[11:0]:output address,最低12bit。

這個劃分方法與X86 4-Level Paging一樣。
其地址轉換過程,與前述的地址轉換過程並無差別,從頁表基址暫存器TTBR_ELx開始逐級查詢到實體地址,如下圖所示。
在這裡插入圖片描述

四、Kernel中的分頁

Linux Kernel分頁為了支援不同的CPU體系架構,設計了五級分頁模型,如下圖所示。五級分頁模型是為了相容X86-64體系架構中的5-Level Paging分頁模式,見第二節。
在這裡插入圖片描述
五級分頁每級命名分別為頁全域性目錄(PGD)、頁4級目錄(P4D)、頁上級目錄(PUD)、頁中間目錄(PMD)、頁表(PTE)。對應的相關宏定義命名如下:

#define PGDIR_SHIFT
#define P4D_SHIFT
#define PUD_SHIFT
#define PMD_SHIFT
#define PAGE_SHIFT

這些宏定義與具體體系架構相關,如果體系架構只使用了4級,3級或者更少的分級對映,則將其中的某幾個定義忽略即可。

Linux對於頁表的操作主要定義了以下函數或宏。這些操作方法也是與體系架構相關的,因此需要按照體系架構的硬體定義去實現。

宏或函數說明
pgd_offset(mm, addr)根據入參記憶體描述符mm和虛擬地址address,找到address在頁全域性目錄中相應表項的線性地址。
pgd_offset_k(addr)根據入參虛擬地址address和init_mm,找到address在頁全域性目錄中相應表項的線性地址。僅用於核心頁表。
p4d_offset(pgd, addr)根據入參pgd和虛擬地址address,找到address在頁四級目錄中相應表項的線性地址。
pud_offset(p4d,addr)根據入參p4d和虛擬地址address,找到address在頁上級目錄中相應表項的線性地址。
pmd_offset(pud, address)根據入參pud和虛擬地址address,找到address在頁中間目錄中相應表項的線性地址。
pte_index(address)根據入參虛擬地址address,找到address在頁表中索引。
set_pgd(pgdp, pgd)向PGD寫入指定的值
set_p4d(p4dp, p4d)向P4D寫入指定的值
set_pud(pudp, pud)向PUD寫入指定的值
set_pmd(pmdp, pmd)向PMD寫入指定的值
set_pte(ptep, pte)向PTE寫入指定的值
pte_dirty(pte)讀Dirty標誌
pte_mkdirty(pte)寫Dirty標誌

分頁機制與CPU體系架構強相關,因此分析Linux Kernel分頁時還是需要根據體系架構分析。

4.1 X86分頁

X86架構中支援四種分頁模式:32-bit,PAE,4-Level Paging和5-Level Paging。對於ARM體系架構最多用到了4級分頁,而X86架構可以用到5級分頁。Linux對於X86分頁定義如下。

#ifdef CONFIG_X86_5LEVEL

/*
 * PGDIR_SHIFT determines what a top-level page table entry can map
 */
#define PGDIR_SHIFT	pgdir_shift
#define PTRS_PER_PGD	512

/*
 * 4th level page in 5-level paging case
 */
#define P4D_SHIFT		39
#define MAX_PTRS_PER_P4D	512
#define PTRS_PER_P4D		ptrs_per_p4d
#define P4D_SIZE		(_AC(1, UL) << P4D_SHIFT)
#define P4D_MASK		(~(P4D_SIZE - 1))

#define MAX_POSSIBLE_PHYSMEM_BITS	52

#else /* CONFIG_X86_5LEVEL */

/*
 * PGDIR_SHIFT determines what a top-level page table entry can map
 */
#define PGDIR_SHIFT		39
#define PTRS_PER_PGD		512
#define MAX_PTRS_PER_P4D	1

#endif /* CONFIG_X86_5LEVEL */

/*
 * 3rd level page
 */
#define PUD_SHIFT	30
#define PTRS_PER_PUD	512

/*
 * PMD_SHIFT determines the size of the area a middle-level
 * page table can map
 */
#define PMD_SHIFT	21
#define PTRS_PER_PMD	512

/*
 * entries per page directory level
 */
#define PTRS_PER_PTE	512

#define PMD_SIZE	(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK	(~(PMD_SIZE - 1))
#define PUD_SIZE	(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK	(~(PUD_SIZE - 1))
#define PGDIR_SIZE	(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK	(~(PGDIR_SIZE - 1))

我們以4級分頁為例,其對虛擬地址的劃分如下圖所示,在2.3節中已經說明。從Linux宏定義可以看出PGDIR_SHIFT對應PML4,值為39;PUD_SHIFT對應Directory Ptr,值為30;PMD_SHIFT對應Directory,值為21;使用4KB也大小時,PAGE_SIZE為4KB,即PAGE_SHIFT大小為12,對應Table。
PTRS_PER_PGD,PTRS_PER_PUD,PTRS_PER_PMD分別對應每級表項的個數,都是512個表項(2^9)。
在這裡插入圖片描述

4.1 ARMv7分頁

ARMv7作為32bit CPU架構,其分頁一般採用兩級分頁。第一級為頁目錄(PGD),第二級為頁對映表(PTE),頁大小為4KB。

如下圖所示為ARMv7頁表對映示意圖,與ARMv7硬體4KB分頁機制相對應。頁表基址暫存器TTBRx(x為0或1)。
TTBRx(Translation Table Base Register x)即頁錶轉換基址暫存器,ARMv7提供了TTBR0和TTBR1兩個暫存器,Linux分別將其應用於核心態和使用者態。程序地址空間切換實質就是將TTBR0暫存器中Translation Table Base 0 Address修改為當前程序的PGD(頁全域性目錄)。一級頁表數量為4096,二級頁表數量為256。

頁表對映過程是MMU通過TTBRx和虛擬地址VA[31:20]索引到PGD一級頁表,再由PGD一級頁表和虛擬地址VA[19:12]索引到PTE頁對映表,在由PTE頁對映表和虛擬地址VA[11:0]索引到實體地址。
在這裡插入圖片描述
Linux對於上述PGD,PTE等資料的定義位於ARM體系架構目錄,如下所示:

/*
 * PMD_SHIFT determines the size of the area a second-level page table can map
 * PGDIR_SHIFT determines what a third-level page table entry can map
 */
#define PMD_SHIFT		21
#define PGDIR_SHIFT		21

#define PMD_SIZE		(1UL << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PGDIR_SIZE		(1UL << PGDIR_SHIFT)
#define PGDIR_MASK		(~(PGDIR_SIZE-1))

這裡會有一個疑惑,PMD和PGD沒有定義成20,和之前分析的不一致?

之前分析了ARMv7硬體分頁機制,4KB頁表大小進行分頁時,採用二級頁表結構,第一級有4096個表項,第二級有256個表項。二級頁表中的屬性沒有「dirty」位。
而Linux有一個三層的頁表結構,可以很容易地將其包裝成適合兩層的頁表結構—只使用PGD和PTE。但是,Linux還要求每個頁面有一個「PTE」表,而且至少要有一個「dirty」位。對於「dirty」位我們前面也講到了,「dirty」位在寫操作時被置位,表示頁面被寫過,頁面交換時會使用該標記。
因此,在這裡稍微調整了實現—告訴Linux在第一級有2048個條目,每個都是8位元組。二級頁表包含兩個連續排列的硬體PTE表項,前面的表項是包含Linux需要的狀態資訊的Linux PTE。因此,最終在「PTE」級別上有512個表項。宏定義如下所示。

#define PTRS_PER_PTE		512
#define PTRS_PER_PMD		1
#define PTRS_PER_PGD		2048

#define PTE_HWTABLE_PTRS	(PTRS_PER_PTE)
#define PTE_HWTABLE_OFF		(PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE	(PTRS_PER_PTE * sizeof(u32))

當頁面在Linux PTE中被標記為「可寫」和「dirty」時,「dirty」位通過授予硬體寫許可權模擬。也就是說ARM頁表設定時將許可權設定為唯讀,當向頁面寫入時,會觸發缺頁異常(Linux PTE頁面表項標記了可寫許可權,但是ARM硬體頁面表項是唯讀許可權),在缺頁例外處理函數handle_pte_fault()中會在該頁的Linux PTE頁面表項標記為「dirty」,為了讓硬體注意到許可權的更改,必須重新整理TLB條目,而ptep_set_access_flags()為我們完成了這項工作。

ARMv7頁表屬性的定義分為Linux版本的頁表和ARMv7硬體的頁表。
Linux版本的PTE頁表屬性定義加入字首L_,如下所示:

/*
 * "Linux" PTE definitions.
 *
 * We keep two sets of PTEs - the hardware and the linux version.
 * This allows greater flexibility in the way we map the Linux bits
 * onto the hardware tables, and allows us to have YOUNG and DIRTY
 * bits.
 *
 * The PTE table pointer refers to the hardware entries; the "Linux"
 * entries are stored 1024 bytes below.
 */
#define L_PTE_VALID		(_AT(pteval_t, 1) << 0)		/* Valid */
#define L_PTE_PRESENT		(_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG		(_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY		(_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY		(_AT(pteval_t, 1) << 7)
#define L_PTE_USER		(_AT(pteval_t, 1) << 8)
#define L_PTE_XN		(_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED		(_AT(pteval_t, 1) << 10)	/* shared(v6), coherent(xsc3) */
#define L_PTE_NONE		(_AT(pteval_t, 1) << 11)

ARMv7硬體的頁表屬性定義如下所示:

/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN		(_AT(pteval_t, 1) << 0)		/* v6 */
#define PTE_EXT_AP_MASK		(_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0		(_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1		(_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO	(_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW	(PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW	(PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW	(PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)		(_AT(pteval_t, (x)) << 6)	/* v5 */
#define PTE_EXT_APX		(_AT(pteval_t, 1) << 9)		/* v6 */
#define PTE_EXT_COHERENT	(_AT(pteval_t, 1) << 9)		/* XScale3 */
#define PTE_EXT_SHARED		(_AT(pteval_t, 1) << 10)	/* v6 */
#define PTE_EXT_NG		(_AT(pteval_t, 1) << 11)	/* v6 */

ARMv7硬體的頁表屬性定義與3.2節中描述的硬體頁表的屬性是相互對應的,其含義與硬體頁表屬性含義一致。
通過對比Linux版本的頁表和ARMv7硬體的頁表會發現,ARMv7硬體的頁表缺少「dirty」位和「young」位。「dirty」位前邊已經講過,「young」位用於標誌頁面剛剛被存取過,在頁面換出時,如果頁面「young」位被標記,則不會將該頁換出,同時清除「young」位標記。「young」位的模擬方法與「dirty」位類似,也是利用了兩套PTE頁表模擬,一套用於Linux,一套用於ARM硬體。

ARMv7頁表如何下發到硬體?是通過set_pte_ext()函數實現的。

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define cpu_set_pte_ext			PROC_TABLE(set_pte_ext)

不同CPU有不同的實現方式,以Cortex-A9為例,其實現是組合函數cpu_v7_set_pte_ext:

/*
 *	cpu_v7_set_pte_ext(ptep, pte)
 *
 *	Set a level 2 translation table entry.
 *
 *	- ptep  - pointer to level 2 translation table entry
 *		  (hardware version is stored at +2048 bytes)
 *	- pte   - PTE value to store
 *	- ext	- value for extended PTE bits
 */
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
	str	r1, [r0]			@ linux version @Linux PTE設定到r0指向的記憶體

	bic	r3, r1, #0x000003f0
	bic	r3, r3, #PTE_TYPE_MASK
	orr	r3, r3, r2
	orr	r3, r3, #PTE_EXT_AP0 | 2

	tst	r1, #1 << 4
	orrne	r3, r3, #PTE_EXT_TEX(1)

	eor	r1, r1, #L_PTE_DIRTY
	tst	r1, #L_PTE_RDONLY | L_PTE_DIRTY
	orrne	r3, r3, #PTE_EXT_APX @模擬「dirty」位,如果L_PTE_DIRTY置位,
	                             @則設定ARM硬體頁表APX位置位,設定頁表為唯讀許可權

	tst	r1, #L_PTE_USER
	orrne	r3, r3, #PTE_EXT_AP1

	tst	r1, #L_PTE_XN
	orrne	r3, r3, #PTE_EXT_XN

	tst	r1, #L_PTE_YOUNG
	tstne	r1, #L_PTE_VALID
	eorne	r1, r1, #L_PTE_NONE
	tstne	r1, #L_PTE_NONE
	moveq	r3, #0   @模擬「young」位,如果L_PTE_YONG清除且L_PTE_PRESENT置位,
	                 @則保持Linux版本頁表不變,ARM硬體頁表清除(r3置0即清空頁表)

 ARM(	str	r3, [r0, #2048]! ) @ARM PTE設定到r0+2048指向的記憶體
 THUMB(	add	r0, r0, #2048 )
 THUMB(	str	r3, [r0] )
	ALT_SMP(W(nop))
	ALT_UP (mcr	p15, 0, r0, c7, c10, 1)		@ flush_pte
#endif
	bx	lr
ENDPROC(cpu_v7_set_pte_ext)

暫存器r0是PTE表項指標,Linux使用的表項即r0所指記憶體,ARM硬體使用的表項地址是r0+2048。暫存器r1表示要寫入記憶體的Linux PTE表項的內容,其屬性bit位設定均使用L_字首的宏定義。
第一句語句將Linux PTE設定到r0指向的記憶體。

str	r1, [r0]

str為記憶體操作指令,表示將資料從暫存器寫的記憶體。

如下語句將ARM PTE設定到r0+2048指向的記憶體。

ARM(	str	r3, [r0, #2048]! )

[r0, #2048]為前索引定址模式,地址為暫存器R0中的值+立即數2048,偏移量為2048,計算出的新地址回寫到R0中。

關於記憶體操作指令詳細內容請看《ARM體系架構—ARMv7-A指令集:記憶體操作指令》

如下語句為ARMv7協處理器指令,指令含義為Data Cache Clean by MVA to PoC,即清除cache。

ALT_UP (mcr	p15, 0, r0, c7, c10, 1)	

CP15協處理器保護c0-c15共16個暫存器,暫存器32位元的組織形式如下: C R n , o p c 1 , C R m , o p c 2 {CRn, opc1, CRm, opc2} CRn,opc1,CRm,opc2 對於組合語句「mcr p15, 0, r0, c7, c10, 1」指示四個運算元結果如下:
在這裡插入圖片描述

  • CRn:第一個協處理器暫存器c7;
  • opc1:協處理器操作碼0;
  • CRm:第二個協處理器暫存器c10;
  • opc2:協處理器操作碼1。

關於協處理指令詳細內容請看《ARM體系架構—ARMv7-A協處理器》

4.2 ARMv8分頁

ARMv8頁表支援三種粒度:4KB,16KB和64KB。
ARMv8支援48bit虛擬地址空間, 實現ARMv8.2-LVA( Large Virtual Address)並使用64KB頁大小時虛擬地址定址空間可達52bit。當使用64KB頁大小時,ARMv8使用三級頁表;當使用4KB和16KB頁大小時,ARMv8使用四級頁表。正如下圖所示。
在這裡插入圖片描述
ARMv8採用4KB頁大小,使用4級頁表時,記憶體分佈如下,核心空間和使用者空間大小分別為256TB。核心空間地址範圍從0xffff000000000000到0xffffffffffffffff,共256TB空間,使用者空間地址範圍從0x0000000000000000到0x0000ffffffffffff,共256TB空間。

AArch64 Linux memory layout with 4KB pages + 4 levels (48-bit)::

  Start			End			Size		Use
  -----------------------------------------------------------------------
  0000000000000000	0000ffffffffffff	 256TB		user
  ffff000000000000	ffff7fffffffffff	 128TB		kernel logical memory map
  ffff800000000000	ffff9fffffffffff	  32TB		kasan shadow region
  ffffa00000000000	ffffa00007ffffff	 128MB		bpf jit region
  ffffa00008000000	ffffa0000fffffff	 128MB		modules
  ffffa00010000000	fffffdffbffeffff	 ~93TB		vmalloc
  fffffdffbfff0000	fffffdfffe5f8fff	~998MB		[guard region]
  fffffdfffe5f9000	fffffdfffe9fffff	4124KB		fixed mappings
  fffffdfffea00000	fffffdfffebfffff	   2MB		[guard region]
  fffffdfffec00000	fffffdffffbfffff	  16MB		PCI I/O space
  fffffdffffc00000	fffffdffffdfffff	   2MB		[guard region]
  fffffdffffe00000	ffffffffffdfffff	   2TB		vmemmap
  ffffffffffe00000	ffffffffffffffff	   2MB		[guard region]

ARM提供了兩個頁表基址暫存器TTBR0和TTBR1,在Linux中分別用於使用者空間和核心空間,核心空間地址高16位元全為1,使用者空間地址高16位元全為0,。如下圖所示,TTBR1和TTBR0分別管理0xffff000000000000到0xffffffffffffffff和0x0000000000000000到0x0000ffffffffffff兩部分地址空間,其餘地址空間存取則會發生異常。MMU做地址轉換時選擇TTBR1和TTBR0是根據虛擬地址VA[63],如果63bit為1則選擇TTBR1,為0則選擇TTBR0。
在這裡插入圖片描述
ARMv8採用4KB頁大小,4級頁表對映,其虛擬地址劃分為,在3.4節中已經做過說明。
在這裡插入圖片描述
Linux上述 Index the level 0 & 1 & 2 & 3 translation table等資料的定義位於ARM體系架構目錄,如下所示。

#define VA_BITS			(CONFIG_ARM64_VA_BITS)
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3)

#define PTRS_PER_PTE		(1 << (PAGE_SHIFT - 3))

/*
 * PMD_SHIFT determines the size a level 2 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE
#endif

/*
 * PUD_SHIFT determines the size a level 1 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 3
#define PUD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define PUD_SIZE		(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK		(~(PUD_SIZE-1))
#define PTRS_PER_PUD		PTRS_PER_PTE
#endif

/*
 * PGDIR_SHIFT determines the size a top-level page table entry can map
 * (depending on the configuration, this level can be 0, 1 or 2).
 */
#define PGDIR_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define PGDIR_SIZE		(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK		(~(PGDIR_SIZE-1))
#define PTRS_PER_PGD		(1 << (VA_BITS - PGDIR_SHIFT))

PGDIR_SHIFT宏對應了 Index the level 0 translation table,當4KB頁大小,4級頁表對映時(PAGE_SHIFT = 12;CONFIG_PGTABLE_LEVELS = 4),通過計算可得PGDIR_SHIFT宏為39,與硬體分頁定義一致。

PGDIR_SHIFT	= (12-3)*(4-0)+3 = 39

CONFIG_PGTABLE_LEVELS表示使用頁表級數,當前使用的是4級頁表,所以CONFIG_PGTABLE_LEVELS>3。因此PUD定義為:

#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3)
#define PUD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(1)

PUD_SHIFT宏對應了 Index the level 1 translation table,帶入計算可知PUD_SHIFT為30,與硬體分頁定義一致。

PUD_SHIFT = (12-3)*(4-1)+3 = 30

CONFIG_PGTABLE_LEVELS表示使用頁表級數,當前使用的是4級頁表,所以CONFIG_PGTABLE_LEVELS>2。因此PMD定義為:

#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3)
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)

PMD_SHIFT宏對應了 Index the level 3 translation table,帶入計算可知PUD_SHIFT為21,與硬體分頁定義一致。

PMD_SHIFT = (12-3)*(4-2)+3 = 21

至此,PGD,PUD,PMD都已獲悉,對應的其他宏的值也可以順利計算出結果。

#define PGDIR_SIZE		512GB
#define PTRS_PER_PGD	512

#define PUD_SIZE		1GB
#define PTRS_PER_PUD	512

#define PMD_SIZE		2MB
#define PTRS_PER_PMD	512

ARMv8 Linux下發PGD,PUD,PMD,PTE並沒有使用組合語言,而是使用C語言實現,對應的函數如下,其實現原理都是將對應的表項內容寫入表項所在地址。

/* 向記憶體下發PGD頁表,入參分別為pgd頁表虛擬地址和pgd表項*/
static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
{
	if (in_swapper_pgdir(pgdp)) {
		set_swapper_pgd(pgdp, pgd);  /* 將pgd寫入swapper_pg_dir所指地址 */
		return;
	}

	WRITE_ONCE(*pgdp, pgd); /* 將pgd寫入pgdp所指地址 */
	dsb(ishst); /* 資料記憶體屏障 */
	isb(); /* 指令記憶體屏障 */
}

static inline void set_pud(pud_t *pudp, pud_t pud)
{
#ifdef __PAGETABLE_PUD_FOLDED
	if (in_swapper_pgdir(pudp)) {
		set_swapper_pgd((pgd_t *)pudp, __pgd(pud_val(pud)));
		return;
	}
#endif /* __PAGETABLE_PUD_FOLDED */

	WRITE_ONCE(*pudp, pud); /* 將pud寫入pudp所指地址 */

	if (pud_valid(pud)) {
		dsb(ishst);
		isb();
	}
}

static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
{
#ifdef __PAGETABLE_PMD_FOLDED
	if (in_swapper_pgdir(pmdp)) {
		set_swapper_pgd((pgd_t *)pmdp, __pgd(pmd_val(pmd)));
		return;
	}
#endif /* __PAGETABLE_PMD_FOLDED */

	WRITE_ONCE(*pmdp, pmd); /* 將pmd寫入pmdp所指地址 */

	if (pmd_valid(pmd)) {
		dsb(ishst);
		isb();
	}
}

static inline void set_pte(pte_t *ptep, pte_t pte)
{
	WRITE_ONCE(*ptep, pte); /* 將pte寫入ptep所指地址 */

	/*
	 * Only if the new pte is valid and kernel, otherwise TLB maintenance
	 * or update_mmu_cache() have the necessary barriers.
	 */
	if (pte_valid_not_user(pte)) {
		dsb(ishst);
		isb();
	}
}

本文核心版本為Linux5.6.4。