直播中
前言
Linux支持很多硬件運(yùn)行平臺(tái),常用的有:Intel X86,Alpha,Sparc等。對(duì)于不能夠通用的一些功能,Linux必須依據(jù)硬件平臺(tái)的特點(diǎn)來(lái)具體實(shí)現(xiàn)。本文的目的是簡(jiǎn)要探討Linux在X86保護(hù)模式上如何實(shí)現(xiàn)虛擬內(nèi)存管理功能。為簡(jiǎn)化和方便敘述,本文做如下限定:X86處理器為80486和其后的處理器,X86工作在保護(hù)模式,不采用物理內(nèi)存擴(kuò)展(使用32bits物理地址),不使用擴(kuò)展頁(yè)(頁(yè)大小為4K)。凡是與限定模式無(wú)關(guān)的內(nèi)容,本文都盡量略過(guò)。Linux的虛擬內(nèi)存管理中與硬件平臺(tái)無(wú)關(guān)的內(nèi)容在本文中也被略過(guò)。本文所援引的Linux內(nèi)核源代碼版本為L(zhǎng)inux 2.2.5。
X86的分段和分頁(yè)機(jī)制
I. X86的分段機(jī)制和相應(yīng)系統(tǒng)結(jié)構(gòu)
X86的分段機(jī)制就是將X86的線性地址空間分成許多小空間--段(segment),利用這些段來(lái)存儲(chǔ)(記錄)代碼和數(shù)據(jù),通過(guò)對(duì)段的保護(hù)來(lái)提供一種對(duì)數(shù)據(jù)或代碼的保護(hù)。根據(jù)每個(gè)段的作用和存儲(chǔ)內(nèi)容的不同,X86將段分為三類(lèi)進(jìn)程段(代碼段、數(shù)據(jù)段和堆棧段)和兩類(lèi)系統(tǒng)段:任務(wù)狀態(tài)段(TSS,Task-State Segment)和LDT段(由于GDT不是通過(guò)段描述符和段選擇符來(lái)訪問(wèn),所以X86沒(méi)有認(rèn)為存在一個(gè)GDT段;同理,也不存在IDT段)。
在分段機(jī)制,X86使用了如下幾種主要數(shù)據(jù)結(jié)構(gòu):
· 全局描述符表(GDT,Global Describtor Table):存放系統(tǒng)用的段描述符和各項(xiàng)任務(wù)共用的段描述符,可以是上述的任何一類(lèi)段的段描述符,最大表長(zhǎng)64KB;
· 局部描述符表(LDT,Local Describtor Table):存放某個(gè)任務(wù)專(zhuān)用的各段的段描述符,只能是三類(lèi)進(jìn)程段的段描述符和調(diào)用門(mén)描述符,最大表長(zhǎng)4GB;
· 段描述符(Segment Describtor):64bits,用來(lái)描述一個(gè)段的基地址(該地址是線性地址),該段的類(lèi)型,對(duì)該段操作的限制;
· 門(mén)描述符(Gate Describtor):64bits,一種特殊的描述符,為處于不同特權(quán)級(jí)的系統(tǒng)調(diào)用或程序的調(diào)用或訪問(wèn)提供保護(hù);分為四類(lèi):調(diào)用門(mén)描述符(Call Gate Describtor)、中斷門(mén)描述符(Interrupt Gate Describtor)、陷阱門(mén)描述符(Trap Gate Describtor)、任務(wù)門(mén)描述符(Task Gate Describtor);
· 段選擇符(Segment Selector):16bits,用于在GDT或LDT中索引相應(yīng)的段描述符;
· 中斷描述表(IDT,Interrupt Describer Table):存放門(mén)描述符,只能是中斷門(mén)描述符,陷阱門(mén)描述符和任務(wù)門(mén)描述符,最大表長(zhǎng)64KB;
同時(shí),X86提供了如下幾個(gè)用于支持分段機(jī)制的寄存器:
· 全局描述符表寄存器(GDTR,GDT Register):48bits,32bits為GDT的基地址(線性地址),16bits為GDT的表長(zhǎng);GDTR的初始值為:基地址0,表長(zhǎng)0xFFFF;
· 局部描述符表寄存器(LDTR,LDT Register):80bits,16bits為L(zhǎng)DT段選擇符,64bits為該LDT段的段描述符;
· 中斷描述符表寄存器(IDTR,IDT Register):48bits,32bits為IDT的基地址(線性地址),16bits為IDT的表長(zhǎng);IDTR的初始值為:基地址0,表長(zhǎng)0xFFFF;
· 任務(wù)寄存器(TR,Task Register):80bits,16bits為任務(wù)狀態(tài)段選擇符,64bits為該任務(wù)狀態(tài)段的段描述符;
· 六個(gè)段寄存器(Segment Register):分為可見(jiàn)部分和隱藏部分,可見(jiàn)部分為段選擇符,隱藏部分為段描述符;六個(gè)段寄存器分別為CS、SS、DS、ES、FS、GS;關(guān)于這些段寄存器的作用參見(jiàn)[1]中3.4.2 'Segment Register';
86工作在保護(hù)模式時(shí),進(jìn)程使用的48bits邏輯地址(Logical address)。邏輯地址的高16bits為段選擇符,低32bits是段內(nèi)的偏移量。通過(guò)段選擇符在GDT或LDT中索引相應(yīng)的段描述符(得到該段的基地址),再加上偏移量得到邏輯地址對(duì)應(yīng)的線性地址(Linear Address)。如果沒(méi)有采用分葉管理,線性地址是直接映射物理地址(Physical Address),于是可以直接用線性地址訪問(wèn)內(nèi)存;否則,還要通過(guò)X86的分頁(yè)轉(zhuǎn)換,將線性地址轉(zhuǎn)換為物理地址。
以上是對(duì)X86分段相關(guān)內(nèi)容的簡(jiǎn)要描述,對(duì)于各數(shù)據(jù)結(jié)構(gòu)、寄存器的細(xì)節(jié)和邏輯地址轉(zhuǎn)換為線性地址的細(xì)節(jié),請(qǐng)查閱 [1]。
II. X86的分頁(yè)機(jī)制和相應(yīng)系統(tǒng)結(jié)構(gòu)
32bits的線性地址空間可以直接映射到物理地址空間,也可以間接映射到許多小塊的物理空間(磁盤(pán)存儲(chǔ)空間)上。這種間接映射方式就是分頁(yè)機(jī)制。X86可用頁(yè)大小為4KB、2MB和4MB(2MB和4MB只能在Pentium和Pentium Pro處理器中使用,本文中限定采用4KB頁(yè))。
在分頁(yè)機(jī)制,X86使用了四種數(shù)據(jù)結(jié)構(gòu):
· 頁(yè)目錄項(xiàng)(PDE,Page Directory Entry):32bits結(jié)構(gòu),高20bits為頁(yè)表基地址(物理地址),以4KB為遞增單位,低12bits為頁(yè)表屬性,具體換算參見(jiàn)后面初始化部分;
· 頁(yè)目錄(Page directory):存儲(chǔ)頁(yè)目錄項(xiàng),位于一頁(yè)中,總共可容納1024個(gè)頁(yè)目錄項(xiàng);
· 頁(yè)表項(xiàng)(PTE,Page Table Entry):32bits結(jié)構(gòu),高20bits為頁(yè)基地址(物理地址),低12bits為頁(yè)屬性;
· 頁(yè)表(Page table):存儲(chǔ)頁(yè)表項(xiàng),位于一頁(yè)中,總共可容納1024個(gè)頁(yè)表項(xiàng);
· 頁(yè)(Page):4KB的連續(xù)地址空間;
為了實(shí)現(xiàn)分頁(yè)機(jī)制和提高地址轉(zhuǎn)換的效率,X86提供和使用了如下的硬件結(jié)構(gòu):
· 頁(yè)標(biāo)志位(PG,Page):該標(biāo)志位為1,說(shuō)明采用頁(yè)機(jī)制;實(shí)際就是控制寄存器CR0的第31bit;
· 頁(yè)緩存/快表(TLBs,Translation Lookaside Buffers):存儲(chǔ)最近使用的PDE和PTE,以提高地址轉(zhuǎn)換的效率;
· 頁(yè)目錄基地址寄存器(PDBR,Page Directory Base Register):用于存儲(chǔ)頁(yè)目錄的基地址(物理地址),實(shí)際就是控制寄存器CR3;
為了實(shí)現(xiàn)將線性地址映射到物理地址,X86將32bits線性地址解釋為三部分:第31bit到第22bit為頁(yè)目錄中的偏移,用于索引頁(yè)目錄項(xiàng)(得到對(duì)應(yīng)頁(yè)表的基地址);第21bit到第12bit為頁(yè)表中的偏移,用于索引頁(yè)表項(xiàng)(得到對(duì)應(yīng)頁(yè)的基地址);第11bit到第0bit為頁(yè)中的偏移。這樣,通過(guò)兩級(jí)索引和頁(yè)中的偏移量,最后能正確得到線性地址對(duì)應(yīng)的物理地址。
關(guān)于分頁(yè)機(jī)制的詳細(xì)描述和作用,請(qǐng)查閱參考文檔[1]。
LINUX的分段策略
Linux在X86上采用最低限度的分段機(jī)制,其目的是為了避開(kāi)復(fù)雜的分段機(jī)制,提高Linux在其他不支持分段機(jī)制的硬件平臺(tái)的可移植性,同時(shí)又充分利用X86的分段機(jī)制來(lái)隔離用戶代碼和內(nèi)核代碼。因此,在Linux上,邏輯地址和線性地址具有相同的值。
由于X86的GDT最大表長(zhǎng)為64KB,每個(gè)段描述符為8B,所以GDT最多能夠容納8192個(gè)段描述符。每產(chǎn)生一個(gè)進(jìn)程,Linux為該進(jìn)程在GDT中創(chuàng)建兩個(gè)描述符:LDT段描述符和TSS描述符,除去Linux在GDT中保留的前12項(xiàng),GDT實(shí)際最多能容納4090個(gè)進(jìn)程。Linux的內(nèi)核自身有獨(dú)立的代碼段和數(shù)據(jù)段,其對(duì)應(yīng)的段描述符分別存儲(chǔ)在GDT中的第2項(xiàng)和第3項(xiàng)。每個(gè)進(jìn)程也有獨(dú)立的代碼段和數(shù)據(jù)段,對(duì)應(yīng)的段描述符存儲(chǔ)在它自己的LDT中。有關(guān)LinuxGDT表項(xiàng)和DLT表項(xiàng)分布情況參見(jiàn)附表1,附表2所示。
在Linux中,每個(gè)用戶進(jìn)程都可以訪問(wèn)4GB的線性地址空間。其中0x0~0xBFFFFFFF的3GB空間為用戶態(tài)空間,用戶態(tài)進(jìn)程可以直接訪問(wèn)。從0xC0000000~0x3FFFFFFF的1GB空間為內(nèi)核態(tài)空間,存放內(nèi)核訪問(wèn)的代碼和數(shù)據(jù),用戶態(tài)進(jìn)程不能直接訪問(wèn)。當(dāng)用戶進(jìn)程通過(guò)中斷或系統(tǒng)調(diào)用訪問(wèn)內(nèi)核態(tài)空間時(shí),會(huì)觸發(fā)X86的特權(quán)級(jí)轉(zhuǎn)換(從特權(quán)級(jí)3切換到特權(quán)級(jí)0),即從用戶態(tài)切換到內(nèi)核態(tài)。
LINUX的分頁(yè)策略
標(biāo)準(zhǔn)Linux的分頁(yè)是三級(jí)頁(yè)表結(jié)構(gòu),除了X86支持的頁(yè)目錄和頁(yè),還有一級(jí)被稱(chēng)為中間頁(yè)目錄。因此,線性地址在轉(zhuǎn)換為物理地址的過(guò)程中,線性地址就被解釋為四個(gè)部分(不是X86所認(rèn)識(shí)的三個(gè)部分),增加了頁(yè)中間目錄中的索引。當(dāng)運(yùn)行在X86平臺(tái)上時(shí),Linux通過(guò)將中間頁(yè)目錄最大的頁(yè)目錄項(xiàng)個(gè)數(shù)定義為1,并提供一組相關(guān)的宏(這些宏將中間頁(yè)目錄用頁(yè)目錄來(lái)替換)將三級(jí)頁(yè)面結(jié)構(gòu)分解過(guò)程完美的轉(zhuǎn)換為X86使用的二級(jí)頁(yè)面分解。這樣,無(wú)需改動(dòng)內(nèi)核中頁(yè)面解釋的主要代碼(這些代碼都是認(rèn)為線性地址由四個(gè)部分組成)。關(guān)于這些宏定義參見(jiàn)Linux源碼"/include/asm/pgtable.h","/include/asm/page.h"。
內(nèi)核態(tài)虛擬空間從3GB到3GB+4MB的一段(對(duì)應(yīng)進(jìn)程頁(yè)目錄第768項(xiàng)指引的頁(yè)表),被映射到物理地址0x0~0x3FFFFF(4MB)。因此,進(jìn)程處于內(nèi)核態(tài)時(shí),只要通過(guò)訪問(wèn)3GB到3GB+4MB就可訪問(wèn)物理內(nèi)存的低4MB空間。所有進(jìn)程從3GB到4GB的線性空間都是一樣的,由同樣的頁(yè)目錄項(xiàng),同樣的頁(yè)表,映射到相同的物理內(nèi)存段。Linux以這種方式讓內(nèi)核態(tài)進(jìn)程共享代碼和數(shù)據(jù)。
Linux分段分頁(yè)初始化
無(wú)論Linux系統(tǒng)如何被引導(dǎo),經(jīng)過(guò)zImage(參見(jiàn)arch/i386/boot/bootsect.s)或經(jīng)過(guò)LILO,最后都會(huì)跳轉(zhuǎn)執(zhí)行arch/i386/boot/setup.s(被裝載到SETUPSEG,物理地址 0x90200),setup.s從BIOS中獲取計(jì)算機(jī)系統(tǒng)的硬件參數(shù)(如硬盤(pán)參數(shù)),放到內(nèi)存參數(shù)區(qū)(臨時(shí)寄放),同時(shí)做一些初步的狀態(tài)檢查,為進(jìn)入保護(hù)模式做準(zhǔn)備。關(guān)于引導(dǎo)過(guò)程和setup.s的具體執(zhí)行參見(jiàn)[2]。
保護(hù)模式下的內(nèi)核初始化模塊從物理地址0x100000開(kāi)始執(zhí)行,該地址開(kāi)始的代碼和數(shù)據(jù)結(jié)構(gòu)都對(duì)應(yīng)在arch/i386/kernel/head.s中,參見(jiàn)附表3。初始化模塊主要功能是對(duì)相關(guān)寄存器IDT,GDT,頁(yè)目錄及頁(yè)表等進(jìn)行初始化。下面,忽略head.s執(zhí)行流程的細(xì)節(jié),概要闡述head.s主要的初始化功能。
1. 部分寄存器的初始化:將段寄存器DS、ES、GS和FS用__KERNEL_DS(0x18,include/asm-i386/segment.h)來(lái)初始化(通過(guò)前面對(duì)段寄存器的描述和段選擇符的介紹可知道,其作用是將定位到GDT中的第三項(xiàng)(內(nèi)核數(shù)據(jù)段),并設(shè)置對(duì)該段的操作特限級(jí)為0);置位CR0的PG位,并根據(jù)CPU的型號(hào)選擇置位AM, WP, NE 和 MP;用0x101000初始化CR3(頁(yè)目錄swapper_pg_dir的地址);置ESP高32bits為_(kāi)_KERNEL_DS(0x18),低32bits為init_user_stack+8192;LDTR初始化為0。
2. 有關(guān)IDT的初始化:這只是臨時(shí)初始化IDT,進(jìn)一步的操作在start_kernel中進(jìn)行;用于表示IDT的變量(idt_table[ ])在arch/i386/kenel/traps.c中定義,變量類(lèi)型(desc_struct)定義在include/asm-i386/desc.h。IDT共有IDT_ENTRIES(256)個(gè)中斷描述符,屬性字均為0x8E00,每個(gè)中斷描述符都指向同一個(gè)中斷服務(wù)程序ignore_init。Ignore_int的功能僅僅是輸出消息int_msg("unknown interrupt")。而IDTR的值為通過(guò)命令lidt idt_descr實(shí)現(xiàn)。通過(guò)在head.s中查看idt_descr的值可以計(jì)算得知,IDT的基地址為idt_table的地址,表長(zhǎng)IDT_ENTRIES*8-1(0x7FF)。
3. 有關(guān)GDT的初始化:GDT共有GDT_ENTRIES個(gè)段描述符。GDT_ENTRIES的計(jì)算公式為:12+2*NR_TASKS。其中12表示前面提到的Linux在GDT中保留的12項(xiàng),NR_TASKS(512)指系統(tǒng)設(shè)定容納的進(jìn)程數(shù),定義在include/linux/tasks.h。GDT在head.s直接分配存儲(chǔ)單元(標(biāo)號(hào)為gdt_table)。初始化后的GDT如附表1所示。GDTR的值通過(guò)命令lgdt gdt_descr實(shí)現(xiàn)。通過(guò)在head.s中查看gdt_descr的值可以計(jì)算得知,GDT的基地址為gdt_table的地址,表長(zhǎng)GDT_ENTRIES*8-1(0x205F)。
4. 頁(yè)目錄的初始化:頁(yè)目錄由變量swapper_pg_dir表示,共有1024個(gè)頁(yè)目錄項(xiàng)。其第0項(xiàng)和第768項(xiàng)均指向pg0(第0頁(yè)),初始化值為0x00102007(根據(jù)其高20bits的值0x102換算:0x102*4KB=0x102000,第0頁(yè)緊跟頁(yè)目錄后,物理地址為0x102000),由此可知,Linux 4GB空間中的虛擬地址0x0和0xBFFFFFFF(3GB)均由pg0映射(物理地址0x0~0x3FFFFF(4MB));其他頁(yè)目錄項(xiàng)初始值為0x0;
5. pg0的初始化:第n項(xiàng)對(duì)應(yīng)第n頁(yè),屬性為0x007;即第n項(xiàng)的初始化值的高20bits值為n,底12bits值為0x007;由此可見(jiàn)pg0映射了物理空間的低4MB空間;
6. 初始化empty_zero_page:該頁(yè)的前2KB空間用來(lái)存儲(chǔ)setup.s保存在內(nèi)存參數(shù)區(qū)的來(lái)自BIOS的系統(tǒng)硬件參數(shù);后2KB空間作為命令行緩沖區(qū);
head.s進(jìn)行完初始化后調(diào)用start_kernel(init/main.c)繼續(xù)各方面的初始化,主要是調(diào)用各方面函數(shù)初始化內(nèi)核的數(shù)據(jù)結(jié)構(gòu),下面對(duì)與X86系統(tǒng)相關(guān)的調(diào)用函數(shù)簡(jiǎn)述其(與本文相關(guān)的)功能。
1. setup_arch() (arch/i386/kernel/setup.c);設(shè)置內(nèi)核可用物理地址范圍(memory_start~memory_end);設(shè)置init_task.mm的范圍;調(diào)用request_region(kernel/resource.c)申請(qǐng)I/O空間,參見(jiàn)附表4。
2. paging_init() (arch/i386/mm/init.c);取消虛擬地址0x0對(duì)物理地址的低端4MB空間的映射;根據(jù)物理地址的實(shí)際大小初始化所有的頁(yè)表。
3. trap_init() (arch/i386/kernel/traps.c);在IDT中設(shè)置各種入口地址,如異常事件處理程序入口,系統(tǒng)調(diào)用入口,調(diào)用門(mén)等。其中,trap0~trap17為各種錯(cuò)誤入口(溢出,0除,頁(yè)錯(cuò)誤等,錯(cuò)誤處理函數(shù)定義在arch/i386/kernel/entry.s);trap18~trap47保留;設(shè)置系統(tǒng)調(diào)用(INT 0x80)的入口為system_call(arch/i386/kernel/entry.s);在GDT中設(shè)置0號(hào)進(jìn)程的TSS段描述符和LDT段描述符。
4. init_IRQ() (arch/i386/kernel/irq.c);初始化IDT 中0x20~0xff項(xiàng)。
5. time_init() (arch/i386/kernel/time.c);讀取實(shí)時(shí)時(shí)間,重新設(shè)置時(shí)鐘中斷irq0的中斷服務(wù)程序入口。
6. mem_init() (arch/i386/mm/init.c);初始化empty_zero_page;標(biāo)記已被占用的頁(yè)。
Linux進(jìn)程和分段分頁(yè)
每當(dāng)啟動(dòng)一個(gè)新的進(jìn)程,Linux都為其創(chuàng)建一個(gè)進(jìn)程控制塊(task_struct,include/linux/sched.h)。task_struct中最重要的與存儲(chǔ)有關(guān)的成員為mm(mm_struct* mm,include/linux/sched.h)和tss(thread_struct tss,include/asm-i386/processor.h)。在創(chuàng)建過(guò)程中,系統(tǒng)所涉及的(與分段分頁(yè)相關(guān))功能包括:
1. 每個(gè)進(jìn)程(根據(jù)需要)建立新頁(yè)目錄(mm成員pgd_t * pgd),并將其地址置入寄存器CR3中;相關(guān)代碼:
new_page_tables(mm/memory.c);//創(chuàng)建和初始化新頁(yè)目錄
SET_PAGE_DIR(include/asm-i386/pgtable.h);//設(shè)置頁(yè)目錄基地址寄存器
2. 在GDT中添加進(jìn)程對(duì)應(yīng)的TSS項(xiàng)和LDT項(xiàng),其占用的GDT項(xiàng)號(hào)分別記錄在tss成員tr(unsigned long tr)和ldt(unsigned long ldt)中;相關(guān)代碼:
_LDT / _TSS(include/asm-i386/desc.h);//換算LDT / TSS對(duì)應(yīng)的GDT項(xiàng)號(hào)
set_ldt_desc / set_tss_desc (arch/i386/kernel/traps.c);//在GDT中添加LDT / TSS描述符
3. 創(chuàng)建該進(jìn)程的LDT(mm成員void * segments);相關(guān)代碼:
copy_segments(arch/i386/kernel/process.c);//創(chuàng)建進(jìn)程的LDT并初始化LDT
Linux采用"按需調(diào)頁(yè)"的原則來(lái)分配內(nèi)存頁(yè)面,從而避免頁(yè)表過(guò)多占用存儲(chǔ)空間。創(chuàng)建一個(gè)進(jìn)程時(shí)頁(yè)面分配的情況大致是這樣的:進(jìn)程控制塊(1頁(yè));內(nèi)存態(tài)堆棧(1頁(yè));頁(yè)目錄(1頁(yè));頁(yè)表(需要的n頁(yè))。在進(jìn)程以后執(zhí)行的執(zhí)行中,再根據(jù)需要逐漸分配更多的內(nèi)存頁(yè)面。