Linux內存管理(二)

Linux內存管理之二:Linux在X86上的虛擬內存管理linux

本文檔來自網絡,並稍有改動。編程

前言緩存

  Linux支持不少硬件運行平臺,經常使用的有:Intel X86AlphaSparc等。對於不可以通用的一些功能,Linux必須依據硬件平臺的特色來具體實現。本文的目的是簡要探討LinuxX86保護模式上如何實現虛擬內存管理功能。爲簡化和方便敘述,本文作以下限定:X86處理器爲80486和其後的處理器,X86工做在保護模式,不採用物理內存擴展(使用32bits物理地址),不使用擴展頁(頁大小爲4K)。凡是與限定模式無關的內容,本文都儘可能略過。Linux的虛擬內存管理中與硬件平臺無關的內容在本文中也被略過。本文所援引的Linux內核源代碼版本爲Linux 2.2.5網絡

X86的分段和分頁機制數據結構

I. X86的分段機制和相應系統結構app

  X86的分段機制就是將X86的線性地址空間分紅許多小空間--段(segment),利用這些段來存儲(記錄)代碼和數據,經過對段的保護來提供一種對數據或代碼的保護。根據每一個段的做用和存儲內容的不一樣,X86將段分爲三類進程段(代碼段、數據段和堆棧段)和兩類系統段:任務狀態段(TSSTask-State Segment)和LDT段(因爲GDT不是經過段描述符和段選擇符來訪問,因此X86沒有認爲存在一個GDT段;同理,也不存在IDT段)。ide

  在分段機制,X86使用了以下幾種主要數據結構:函數

  · 全局描述符表(GDTGlobal Describtor Table):存放系統用的段描述符和各項任務共用的段描述符,能夠是上述的任何一類段的段描述符,最大表長64KBspa

  · 局部描述符表(LDTLocal Describtor Table):存放某個任務專用的各段的段描述符,只能是三類進程段的段描述符和調用門描述符,最大表長4GB操作系統

  · 段描述符(Segment Describtor):64bits,用來描述一個段的基地址(該地址是線性地址),該段的類型,對該段操做的限制;

  · 門描述符(Gate Describtor):64bits,一種特殊的描述符,爲處於不一樣特權級的系統調用或程序的調用或訪問提供保護;分爲四類:調用門描述符(Call Gate Describtor)、中斷門描述符(Interrupt Gate Describtor)、陷阱門描述符(Trap Gate Describtor)、任務門描述符(Task Gate Describtor);

  · 段選擇符(Segment Selector):16bits,用於在GDTLDT中索引相應的段描述符;

  · 中斷描述表(IDTInterrupt Describer Table):存放門描述符,只能是中斷門描述符,陷阱門描述符和任務門描述符,最大表長64KB

  同時,X86提供了以下幾個用於支持分段機制的寄存器:

  · 全局描述符表寄存器(GDTRGDT Register):48bits32bitsGDT的基地址(線性地址),16bitsGDT的表長;GDTR的初始值爲:基地址0,表長0xFFFF

  · 局部描述符表寄存器(LDTRLDT Register):80bits16bitsLDT段選擇符,64bits爲該LDT段的段描述符; 

  · 中斷描述符表寄存器(IDTRIDT Register):48bits32bitsIDT的基地址(線性地址),16bitsIDT的表長;IDTR的初始值爲:基地址0,表長0xFFFF

  · 任務寄存器(TRTask Register):80bits16bits爲任務狀態段選擇符,64bits爲該任務狀態段的段描述符;

  · 六個段寄存器(Segment Register):分爲可見部分和隱藏部分,可見部分爲段選擇符,隱藏部分爲段描述符;六個段寄存器分別爲CSSSDSESFSGS;關於這些段寄存器的做用參見[1]3.4.2 'Segment Register';

  86工做在保護模式時,進程使用的48bits邏輯地址(Logical address)。邏輯地址的高16bits爲段選擇符,低32bits是段內的偏移量。經過段選擇符在GDTLDT中索引相應的段描述符(獲得該段的基地址),再加上偏移量獲得邏輯地址對應的線性地址(Linear Address)。若是沒有采用分葉管理,線性地址是直接映射物理地址(Physical Address),因而能夠直接用線性地址訪問內存;不然,還要經過X86的分頁轉換,將線性地址轉換爲物理地址。

  以上是對X86分段相關內容的簡要描述,對於各數據結構、寄存器的細節和邏輯地址轉換爲線性地址的細節,請查閱 [1]。 

 

II. X86的分頁機制和相應系統結構

  32bits的線性地址空間能夠直接映射到物理地址空間,也能夠間接映射到許多小塊的物理空間(磁盤存儲空間)上。這種間接映射方式就是分頁機制。X86可用頁大小爲4KB2MB4MB2MB4MB只能在PentiumPentium Pro處理器中使用,本文中限定採用4KB頁)。

  在分頁機制,X86使用了四種數據結構:

  · 頁目錄項(PDEPage Directory Entry):32bits結構,高20bits爲頁表基地址(物理地址),以4KB爲遞增單位,低12bits爲頁表屬性,具體換算參見後面初始化部分;

  · 頁目錄(Page directory):存儲頁目錄項,位於一頁中,總共可容納1024個頁目錄項;

  · 頁表項(PTEPage Table Entry):32bits結構,高20bits爲頁基地址(物理地址),低12bits爲頁屬性;

  · 頁表(Page table):存儲頁表項,位於一頁中,總共可容納1024個頁表項;

  · 頁(Page):4KB的連續地址空間;

  爲了實現分頁機制和提升地址轉換的效率,X86提供和使用了以下的硬件結構:

  · 頁標誌位(PGPage):該標誌位爲1,說明採用頁機制;實際就是控制寄存器CR0的第31bit

  · 頁緩存/快表(TLBsTranslation Lookaside Buffers):存儲最近使用的PDEPTE,以提升地址轉換的效率;

  · 頁目錄基地址寄存器(PDBRPage Directory Base Register):用於存儲頁目錄的基地址(物理地址),實際就是控制寄存器CR3

  爲了實現將線性地址映射到物理地址,X8632bits線性地址解釋爲三部分:第31bit到第22bit爲頁目錄中的偏移,用於索引頁目錄項(獲得對應頁表的基地址);第21bit到第12bit爲頁表中的偏移,用於索引頁表項(獲得對應頁的基地址);第11bit到第0bit爲頁中的偏移。這樣,經過兩級索引和頁中的偏移量,最後能正確獲得線性地址對應的物理地址。

  關於分頁機制的詳細描述和做用,請查閱參考文檔[1]

 

LINUX的分段策略

 

  LinuxX86上採用最低限度的分段機制,其目的是爲了避開復雜的分段機制,提升Linux在其餘不支持分段機制的硬件平臺的可移植性,同時又充分利用X86的分段機制來隔離用戶代碼和內核代碼。所以,在Linux上,邏輯地址和線性地址具備相同的值。

  因爲X86GDT最大表長爲64KB,每一個段描述符爲8B,因此GDT最多可以容納8192個段描述符。每產生一個進程,Linux爲該進程在GDT中建立兩個描述符:LDT段描述符和TSS描述符,除去LinuxGDT中保留的前12項,GDT實際最多能容納4090個進程。Linux的內核自身有獨立的代碼段和數據段,其對應的段描述符分別存儲在GDT中的第2項和第3項。每一個進程也有獨立的代碼段和數據段,對應的段描述符存儲在它本身的LDT中。有關LinuxGDT表項和DLT表項分佈狀況參見附表1,附表2所示。

  在Linux中,每一個用戶進程均可以訪問4GB的線性地址空間。其中0x0~0xBFFFFFFF3GB空間爲用戶態空間,用戶態進程能夠直接訪問。從0xC0000000~0x3FFFFFFF1GB空間爲內核態空間,存放內核訪問的代碼和數據,用戶態進程不能直接訪問。當用戶進程經過中斷或系統調用訪問內核態空間時,會觸發X86的特權級轉換(從特權級3切換到特權級0),即從用戶態切換到內核態。

 

LINUX的分頁策略

 

  標準Linux的分頁是三級頁表結構,除了X86支持的頁目錄和頁,還有一級被稱爲中間頁目錄。所以,線性地址在轉換爲物理地址的過程當中,線性地址就被解釋爲四個部分(不是X86所認識的三個部分),增長了頁中間目錄中的索引。當運行在X86平臺上時,Linux經過將中間頁目錄最大的頁目錄項個數定義爲1,並提供一組相關的宏(這些宏將中間頁目錄用頁目錄來替換)將三級頁面結構分解過程完美的轉換爲X86使用的二級頁面分解。這樣,無需改動內核中頁面解釋的主要代碼(這些代碼都是認爲線性地址由四個部分組成)。關於這些宏定義參見Linux源碼"/include/asm/pgtable.h""/include/asm/page.h"

  內核態虛擬空間從3GB3GB+4MB的一段(對應進程頁目錄第768項指引的頁表),被映射到物理地址0x0~0x3FFFFF4MB)。所以,進程處於內核態時,只要經過訪問3GB3GB+4MB就可訪問物理內存的低4MB空間。全部進程從3GB4GB的線性空間都是同樣的,由一樣的頁目錄項,一樣的頁表,映射到相同的物理內存段。Linux以這種方式讓內核態進程共享代碼和數據。

 

Linux分段分頁初始化

  不管Linux系統如何被引導,通過zImage(參見arch/i386/boot/bootsect.s)或通過LILO,最後都會跳轉執行arch/i386/boot/setup.s(被裝載到SETUPSEG,物理地址 0x90200),setup.sBIOS中獲取計算機系統的硬件參數(如硬盤參數),放到內存參數區(臨時寄放),同時作一些初步的狀態檢查,爲進入保護模式作準備。關於引導過程和setup.s的具體執行參見[2]

  保護模式下的內核初始化模塊從物理地址0x100000開始執行,該地址開始的代碼和數據結構都對應在arch/i386/kernel/head.s中,參見附表3。初始化模塊主要功能是對相關寄存器IDTGDT,頁目錄及頁表等進行初始化。下面,忽略head.s執行流程的細節,概要闡述head.s主要的初始化功能。

  1. 部分寄存器的初始化:將段寄存器DSESGSFS__KERNEL_DS0x18include/asm-i386/segment.h)來初始化(經過前面對段寄存器的描述和段選擇符的介紹可知道,其做用是將定位到GDT中的第三項(內核數據段),並設置對該段的操做特限級爲0);置位CR0PG位,並根據CPU的型號選擇置位AM, WP, NE 和 MP;用0x101000初始化CR3(頁目錄swapper_pg_dir的地址);置ESP32bits__KERNEL_DS0x18),低32bitsinit_user_stack+8192LDTR初始化爲0

  2. 有關IDT的初始化:這只是臨時初始化IDT,進一步的操做在start_kernel中進行;用於表示IDT的變量(idt_table[ ])在arch/i386/kenel/traps.c中定義,變量類型(desc_struct)定義在include/asm-i386/desc.hIDT共有IDT_ENTRIES256)箇中斷描述符,屬性字均爲0x8E00,每一箇中斷描述符都指向同一個中斷服務程序ignore_initIgnore_int的功能僅僅是輸出消息int_msg"unknown interrupt")。而IDTR的值爲經過命令lidt idt_descr實現。經過在head.s中查看idt_descr的值能夠計算得知,IDT的基地址爲idt_table的地址,表長IDT_ENTRIES*8-10x7FF)。

  3. 有關GDT的初始化:GDT共有GDT_ENTRIES個段描述符。GDT_ENTRIES的計算公式爲:12+2*NR_TASKS。其中12表示前面提到的LinuxGDT中保留的12項,NR_TASKS512)指系統設定容納的進程數,定義在include/linux/tasks.hGDThead.s直接分配存儲單元(標號爲gdt_table)。初始化後的GDT如附表1所示。GDTR的值經過命令lgdt gdt_descr實現。經過在head.s中查看gdt_descr的值能夠計算得知,GDT的基地址爲gdt_table的地址,表長GDT_ENTRIES*8-10x205F)。

  4. 頁目錄的初始化:頁目錄由變量swapper_pg_dir表示,共有1024個頁目錄項。其第0項和第768項均指向pg0(第0頁),初始化值爲0x00102007(根據其高20bits的值0x102換算:0x102*4KB=0x102000,第0頁緊跟頁目錄後,物理地址爲0x102000),由此可知,Linux 4GB空間中的虛擬地址0x00xBFFFFFFF3GB)均由pg0映射(物理地址0x0~0x3FFFFF4MB));其餘頁目錄項初始值爲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_kernelinit/main.c)繼續各方面的初始化,主要是調用各方面函數初始化內核的數據結構,下面對與X86系統相關的調用函數簡述其(與本文相關的)功能。

  1. setup_arch() (arch/i386/kernel/setup.c);設置內核可用物理地址範圍(memory_start~memory_end);設置init_task.mm的範圍;調用request_regionkernel/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_callarch/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_structinclude/linux/sched.h)。task_struct中最重要的與存儲有關的成員爲mmmm_struct* mminclude/linux/sched.h)和tssthread_struct tssinclude/asm-i386/processor.h)。在建立過程當中,系統所涉及的(與分段分頁相關)功能包括:

  1. 每一個進程(根據須要)創建新頁目錄(mm成員pgd_t * pgd),並將其地址置入寄存器CR3中;相關代碼:

new_page_tablesmm/memory.c);//建立和初始化新頁目錄

SET_PAGE_DIRinclude/asm-i386/pgtable.h);//設置頁目錄基地址寄存器

  2. GDT中添加進程對應的TSS項和LDT項,其佔用的GDT項號分別記錄在tss成員trunsigned long tr)和ldtunsigned long ldt)中;相關代碼:

  _LDT / _TSSinclude/asm-i386/desc.h);//換算LDT / TSS對應的GDT項號

  set_ldt_desc / set_tss_desc arch/i386/kernel/traps.c);//GDT中添加LDT / TSS描述符

  3. 建立該進程的LDTmm成員void * segments);相關代碼:

  copy_segmentsarch/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 系統分析與高級編程技術",周巍鬆等編著,機械工業出版社

相關文章
相關標籤/搜索