作業系統的核心任務是對系統資源的管理,而重中之重的是對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配合作業系統完成了諸多功能:
本文重點關注地址轉換,而地址轉換的核心是頁表對映。
分頁即將記憶體劃分為固定長度的單元,每個單元就是一頁。
對於虛擬地址空間,分頁機制將地址空間分割成固定大小的單元,每個單元稱為一頁。對於實體地址空間,實體記憶體被抽象成固定大小的單元,每個單元稱為頁幀(frame)。通過分頁管理記憶體可以避免分段帶來的記憶體外碎片問題。
分頁管理記憶體的核心問題是虛擬地址頁到實體地址頁幀的對映關係。
虛擬地址到實體地址的轉換可以抽象簡化成下圖,假設地址是32位元的。
為了將虛擬地址轉換成實體地址,將虛擬地址分割成兩部分:
實體地址也抽象成兩部分:
虛擬頁面號VPN用於索引物理頁幀號PFN,VPN索引PFN的過程就是地址轉換的核心。VA offset通常就是PA offset,即PFN + VA offset就是最終實體地址。
所以,可以說分頁機制的核心就是VPN到PFN的對映。而VPN到PFN的對映關係是通過頁表記錄的。MMU通過頁表記錄的對映關係完成VPN到PFN的轉換,即找到了頁表就找到了實體地址。
以32位元地址空間為例,分頁大小為4KB(最常用的分頁大小),上述抽象例子中的X為12,那麼VPN長度就是20bit,偏移量為12bit。
20bit的VPN意味著作業系統需要2^20個地址轉換對映,假設每個轉換對映需要4Byte空間儲存,那麼所有對映關係需要4MB空間。
開篇我們提到,程序的虛擬地址到實體地址的轉換是不同的,所以每個程序的對映關係也是不同的,就是說每個程序都需要4MB的空間來儲存頁表。如果作業系統執行100個程序,則需要400MB空間。
可見頁表所需要的空間是很大的,所以頁表都儲存在實體記憶體中。即MMU通將虛擬地址轉換為實體地址,需要存取實體記憶體中對應的頁表。
當然頁表佔用實體記憶體大的問題還是需要解決的,這是分頁相對於分段的一個劣勢,解決方案是多級頁表配合缺頁異常的方式,後面再詳細介紹多級頁表的機制。
頁表是如何完成VPN到PFN的轉換的,要知道這個問題就得清楚頁表的基本內容,即頁表記錄了什麼資訊。
頁表的作用就是通過VPN找到PFN,那麼頁表最基本的組成部分需要包含如下內容:
每個程序都擁有自己獨立的地址空間,程序切換時地址空間也會切換。不同程序都擁有自己的一套頁表,因而即使兩個程序虛擬地址相同,對映的實體地址也是不同的。
切換地址空間相當於控制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)。
考慮到分頁機制佔用記憶體過多的問題,實際的分頁機制是多級分頁。
以二級頁表為例,如下圖所示,MMU通過頁表基址暫存器配合虛擬地址中的PGD index(Page Global Directory)找到一級頁表,通過一級頁表配合虛擬地址中的PTE index(Page Table Entry)找到二級頁表,通過二級頁表配合虛擬地址中Offset找到實體地址。
多級頁表要做到節省記憶體,還需要配合缺頁異常,程序往往只需將一級頁表保持到記憶體中,二級頁表在缺頁異常時再分配。
下圖範例中,一級頁表一共4096項(212),二級頁表一共512項(29)。因此程序頁表可以只使用4096 X 4Byte空間即可。如果使用一級頁表,則需要2097152 X 4Byte空間。因此多級頁錶帶來的最大好處就是降低了記憶體空間的佔用。
多級頁錶帶來了好處,降低了作業系統程序管理,記憶體管理對記憶體空間的佔用。當然計算機領域總是沒有那麼完美的方案,多級分頁也逃避不了這個宿命,獲得了空間的優勢,也帶來時間上的損失。
多級分頁時間上的損失主要體現在如下幾個方面:
Translation Lookside Buffer簡稱TLB,按其真實作用應當翻譯為地址轉換快取。
方才抨擊了多級頁表對映基址,提出了它可能導致系統變慢的缺點,那麼如何解決這一問題呢?如果使MMU做頁錶轉換時不存取記憶體,是不是就解決問題了?TLB就是幹這個事的。
TLB之所以可以解決這個問題是因為TLB是Cache,它將CPU存取記憶體替換為CPU存取Cache,也就是說MMU做頁錶轉換時不再存取記憶體的頁表,而是存取快取在TLB中的頁表,因而降低了時間的消耗。
TLB要實現這個替換,其需要實現的基本工作原理是:
誠然,TLB是好,但是也引入了一些麻煩事(既然是Cache,就有一致性問題):程序切換時TLB如何處理?TLB表項滿了如何處理?mmap對映的記憶體被munmap解除TLB怎麼處理?……
針對這些話題本文不做深入探討,可以閱讀另一篇為其量身定做的博文《深入Linux核心(記憶體篇)—TLB》。
大頁表的好處:
大頁表的壞處:
顯然小頁表的好處和壞處正好與大頁表對立。
因此頁表不是越大越好,也不是越小越好,找到折中的大小是才最適合。通常作業系統的使用的頁大小是4KB。
各種體系架構的CPU都支援很多種頁大小。因此實際頁表的應用可能會更「聰明」,使用者程序在請求地址空間時,可以因需求選擇合適的頁大小,這樣既可以滿足資料的存放,同時佔用更少的TLB表項。一個典型的例子,DPDK使用了1GB的大頁記憶體,這樣DPDK程序的頁表對映只佔用一個TLB表項,在程序執行過程中杜絕了TLB miss情況的發生,保障了效能。
X86中定義分頁即將每個線性地址轉換為實體地址,並確定對於每個轉換,允許對線性地址的何種存取(地址的存取許可權)以及用於此類存取的快取型別(地址的記憶體型別)。
X86支援如下四種分頁模式:
分頁模式的選擇主要由control register CR0,control register CR4,IA32_EFER MSR控制。
由上表可以看出:
暫存器狀態CR0.PG = 1 && CR4.PAE = 0 && IA32_EFER.LME = 0 時,X86選擇32-BIT PAGING分頁模式。
32-BIT PAGING分頁模式支援頁大小是4KB和4MB兩種。
以4KB大小頁為例,其分頁機制如下圖所示。
32bit線性地址被劃分為3部分:
其原理與第一節所述原理如出一轍。頁表基址暫存器為CR3,用於索引一級頁表Page Directory(PDE),Page Directory用於索引二級頁表Page Table(PTE),Page Table和Offset共同找到Physical Address。
如下圖所示是32-BIT PAGING分頁模式下,CR3暫存器和一級頁表PDE,二級頁表PTE的長相。
CR3暫存器
PDE
PTE
其長相與第一節所述如出一轍。
再來看一個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部分:
地址轉換過程也是一樣,從CR3開始逐級找到Physical Address,這裡不再贅述了。
其原理真是如出一轍。
看完了X86中的分頁,再看ARM中分頁。
ARMv7架構支援三種頁大小:1MB,64KB和4KB。同時ARMv7支援LPAE,可以將實體地址範圍擴大到40bit。
以4KB頁大小,未開啟LPAE為例,如下圖所示。
32bit線性地址被劃分為3部分:
地址轉換過程,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]索引到實體地址。
ARMv7 4KB分頁機制採用二級頁表管理,其一級頁表屬性如下圖所示。
二級頁表屬性如下圖所示。
ARMv8架構AArm64支援三種頁大小:64KB,16KB和4KB。
頁大小選擇由系統暫存器TCR控制,如下圖所示為TCR_EL1暫存器。
比較重要的bit位說明:
說明:
顯然系統暫存器TCR控制了頁表對映的引數,其中TCR.TG0/TG1決定了頁大小。
當頁大小為4KB時,分頁單元每級頁表的地址範圍如下,其中TnSZmin和TnSZmax分別表示TCR_ELx.TnSZ的最小最大值,IA表示Input Address,即虛擬地址:
以頁大小為4KB,虛擬地址位寬為48bit為例,符合上一節中TCR_ELx.TnSZ為最小值的情況,如下圖所示。
ARMv8對IA(input address)劃分成了五部分:
這個劃分方法與X86 4-Level Paging一樣。
其地址轉換過程,與前述的地址轉換過程並無差別,從頁表基址暫存器TTBR_ELx開始逐級查詢到實體地址,如下圖所示。
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分頁時還是需要根據體系架構分析。
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)。
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協處理器》
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。