前言 Linux支持很多硬件運行平台,常用的有:Intel X86,Alpha,Sparc等。對於不能夠通用的一些功能,Linux必須依據硬件平台的特點來具體實現。本文的目的是簡要探討Linux在X86保護模式上如何實現虛擬內存管理功能。為簡化和方便敘述,本文做如下限定:X86處理器為80486和其後的處理器,X86工作在保護模式,不采用物理內存擴展(使用32bits物理地址),不使用擴展頁(頁大小為4K)。凡是與限定模式無關的內容,本文都盡量略過。Linux的虛擬內存管理中與硬件平台無關的內容在本文中也被略過。本文所援引的Linux內核源代碼版本為Linux 2.2.5。
X86的分段和分頁機制 I. X86的分段機制和相應系統結構 X86的分段機制就是將X86的線性地址空間分成許多小空間--段(segment),利用這些段來存儲(記錄)代碼和數據,通過對段的保護來提供一種對數據或代碼的保護。根據每個段的作用和存儲內容的不同,X86將段分為三類進程段(代碼段、數據段和堆棧段)和兩類系統段:任務狀態段(TSS,Task-State Segment)和LDT段(由於GDT不是通過段描述符和段選擇符來訪問,所以X86沒有認為存在一個GDT段;同理,也不存在IDT段)。 在分段機制,X86使用了如下幾種主要數據結構: • 全局描述符表(GDT,Global DescriBTor Table):存放系統用的段描述符和各項任務共用的段描述符,可以是上述的任何一類段的段描述符,最大表長64KB; • 局部描述符表(LDT,Local Describtor Table):存放某個任務專用的各段的段描述符,只能是三類進程段的段描述符和調用門描述符,最大表長4GB; • 段描述符(Segment Describtor):64bits,用來描述一個段的基地址(該地址是線性地址),該段的類型,對該段操作的限制; • 門描述符(Gate Describtor):64bits,一種特殊的描述符,為處於不同特權級的系統調用或程序的調用或訪問提供保護;分為四類:調用門描述符(Call Gate Describtor)、中斷門描述符(Interrupt Gate Describtor)、陷阱門描述符(Trap Gate Describtor)、任務門描述符(Task Gate Describtor); • 段選擇符(Segment Selector):16bits,用於在GDT或LDT中索引相應的段描述符; • 中斷描述表(IDT,Interrupt Describer Table):存放門描述符,只能是中斷門描述符,陷阱門描述符和任務門描述符,最大表長64KB; 同時,X86提供了如下幾個用於支持分段機制的寄存器: • 全局描述符表寄存器(GDTR,GDT Register):48bits,32bits為GDT的基地址(線性地址),16bits為GDT的表長;GDTR的初始值為:基地址0,表長0xFFFF; • 局部描述符表寄存器(LDTR,LDT Register):80bits,16bits為LDT段選擇符,64bits為該LDT段的段描述符; • 中斷描述符表寄存器(IDTR,IDT Register):48bits,32bits為IDT的基地址(線性地址),16bits為IDT的表長;IDTR的初始值為:基地址0,表長0xFFFF; • 任務寄存器(TR,Task Register):80bits,16bits為任務狀態段選擇符,64bits為該任務狀態段的段描述符; • 六個段寄存器(Segment Register):分為可見部分和隱藏部分,可見部分為段選擇符,隱藏部分為段描述符;六個段寄存器分別為CS、SS、DS、ES、FS、GS;關於這些段寄存器的作用參見[1]中3.4.2 'Segment Register'; 86工作在保護模式時,進程使用的48bits邏輯地址(Logical address)。邏輯地址的高16bits為段選擇符,低32bits是段內的偏移量。通過段選擇符在GDT或LDT中索引相應的段描述符(得到該段的基地址),再加上偏移量得到邏輯地址對應的線性地址(Linear Address)。如果沒有采用分葉管理,線性地址是直接映射物理地址(Physical Address),於是可以直接用線性地址訪問內存;否則,還要通過X86的分頁轉換,將線性地址轉換為物理地址。 以上是對X86分段相關內容的簡要描述,對於各數據結構、寄存器的細節和邏輯地址轉換為線性地址的細節,請查閱 [1]。
II. X86的分頁機制和相應系統結構 32bits的線性地址空間可以直接映射到物理地址空間,也可以間接映射到許多小塊的物理空間(磁盤存儲空間)上。這種間接映射方式就是分頁機制。X86可用頁大小為4KB、2MB和4MB(2MB和4MB只能在Pentium和Pentium Pro處理器中使用,本文中限定采用4KB頁)。 在分頁機制,X86使用了四種數據結構: • 頁目錄項(PDE,Page Directory Entry):32bits結構,高20bits為頁表基地址(物理地址),以4KB為遞增單位,低12bits為頁表屬性,具體換算參見後面初始化部分; • 頁目錄(Page directory):存儲頁目錄項,位於一頁中,總共可容納1024個頁目錄項; • 頁表項(PTE,Page Table Entry):32bits結構,高20bits為頁基地址(物理地址),低12bits為頁屬性; • 頁表(Page table):存儲頁表項,位於一頁中,總共可容納1024個頁表項; • 頁(Page):4KB的連續地址空間; 為了實現分頁機制和提高地址轉換的效率,X86提供和使用了如下的硬件結構: • 頁標志位(PG,Page):該標志位為1,說明采用頁機制;實際就是控制寄存器CR0的第31bit; • 頁緩存/快表(TLBs,Translation Lookaside Buffers):存儲最近使用的PDE和PTE,以提高地址轉換的效率; • 頁目錄基地址寄存器(PDBR,Page Directory Base Register):用於存儲頁目錄的基地址(物理地址),實際就是控制寄存器CR3; 為了實現將線性地址映射到物理地址,X86將32bits線性地址解釋為三部分:第31bit到第22bit為頁目錄中的偏移,用於索引頁目錄項(得到對應頁表的基地址);第21bit到第12bit為頁表中的偏移,用於索引頁表項(得到對應頁的基地址);第11bit到第0bit為頁中的偏移。這樣,通過兩級索引和頁中的偏移量,最後能正確得到線性地址對應的物理地址。 關於分頁機制的詳細描述和作用,請查閱參考文檔[1]。
LINUX的分段策略 Linux在X86上采用最低限度的分段機制,其目的是為了避開復雜的分段機制,提高Linux在其他不支持分段機制的硬件平台的可移植性,同時又充分利用X86的分段機制來隔離用戶代碼和內核代碼。因此,在Linux上,邏輯地址和線性地址具有相同的值。 由於X86的GDT最大表長為64KB,每個段描述符為8B,所以GDT最多能夠容納8192個段描述符。每產生一個進程,Linux為該進程在GDT中創建兩個描述符:LDT段描述符和TSS描述符,除去Linux在GDT中保留的前12項,GDT實際最多能容納4090個進程。Linux的內核自身有獨立的代碼段和數據段,其對應的段描述符分別存儲在GDT中的第2項和第3項。每個進程也有獨立的代碼段和數據段,對應的段描述符存儲在它自己的LDT中。有關LinuxGDT表項和DLT表項分布情況參見附表1,附表2所示。 在Linux中,每個用戶進程都可以訪問4GB的線性地址空間。其中0x0~0xBFFFFFFF的3GB空間為用戶態空間,用戶態進程可以直接訪問。從0xC0000000~0x3FFFFFFF的1GB空間為內核態空間,存放內核訪問的代碼和數據,用戶態進程不能直接訪問。當用戶進程通過中斷或系統調用訪問內核態空間時,會觸發X86的特權級轉換(從特權級3切換到特權級0),即從用戶態切換到內核態。
LINUX的分頁策略 標准Linux的分頁是三級頁表結構,除了X86支持的頁目錄和頁,還有一級被稱為中間頁目錄。因此,線性地址在轉換為物理地址的過程中,線性地址就被解釋為四個部分(不是X86所認識的三個部分),增加了頁中間目錄中的索引。當運行在X86平台上時,Linux通過將中間頁目錄最大的頁目錄項個數定義為1,並提供一組相關的宏(這些宏將中間頁目錄用頁目錄來替換)將三級頁面結構分解過程完美的轉換為X86使用的二級頁面分解。這樣,無需改動內核中頁面解釋的主要代碼(這些代碼都是認為線性地址由四個部分組成)。關於這些宏定義參見Linux源碼"/include/asm/pgtable.h","/include/asm/page.h"。 內核態虛擬空間從3GB到3GB+4MB的一段(對應進程頁目錄第768項指引的頁表),被映射到物理地址0x0~0x3FFFFF(4MB)。因此,進程處於內核態時,只要通過訪問3GB到3GB+4MB就可訪問物理內存的低4MB空間。所有進程從3GB到4GB的線性空間都是一樣的,由同樣的頁目錄項,同樣的頁表,映射到相同的物理內存段。Linux以這種方式讓內核態進程共享代碼和數據。
Linux分段分頁初始化 無論Linux系統如何被引導,經過zImage(參見arch/i386/boot/bootsect.s)或經過LILO,最後都會跳轉執行arch/i386/boot/setup.s(被裝載到SETUPSEG,物理地址 0x90200),setup.s從BIOS中獲取計算機系統的硬件參數(如硬盤參數),放到內存參數區(臨時寄放),同時做一些初步的狀態檢查,為進入保護模式做准備。關於引導過程和setup.s的具體執行參見[2]。 保護模式下的內核初始化模塊從物理地址0x100000開始執行,該地址開始的代碼和數據結構都對應在arch/i386/kernel/head.s中,參見附表3。初始化模塊主要功能是對相關寄存器IDT,GDT,頁目錄及頁表等進行初始化。下面,忽略head.s執行流程的細節,概要闡述head.s主要的初始化功能。 1. 部分寄存器的初始化:將段寄存器DS、ES、GS和FS用__KERNEL_DS(0x18,include/asm-i386/segment.h)來初始化(通過前面對段寄存器的描述和段選擇符的介紹可知道,其作用是將定位到GDT中的第三項(內核數據段),並設置對該段的操作特限級為0);置位CR0的PG位,並根據CPU的型號選擇置位AM, WP, NE 和 MP;用0x101000初始化CR3(頁目錄swapper_pg_dir的地址);置ESP高32bits為__KERNEL_DS(0x18),低32bits為init_user_stack+8192;LDTR