前言

上一篇《x86段暫存器和分段機制》瞭解了x86分段機制原理,以及CPU定址的整個過程。本片補上定址的最後一棒:分頁機制,並結合linux核心瞭解下OS中的分頁機制實現。

為何需要分頁機制

80x86的兩種工作模式:80386的工作模式包括實地址模式和虛地址模式(保護模式)。真實模式下,程序直接使用物理記憶體,程序地址空間不隔離。由於程式都是直接訪問物理記憶體,所以惡意程式可以隨意修改別的程序的記憶體資料;這種模式記憶體使用效率低且不安全,於是引入了虛擬地址概念,即CPU工作在保護模式。

從 80386 處理器開始,引入了分頁機制,分頁單元把物理記憶體分成大小固定(4KB)的頁框。段機制實現虛擬地址到線性地址的轉換,分頁機制實現線性地址到物理地址的轉換。

80x86常規分頁單元

分頁機制是80x86記憶體管理機制的第二部分。它在分段機制的基礎上完成虛擬(邏輯)地址到物理地址轉換的過程。

上一章《x86段暫存器和CPU定址》中,我介紹到x86CPU從邏輯地址到物理地址的整個定址過程,實際做這些工作的就是記憶體管理單元(MMU)。MMU透過一種叫做分段單元的硬體電路把一個邏輯地址轉換成線性地址,然後另一個叫做分頁單元的硬體電路把線性地址轉換成物理地址。不過像ARM這種RISC(Reduced Instruction Set Computer),CPU上的MMU並沒有分段單元。下圖展示了分頁單元的作用:

x86的分頁機制和Linux實現

圖1 MMU記憶體管理單元

(1)分頁單元將線性地址分成以固定長度位單位的組,稱為頁(page)。頁內部連續,對映是以頁位基本單位。而且核心可以指定一個頁的物理地址和其存取許可權,無需為頁所有包含所以線性地址分別指定許可權。分頁單元把線性地址轉換成物理地址,一個關鍵任務是把所請求的訪問型別與線性地址的訪問許可權相比較,如果訪問無效會產生一個缺頁異常。

(2)分頁單元把所有的RAM物理記憶體分成固定長度的頁框(page frame),長度也是4KB。注意區分頁和頁框的區別:

一頁指一系列的線性地址和包含於其中的資料(程序中的概念),頁框是指真正的物理頁,是一個實際的儲存區域。其長度一致,頁可以存放在任何頁框中。

和分頁相關的暫存器有:

暫存器

用途

CR0

PG位記憶體是否開啟分頁,PG=1時開啟

CR2

儲存發生缺頁異常時的虛擬地址

CR3

儲存當前程序的頁目錄表 物理記憶體基地址

CR4

PRE

是否開啟物理地址擴充套件

其中最重要的是

CR3

暫存器,CR3中含有頁目錄表 物理記憶體基地址,因此該暫存器也被稱為頁目錄基 地址暫存器PDBR(Page-Directory Base address Register)。CR0暫存器的PG標識等於1時,表示啟用分頁機制。

80x86

常規分頁

將頁固定為4KB,使用二級頁表,將32位線性地址劃分成三個域:

x86的分頁機制和Linux實現

使用這種二級頁表的目的是減少每個程序頁表所需的RAM數量。如果簡單得使用一級頁表,將有2^20個頁表項(假如每個表項用4位元組儲存。則需要4MB RAM)來表示每個程序的頁表,即便是程序只用了很少的一部分線性地址。二級頁表只為程序實際使用的虛擬記憶體區域請求頁表,來減少儲存每個程序頁表所需的記憶體。

每個程序有一個獨立的頁表,正在執行的程序的頁目錄物理基地址會被放在控制暫存器CR3中。如上圖,一個線性地址由 頁目錄項PDE(page directory entry)、頁表項PTE(page table entry)和頁內偏移量組成。地址翻譯過程可以用下面的圖表示:

x86的分頁機制和Linux實現

圖2 80x86兩級頁表

分析:

1)首先在程序切換時將該程序的頁目錄起始物理地址放到CR3暫存器中;

2)當該程序指令訪問一個地址時,先經過MMU分段單元轉化成線性地址;

3)把線性地址中DIRECTORY項(前10bit)作為偏移量,加上CR3暫存器中儲存的頁目錄物理基地址,得到的PDE就是該地址所在頁表的物理基地址;

4)把線性地址中TABLE項(中間10bit)作為偏移量,加上3)中得到的頁表物理基地址,得到的PTE就是該地址所在的物理頁框基地址;

5)線性地址中OFFSET項(最後12bit),是頁框中的偏移量,加上上面得到的頁框基地址就是該線性地址對映的物理記憶體地址。

另外x86還支援擴充套件分頁(extended paging),即支援4MB大小的頁框。在這種情況下核心可以不用中間頁表進行地址轉換,這裡不再詳細分析。

64位系統中的分頁

前面介紹的是80x86 32位系統中的分頁機制,採用的二級頁表管理頁框對映。然而它並不適用於64位系統。還是以標準的4KB頁框位基準,OFFSET欄位佔12位。即便是目前普遍採用的使用64bit中的48bit來定址,TABLE和DIRECTORY要分36位。即便是五五開,每級頁表包含2^18 = 256000個項。每個程序頁表佔用的記憶體還是非常大的,因此x86_64採用四級頁表,來管理標準4KB頁。

在x64體系中只實現了48位的virtual address定址,理論上48位地址長度可以管理512TB的地址空間,不過x86_64四級頁表模型只使用了256TB的虛擬地址空間。其中高16位(48-64位)將填充第47位相同的內容(這種方式類似於符號擴充套件),這樣可以將地址空間分成高128TB的核心空間和低128TB的使用者空間:

x86的分頁機制和Linux實現

圖3 x86_64地址空間分佈

由於在x64體系結構中,普通頁大小仍為4KB,然而資料卻表示64位長,因此一個4KB頁在x64體系結構下只能包含512項(2^9)內容。所以為了保證頁對齊和以頁為單位的頁表內容換入換出,在x64下每級頁表定址部分長度定位9位:

x86的分頁機制和Linux實現

圖4 x86_64線性地址劃分

1)

PML4T

(Page Map Level4 Table)及表內的PML4E結構,每個表為4K,內含512個PML4E結構,每個8位元組

2)

PDPT

(Page Directory Pointer Table)及表內的PDPTE結構,每個表4K,內含512個PDPTE結構,每個8位元組

3)

PDT

(Page Directory Table) 及表內的PDE結構,每個表4K,內含512個PDE結構,每個8位元組

4)

PT

(Page Table)及表內額PTE結構,每個表4K,內含512個PTE結構,每個8位元組。

5)

OFFSET

:頁內偏移量

同x86 32位一樣,系統使用CR3儲存頁表的基地址,即PML4T的物理基地址。x86_64支援多種頁面大小:

1)4K頁面:使用PML4T,PDPT,PDT和PT 四級頁轉化表結構;

2)2M頁面:使用PML4T,PDPT 和PDT三級頁轉化表結構;

3)1G 頁面:使用PML4T和PDPT二級頁錶轉化結構。

詳細的地址轉換過程,在下面的linux 系統分頁機制實現再闡述。

TLB

轉譯後備緩衝器:TLB(translation lookaside buffer) ,是CPU的一種快取,由儲存器管理單元用於改進虛擬地址到物理地址的轉譯速度。

TLB中快取虛擬地址和物理地址對映關係,在地址翻譯時比查表要快得多。程序在訪問記憶體地址時,首先把該虛擬地址發往TLB確認是否命中cache,如果cache hit直接可以得到物理地址,否則需要老老實實得進行查表操作。同時當程序地址空間中地址對映關係改變後,TLB需要同步重新整理。

不同於快取記憶體,TLB中放的對映關係跟OS相關,不由硬體決定,所以需要OS來重新整理TLB(頁表改變時,即對映關係改變時需要重新整理)。常見的重新整理場景有 :

1)程序切換(使用相同程序頁表切換,以及核心使用者程序切換,不需要重新整理);

2)頁表更改:有新物理地址對映,這時候需要更新TLB;

關於TLB的詳細解釋,可以參考smcdef寫的一篇《TLB原理》,寫的非常詳細易懂。

linux 分頁機制實現

在《x86段暫存器和CPU定址》中我們簡要提到過,linux系統把x86所有段基地址(圖中段描述符base)全部置零,即所有段都從0x00000000開始,遮蔽了分段機制,因此

在linux中邏輯地址等於線性地址

!分頁機制就是linux採用的整個記憶體管理全部。

linux系統採用了一種同時適用於32位和64位系統的普通分頁模型。從kernel 2。6。11版本開始,linux使用的四級頁表型如下:

x86的分頁機制和Linux實現

圖5 linux 64位線性地址劃分

可以看到和x86_64的線性地址劃分一致!實際上linux的四級頁表就是用來全力支援x86_64平臺而設立的。只不過在linux中各個項命名不同,和x86_64佈局一一對應:

1)

PGD

:頁全域性目錄(page global directory),多級頁表的抽象最高層 宏:PGDIR_SHIFT

2)

PUD

:頁上級目錄(page upper directory) 宏:PUD_SHIFT

3)

PMD

:頁中間目錄( page middle directory), 頁表的中間層 宏:PMD_SHIFT

4)

PTE

:頁表(page table entry) 頁表 宏:PAGE_SHIFT

5)

OFFSET

:頁偏移量(offset),具體物理地址頁內偏移

linux使用四級頁表模型相容大部分的地址劃分需求,具體系統使用的是幾級頁表取決於硬體對線性地址的位的劃分!下面是四級頁表的地址轉換關係:

x86的分頁機制和Linux實現

圖6 linux四級頁表模型

linux每個程序有自己獨有的頁表,頁表物理基地址儲存在程序描述符中,程序切換時將該程序的pgd放到CR3暫存器中;

另外linux 核心維持著一組自己使用的頁表,即主核心頁全域性目錄。當核心在初始化完成後,其存放在

swapper_pg_dir

全域性變數中,swapper_pg_dir其實就是一個頁目錄的指標,執行核心程序時載入到cr3暫存器中。注意,核心頁表是所有程序共享的,

每個程序的程序頁表中核心態地址相關的頁表項都是核心頁表的一個複製

。目的是程序在呼叫syscall陷入核心時不需要重新整理cr3和TLB,較少系統呼叫開銷。程序頁表中的“核心頁表”副本採用延遲更新策略,即當核心頁表更新時(比如vmalloc申請記憶體),並不會更新程序中的核心頁表副本,只有當該程序陷入核心訪問相應核心地址,產生缺頁異常時,才會更新副本!

不過swapper_pg_dir只是在核心初始化的時候被載入到cr3指示記憶體對映資訊,之後在init程序啟動後就成了idle核心執行緒的頁目錄指標。Linux中swapper_pg_dir定義的位置:arch/x86/include/asm/pgtable_64。h

下面從原始碼中看下,程序切換時CR3切換pgd過程。linux程序切換核心函式是

__schedule()

,涉及頁表基地址切換的函式呼叫棧如下:

__schedule

()

└→

context_switch

()

└→

switch_mm_irqs_off

()

└→

load_new_mm_cr3

next_mm

->

pgd

new_asid

true

);

核心函式load_new_mm_cr3 ()函式:

static

void

load_new_mm_cr3

pgd_t

*

pgdir

u16

new_asid

bool

need_flush

{

unsigned

long

new_mm_cr3

if

need_flush

{

invalidate_user_asid

new_asid

);

new_mm_cr3

=

build_cr3

pgdir

new_asid

);

}

else

{

new_mm_cr3

=

build_cr3_noflush

pgdir

new_asid

);

}

/*

* Caution: many callers of this function expect

* that load_cr3() is serializing and orders TLB

* fills with respect to the mm_cpumask writes。

*/

write_cr3

new_mm_cr3

);

}

linux將程序的頁表物理基地址放在task_struct->mm_struct->pgd 中,此函式入參pgdir 就是上級傳下來的下個執行程序的頁表物理基地址。build_cr3/build_cr3_noflush 將程序頁表基地址pgd處理成CR3中儲存的bit位置,呼叫write_cr3寫入CR3暫存器中。

核心資料結構

【1】頁描述符和頁大小

linux使用 struct page 來描述一個物理頁框,用於記憶體管理的夥伴系統也是以頁為基本單位。x86_64頁有三種大小,4KB,2MB,1GB,在linux中定義如下(arch/x86/include/asm/page_types。h):

x86的分頁機制和Linux實現

x86的分頁機制和Linux實現

【2】頁表處理資料結構

Linux分別採用pgd_t、pmd_t、pud_t和pte_t四種資料結構來表示頁全域性目錄項、頁上級目錄項、頁中間目錄項和頁表項。在x86中實現(include/asm-generic/page。h):

x86的分頁機制和Linux實現

x86的分頁機制和Linux實現

我們知道 pte是頁表項,儲存的是頁框基地址。因為系統將物理記憶體劃分成4KB的頁框,頁框基地址是4KB地址對齊,因此pte中低12bit必然為零。linux使用這部分空間儲存頁表的屬性資訊:

x86的分頁機制和Linux實現

同理根據地址對齊,頁目錄項中也有屬性段。

頁目錄項

頁表項

中屬性資訊包含(arch/x86/include/asm/pgtable_types。h):

x86的分頁機制和Linux實現

重要成員有:

(1)

PRESENT

置1 表示頁在主存中,置0表示不在主存。當執行一個地址轉換所需頁表項或頁目錄PRESENT標誌被清零,分頁單元會執行操作產生缺頁異常,然後分配物理頁框。即程序在訪問具體地址時才會真正對映物理記憶體!

(2)

ACCESSED

頁框分配給頁後分頁單元置位表示頁被訪問,在頁被交換出去後由作業系統重置為0。

(3)

DIRTY

在寫頁框時置位1,只存在頁表項中。

非4KB頁的頁表中,頁表項有更多空間儲存屬性資訊,超過12bit的屬性資訊這裡不再羅列,但興趣的可以去kernel 原始碼中查詢。

【3】

頁表描述宏

前面圖5提到,四級頁表把線性地址分成五段,比如x86_64 為9+9+9+9+12,Linux使用下面的宏來劃分線性地址空間:

1)四級頁表

x86的分頁機制和Linux實現

2)三級頁表

x86的分頁機制和Linux實現

3)兩級頁表

x86的分頁機制和Linux實現

【4】

頁表處理

linux核心提供了很多宏和函式用於讀取或修改頁表項。這裡以ptr_present為例:

x86的分頁機制和Linux實現

函式中,pte_flags是做掩碼計算,掩碼PTE_FLAGS_MASK 就是“~(PAGE_SIZE-1)”,即對於4kb頁掩碼是12bit。

當pte中PRESENT bit 為0時,分頁單元會執行操作產生缺頁異常,表示頁不在主存中,可能是未建立對映或者非法地址!

補充:關於核心五級頁表

X86的4級頁表已經能夠管理48bit(256TB)的VA,以及64TB的PA。不過由於某些供應商釋出了超過64T的超大物理記憶體,新的Intel晶片的MMU硬體規定可以進行5級頁表管理。擴充套件9位達到57bit VA和52bit PA,支援喪心病狂的128PB VA和4PB PA,甚至大頁都達到256G。因此需要實現了一個5級頁表特性來進行支援。

核心在PGD和PUD之間,增加了一個叫P4D的層次:

PGD->P4D->PUD->PMD->PTE

不過新的5級頁表可以透過核心cmdline來控制核心啟用4級還是5級頁表,這個比原先4級頁表的支援又智慧了不少(OS發行版不用出兩個版本了)。

使用了五級頁表的虛擬地址空間劃分可以參考核心文件:

/Documentation/x86/x86_64/mm。txt

由於五級頁表應用場景少,還不夠普遍,本文不再深入研究,後續遇到再詳細分析。

參考

《深入理解計算機體系結構》

《深入理解linux核心》第三版

注:所有核心原始碼均來自kernel 4。18。0