Linux在X86上的虛擬記憶體管理(轉)

BSDLite發表於2007-08-12
Linux在X86上的虛擬記憶體管理(轉)[@more@]前言
  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初始化為0。
  2. 有關IDT的初始化:這只是臨時初始化IDT,進一步的操作在start_kernel中進行;用於表示IDT的變數(idt_table[ ])在arch/i386/kenel/traps.c中定義,變數型別(desc_struct)定義在include/asm-i386/desc.h。IDT共有IDT_ENTRIES(256)箇中斷描述符,屬性字均為0x8E00,每個中斷描述符都指向同一個中斷服務程式ignore_init。Ignore_int的功能僅僅是輸出訊息int_msg("unknown interrupt")。而IDTR的值為透過命令lidt idt_descr實現。透過在head.s中檢視idt_descr的值可以計算得知,IDT的基地址為idt_table的地址,表長IDT_ENTRIES*8-1(0x7FF)。
  3. 有關GDT的初始化:GDT共有GDT_ENTRIES個段描述符。GDT_ENTRIES的計算公式為:12+2*NR_TASKS。其中12表示前面提到的Linux在GDT中保留的12項,NR_TASKS(512)指系統設定容納的程式數,定義在include/linux/tasks.h。GDT在head.s直接分配儲存單元(標號為gdt_table)。初始化後的GDT如附表1所示。GDTR的值透過命令lgdt gdt_descr實現。透過在head.s中檢視gdt_descr的值可以計算得知,GDT的基地址為gdt_table的地址,表長GDT_ENTRIES*8-1(0x205F)。
  4. 頁目錄的初始化:頁目錄由變數swapper_pg_dir表示,共有1024個頁目錄項。其第0項和第768項均指向pg0(第0頁),初始化值為0x00102007(根據其高20bits的值0x102換算:0x102*4KB=0x102000,第0頁緊跟頁目錄後,實體地址為0x102000),由此可知,Linux 4GB空間中的虛擬地址0x0和0xBFFFFFFF(3GB)均由pg0對映(實體地址0x0~0x3FFFFF(4MB));其他頁目錄項初始值為0x0;
  5. pg0的初始化:第n項對應第n頁,屬性為0x007;即第n項的初始化值的高20bits值為n,底12bits值為0x007;由此可見pg0對映了物理空間的低4MB空間;
  6. 初始化empty_zero_page:該頁的前2KB空間用來儲存setup.s儲存在記憶體引數區的來自BIOS的系統硬體引數;後2KB空間作為命令列緩衝區;
  head.s進行完初始化後呼叫start_kernel(init/main.c)繼續各方面的初始化,主要是呼叫各方面函式初始化核心的資料結構,下面對與X86系統相關的呼叫函式簡述其(與本文相關的)功能。
  1. setup_arch() (arch/i386/kernel/setup.c);設定核心可用實體地址範圍(memory_start~memory_end);設定init_task.mm的範圍;呼叫request_region(kernel/resource.c)申請I/O空間,參見附表4。
  2. paging_init() (arch/i386/mm/init.c);取消虛擬地址0x0對實體地址的低端4MB空間的對映;根據實體地址的實際大小初始化所有的頁表。
  3. trap_init() (arch/i386/kernel/traps.c);在IDT中設定各種入口地址,如異常事件處理程式入口,系統呼叫入口,呼叫門等。其中,trap0~trap17為各種錯誤入口(溢位,0除,頁錯誤等,錯誤處理函式定義在arch/i386/kernel/entry.s);trap18~trap47保留;設定系統呼叫(INT 0x80)的入口為system_call(arch/i386/kernel/entry.s);在GDT中設定0號程式的TSS段描述符和LDT段描述符。
  4. init_IRQ() (arch/i386/kernel/irq.c);初始化IDT 中0x20~0xff項。
  5. time_init() (arch/i386/kernel/time.c);讀取實時時間,重新設定時鐘中斷irq0的中斷服務程式入口。
  6. mem_init() (arch/i386/mm/init.c);初始化empty_zero_page;標記已被佔用的頁。

Linux程式和分段分頁

  每當啟動一個新的程式,Linux都為其建立一個程式控制塊(task_struct,include/linux/sched.h)。task_struct中最重要的與儲存有關的成員為mm(mm_struct* mm,include/linux/sched.h)和tss(thread_struct tss,include/asm-i386/processor.h)。在建立過程中,系統所涉及的(與分段分頁相關)功能包括:
  1. 每個程式(根據需要)建立新頁目錄(mm成員pgd_t * pgd),並將其地址置入暫存器CR3中;相關程式碼:
new_page_tables(mm/memory.c);//建立和初始化新頁目錄
SET_PAGE_DIR(include/asm-i386/pgtable.h);//設定頁目錄基地址暫存器
  2. 在GDT中新增程式對應的TSS項和LDT項,其佔用的GDT項號分別記錄在tss成員tr(unsigned long tr)和ldt(unsigned long ldt)中;相關程式碼:
  _LDT / _TSS(include/asm-i386/desc.h);//換算LDT / TSS對應的GDT項號
  set_ldt_desc / set_tss_desc (arch/i386/kernel/traps.c);//在GDT中新增LDT / TSS描述符
  3. 建立該程式的LDT(mm成員void * segments);相關程式碼:
  copy_segments(arch/i386/kernel/process.c);//建立程式的LDT並初始化LDT
  Linux採用"按需調頁"的原則來分配記憶體頁面,從而避免頁表過多佔用儲存空間。建立一個程式時頁面分配的情況大致是這樣的:程式控制塊(1頁);記憶體態堆疊(1頁);頁目錄(1頁);頁表(需要的n頁)。在程式以後執行的執行中,再根據需要逐漸分配更多的記憶體頁面。


參考資料
  1. "Inter Architecture Software Developer's Manual Volume 3: System Programming", http://developer.intel.com/design/pentiumii/manuals/243192.htm
  2. "Linux作業系統及實驗教程",李善平 鄭扣根編著,機械工業出版社
  3. "Linux 核心原始碼分析",Scott Maxwell著,馮銳 邢飛 劉隆國 陸麗娜譯,機械工業出版社
  4. "Linux 系統分析與高階程式設計技術",周巍松等編著,機械工業出版社

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-948694/,如需轉載,請註明出處,否則將追究法律責任。

相關文章