Linux 內存尋址
內存地址分類
邏輯地址:機器語言指令中用來指定一個操作數或一條指令的地址。每一個邏輯地址都由一個段(segment)和偏移量(offset或displacement)組成,偏移量指明了從段開始的地方到實際地址之間的距離。
線性地址(或 虛擬地址):一個32位(或64位)無符號整數,在32位系統中可以用來表示高達4GB(0x0000 0000 —— 0xffff ffff)的地址,也就是高達 4 * 1024 * 1024 * 1024個內存單元(字節)。
物理地址(physical address):芯片級內存單元尋址。與從微處理器的地址引腳發送到內存總線上的電信號相對應。物理地址由32位或36位(開啟PAE)無符號整數表示。
內存管理單元(MMU)通過分段單元(segmentation unit)把邏輯地址轉換成線性地址;然後,通過分頁單元(paging unit)把線性地址轉換成物理地址。分段單元和分頁單元都是一種硬件電路。
硬件中的分段
段選擇符和段寄存器
邏輯地址由兩部分組成:段選擇符和指定段內相對地址的偏移量。段選擇符(Segment Selector)是一個16位長的字段,而偏移量是一個32位長的字段。
字段名 | 描述 |
---|---|
索引 | 指定了放在GDT或LDT中的相應段描述符 |
TI | TI(Table Indicator)標誌,指明段描述符是在GDT中(TI=0)或在LDT中(TI=1) |
RPL | 請求者特權級,當相應的段選擇符裝入到cs寄存器中時指示出CPU當前的特權級,它還可以用於在訪問數據段時有選擇地削弱處理器的特權級 |
處理器提供段寄存器來存放段選擇符以保證查找段選擇符的效率。這些段寄存器稱為cs, ss, ds, es, fs和gs。程序可以把同一個段寄存器用於不同的目的:先將其值保存在內存中,用完後再恢復。6個段寄存器中3個有專門的用途:
-
cs 代碼段寄存器,指向包含程序指令的段。
-
ss 棧段寄存器,指向包含當前程序棧的段。
-
ds 數據段寄存器,指向包含靜態數據或者全局數據段(初始化數據)。
其他3個段寄存器作一般用途,可以指向任意的數據段。cs寄存器還有一個很重要的功能:它含有一個 兩位的字段,用以指明CPU的 當前特權級(Current Privilege Level, CPL)。0代表最高優先級——內核態,而3代表最低優先級——用戶態。
段描述符
每個段由一個 8字節(64 bit) 的段描述符(Segment Descriptor)表示,它描述了段的特征。段描述符放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中。GDT在主存中的地址和大小存放在gdtr控制寄存器中,當前正被使用的LDT地址和大小放在ldtr控制寄存器中。
字段名 | 描述 |
---|---|
基地址(Base) | 包含段的首字節的線性地址 (32 bit) |
G | 粒度標誌;置0,則段大小以字節為單位,否則以4096字節的倍數計 |
Limit | 最大段偏移量,段的長度(20 bit)。如果G被置為0,則一個段的大小在1個字節到1MB之間變化;否則,將在4KB到4GB之間變化 |
S | 系統標誌;置0,系統段,存儲諸如LDT這種關鍵的數據結構,否則它是一個普通的代碼段或數據段 |
Type | 描述了段的類型特征和它的存取權限 |
DPL | 描述符特權級(Descriptor Privilege Level)字段;用於限制對這個段的存取。表示訪問這個段要求的CPU最小的優先級 |
P | Segment-Present標誌;為0表示段當前不在主存中。Linux總是把這個標誌(第47位)設為1,因為它從來不把整個段交換到磁盤上去 |
D或B | 取決於是代碼段還是數據段 |
AVL | 操作系統使用,但被Linux忽略 |
為加速邏輯地址到線性地址的轉換,80x86處理器提供一種附加的非編程的寄存器(不能被編程者設置的寄存器),供6個可編程的段寄存器使用。每一個非編程的寄存器含有8個字節的段描述符,由相應的段寄存器中的段選擇符來指定。每當一個段選擇符被裝入段寄存器時,相應的段描述符就由內存裝入到對應的非編程CPU寄存器。之後,針對那個段的邏輯地址轉換就可以不訪問主存中的GDT或LDT,處理器只需直接引用存放段描述符的CPU寄存器即可。僅當段寄存器的內容改變時,才有必要訪問GDT或LDT。
分段單元
下圖顯示一個邏輯地址轉換的詳細過程,分段單元(segmentation unit)執行以下操作:
-
先檢查段選擇符的TI字段,以決定段描述符保存在哪一個描述符表中。GDT中,分段單元從gdtr寄存器得到GDT的線性基地址;LDT中,分段單元從ldtr寄存器得到LDT的線性基地址。
-
從段選擇符的index字段計算段描述符的地址,index字段的值乘以8(一個段描述符的大小),這個結果與gdtr或ldtr寄存器中的內容相加。
-
把邏輯地址的偏移量與段描述符Base字段的值相加就得到了線性地址。
有了與段寄存器相關的不可編程寄存器,只有當段寄存器的內容被改變時才需要執行前兩個操作。
Linux中的分段
2.6版的Linux只有在x86結構下才需要分段。
運行在用戶態的所有Linux進程都使用一對相同的段來對指令和數據尋址。這兩個段就是所謂的用戶代碼段和用戶數據段。類似地,運行在內核態的所有Linux進程都使用一對相同的段對指令和數據尋址:內核代碼段和內核數據段。
下表顯示了這4個重要段的段描述符字段的值:
段 | Base | G | Limit | S | Type | DPL | D/B | p |
---|---|---|---|---|---|---|---|---|
用戶代碼段 | 0x0000 0000 | 1 | 0xfffff | 1 | 10 | 3 | 1 | 1 |
用戶數據段 | 0x0000 0000 | 1 | 0xfffff | 1 | 2 | 3 | 1 | 1 |
內核代碼段 | 0x0000 0000 | 1 | 0xfffff | 1 | 10 | 0 | 1 | 1 |
內核數據段 | 0x0000 0000 | 1 | 0xfffff | 1 | 2 | 0 | 1 | 1 |
G為1,粒度為4KB,Limit為 0xfffff,則空間為 4GB
相應的段選擇符由宏定義。
__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS
為了對內核代碼段尋址,內核只需把__KERNEL_CS宏產生的值裝進cs段寄存器即可。
註意,與段相關的線性地址從0開始,達到2^23 - 1的尋址限長。這就意味著在用戶態或內核態下的所有進程可以使用相同的邏輯地址。
所有段都從0x0000 0000 開始,那麽,在Linux下邏輯地址與線性地址是一致的,即邏輯地址的偏移量字段的值與相應的線性地址的值總是一致的。
當對指向指令或者數據結構的指針進行保存時,內核不需要為其設置邏輯地址的段選擇符,因為cs寄存器就含有當前的段選擇符。例如,當內核調用一個函數時,它執行一條call匯編語言指令,該指令僅指定其邏輯地址的偏移量部分,而段選擇符不用設置,它已經隱含在cs寄存器中了。因為“在內核態執行”的段只有一種,叫做代碼段,由宏__KERNEL_CS定義,所以只要當CPU切換到內核態時將__KERNEL_CS裝載進cs就足夠了。同樣的道理也適用於指向內核數據結構的指針(隱含地使用ds寄存器)以及指向用戶數據結構的指針(內核顯式地使用es寄存器)。
Linux GDT
在單處理器系統中只有一個GDT,而在多處理器系統中每個CPU對應一個GDT。所有的GDT都存放在cpu_gdt_table數組中,而所有GDT的地址和它們的大小(當初始化gdtr寄存器時使用)被存放在cpu_gdt_descr數組中。這些符號都在文件arch/i386/kernel/head.S中被定義。
下圖是GDT的布局示意圖。每個GDT包含18個段描述符和14個空的,未使用的,或保留的項。插入未使用的項的目的是為了使經常一起訪問的描述符能夠處於同一個32字節的硬件高速緩存行中。
每一個GDT中包含的18個段描述符指同下列的段:
-
用戶態和內核態下的代碼段和數據段,共4個。
-
任務狀態段(TSS),每個處理器有1個。每個TSS相應的線性地址空間都是內核數據段相應線性地址空間的一個小子集。所有的任務狀態段都順序地存放在init_tss數組中,值得特別說明的是,第n個CPU的TSS描述符的Base字段指向init_tss數組的第n個元素。G(粒度)標誌被清0,而Limit字段置為0xeb, 因為TSS段是236字節長。Type字段置為9或11(可用的32位TSS),且DPL置 為0,因為不允許用戶態下的進程訪問TSS段。
-
1個包括缺省局部描述符表的段,這個段通常被所有進程共享。
-
3個局部線程存儲(Thread-Local Storage, TLS)段:這種機制允許多線程應用程序使用最多3個局部於線程的數據段。系統使用set_thread_area()和get_thread_area()分別為正在執行的進程創建和撤銷一個TLS段。
-
與高級電源管理(APM)相關的3個段:由於BIOS代碼使用段,所以當Linux APM驅動程序調用BIOS函數來獲取或者設置APM設備的狀態時,就可以使用自定義的代碼段和數據段。
-
與支持即插即用(PnP)功能的BIOS服務程序相關的5個段。
-
被內核用來處理“雙重錯誤”異常(處理一個異常時可能會引發另一個異常)的特殊TSS段。
系統中每個處理器都有一個GDT副本。除少數幾種情況外,所有GDT的副本都存放相同的表項:
-
每個處理器都有它自己的TSS段。
-
GDT中只有少數項可能依賴於CPU正在執行的進程(LDT和TLS段描述符)。
-
在某些情況下,處理器可能臨時修改GDT副本裏的某個項,例如,當調用APM的BIOS例程時就會發生這種情況。
Linux LDT
大多數用戶態下的Linux程序不使用局部描述符表,因此內核就定義了一個缺省的LDT供大多數進程共享。缺省的局部描述符表存放在default_ldt數組中。它包含5個項,但內核僅僅有效地使用了其中的兩個項:用於iBCS執行文件的調用門和Solaris/x86可執行文件的調用門。調用門是80x86微處理器提供的一種機制,用於在調用預定義函數時改變CPU的特權級(參考Intel文檔以獲取更多詳情)。
硬件中的分頁
分頁單元(paging unit)把線性地址轉換成物理地址。其中的一個關鍵任務是把所請求的訪問類型與線性地址的訪問權限相比較,如果這次內存訪問是無效的,就產生一個缺頁異常。
為了效率起見,線性地址被分成以固定長度為單位的組,稱為頁(page)。頁內部連續的線性地址被映射到連續的物理地址中。這樣,內核可以指定一個頁的物理地址和其存取權限,而不用指定頁所包含的全部線性地址的存取權限。我們遵循通常習慣,使用術語“頁”既指一組線性地址,又指包含在這組地址中的數據。
分頁單元把所有的RAM分成固定長度的葉框(page frame)(也叫做物理頁)。每一個葉框包含一個頁,也就是說葉框的長度與一個頁的長度一致。頁框是主存的一部分,因此也是一個存儲區域。區分一頁和一個頁框是很重要的,前者只是一個數據塊,可以存放在任何頁框或磁盤中。
把線性地址映射到物理地址的數據結構稱為頁表(page table )。頁表存放在主存中,並在啟用分頁單元之前必須由內核對頁表進行適當的初始化。
從80386開始,所有的80x86處理器都支持分頁,它通過設置cr0寄存器的PG標誌啟用。當PG=0時,線性地址就被解釋成物理地址。<需要了解控制寄存器(cr0~cr3)的結構及作用>
常規分頁
從80386起,Intel處理器的分頁單元處理4KB的頁。32位的線性地址被分成3個域:
-
Directory(目錄):最高10位
-
Table(頁表):中間10位
-
Offset(偏移量):最低12位
線性地址的轉換分兩步完成,每一步都基於一種轉換表,第一種轉換表稱為頁目錄表(page directory),第二種轉換表稱為頁表(page table )。
頁目錄 及 頁表都分別存放在1個頁中(4KB),其中每個表項也都是4個字節。
使用這種二級模式的目的在於減少每個進程頁表所需RAM的數量。如果使用簡單的一級頁表,那將需要高達2^20個表項(4GB/4KB = 2^20 ,也就是,在每項4個字節時,需要4MB RAM)來表示每個進程的頁表(如果進程使用全部4GB線性地址空間),即使一個進程並不使用那個範圍內的所有地址。二級模式通過只為進程實際使用的那些“虛擬內存區”請求頁表來減少內存容量。
每個活動進程必須有一個分配給它的頁目錄。不過,沒有必要馬上為進程的所有頁表都分配RAM。只有在進程實際需要一個頁表時才給該頁表分配RAM會更為有效率。
正在使用的頁目錄的物理地址存放在控制寄存器cr3中。
頁目錄項和頁表項有相同的結構,每項都包含下面的字段:
字段 | 描述 |
---|---|
Present標誌 | 置為1,所指的頁(或頁表)就在主存中;為0,則這一頁不在主存,此時這個表項剩余的位可由操作系統用於自己的目的。如果只需一個地址轉換所需的頁表項或頁目錄項中Present標誌被清0,那麽分頁單元就把該線性地址存放在控制寄存器cr2中,並產生14號異常:缺頁異常。 |
包含頁框物理地址最高20位的字段 | 由於每一個頁框有4KB的容量,它的物理地址必須是4096的倍數,因此物理地址的最低12位總是為0。若這個字段指向一個頁目錄,相應的頁框就含有一個頁表,若指向一個頁表,相應的頁框就含有一頁數據。 |
Accessed標誌 | 每當分頁單元對相應頁框進行尋址時就設置這個標誌。當選中的頁被交換出去時,這一標誌由操作系統使用。分頁單元從來不重置這個標誌,而是必須由操作系統去做。 |
Dirty標誌 | 只應用於頁表項中。每當對一個頁框進行寫操作時就設置這個標誌。與Accessed標誌一樣,“當選中…………系統去做”。 |
Read/Write標誌 | 含有頁或頁表的存取權限。 |
User/Supervisor標誌 | 含有訪問頁或頁表所需的特權級。 |
PCD和PWT標誌 | 控制硬件高速緩存處理頁或頁表的方式。 |
Page Size標誌 | 只應用於頁目錄項。置為1,則頁目錄指的是2MB或4MB的頁框。 |
Global標誌 | 只應用於頁表項。這個標誌是在Pentium Pro中引入的,用來防止常用頁從TLB(俗稱“快表”)高速緩存中刷新出去。只有在cr4寄存器的頁全局啟用(Page Global Enable, PGE)標誌置位時這個標誌才起作用。 |
擴展分頁
從Pentium模型開始,80x86微處理器引入了擴展分頁(extended paging),它允許頁框大小為4MB而不是4KB。擴展分頁用於把大段連續的線性地址轉換成相應的物理地址,在這些情況下,內核可以不用中間頁表進行地址轉換,從而節省內存並保留TLB項。
通過設置頁目錄項的Page Size標誌啟用擴展分頁功能。分頁單元吧32位線性地址分成兩個字段:
-
Directory:最高10位
-
Offset:其余22位
擴展分頁和正常分頁的目錄項基本相同,除了:
-
Page Size標誌必須被設置。
-
32位物理地址字段只有最高10位是有意義的。這是因為每一個物理地址都是在以4MB為邊界的地方開始的,故這個地址的最低22位為0。
通過設置cr4處理器寄存器的PSE標誌能使擴展分頁與常規分頁共存。
硬件保護方案
分頁單元和分段單元的保護方案不同。盡管x86處理器允許一個段使用4種可能的特權級別,但與頁和頁表相關的特權級只有兩個,因為特權由User/Supervisor標誌所控制。若這個標誌為0,只有當CPL小於3(這意味著對於Linux而言,處理器處於內核態)時才能對頁尋址。若該標誌為1,則總能對頁尋址。
此外,與段的3種存取權限(讀、寫、執行)不同的是,頁的存取權限只有兩種(度、寫)。如果頁目錄項或頁表項的Read/Write標誌等於0,說明相應的頁表或頁是只讀的,否則是可讀寫的。
物理地址擴展(PAE)分頁機制
處理器所支持的RAM容量受連接到地址總線上的地址管腳數限制。早期Intel處理器從80386到Pentium使用32位物理地址。從理論上講,這樣的系統上可以安裝高達4GB的RAM;而實際上,由於用戶進程線性地址空間的需要,內核不能直接對1GB以上的RAM進行尋址。
然而,大型服務器需要大於4GB的RAM來同時運行數以千計的進程,所以必須擴展32位x86結構所支持的RAM容量。Intel通過在它的處理器上把管腳數從32增加到36已經滿足了這些需求。尋址能力可達到2^36 = 64GB。不過,只有引入一種新的分頁機制把32位線性地址轉換為36位物理地址才能使用所增加的物理地址。
從Pentium Pro處理器開始,Intel引入一種叫做 物理地址擴展(Physical Address Extension, PAE)的機制。另外一種叫做頁大小擴展[Page Size Extension (PSE-36)]的機制在Pentium 3處理器中引入,但是Linux並沒有采用這種機制。
通過設置cr4控制寄存器中的物理地址擴展(PAE)標誌激活PAE。頁目錄項中的頁大小標誌PS啟用大尺寸頁(在PAE啟用時為2MB)。
Intel為了支持PAE改變了分頁機制:
-
64GB的RAM被分為2^24個頁框(4KB),頁表項的物理地址字段從20位擴展到了24位。因為PAE頁表項必須包含12個標誌位(在前面已描述)和24個物理地址位,總數之和為36,頁表項大小從32位變為64位增加了一倍。結果,一個4KB的頁表包含512個表項而不是1024個表項。
-
引入一個叫做頁目錄指針表(Page Directory Pointer Table, PDPT)的頁表新級別,它由4個64位表項組成。
-
cr3控制寄存器包含一個27位的頁目錄指針表(PDPT)基地址字段。因為PDPT存放在RAM的前4GB中,並在32字節(25)的倍數上對齊,因此27位足以表示這種表的基地址。
-
當把線性地址映射到4KB的頁時(頁目錄項中的PS標誌清0), 32位線性地址按下列方式解釋:
-
cr3:指向一個PDPT
-
位31-30:指向PDPT中4個項中的一個
-
位29-21:指向頁目錄中512個項目中的一個
-
位20-12:指向頁表中512項中的一個
-
位11-0:4KB頁中的偏移量
-
-
當把線性地址映射到2MB的頁時(頁目錄項中的PS標誌置為1), 32位線性地址按下列方式解釋:
-
cr3:指向一個PDPT
-
位31-30:指向PDPT中4個項中的一個
-
位29-21:指向頁目錄中512個項中的一個
-
位20-0:2MB頁中的偏移量
-
總之,一旦cr3被設置,就可能尋址高達4GB RAM。如果我們希望對更多的RAM尋址,就必須在cr3中放置一個新值,或改變PDPT的內容。然而,使用PAE的主要問題是線性地址仍然是32位長。這就迫使內核編程人員用同一線性地址映射不同的RAM區。很明顯,PAE並沒有擴大進程的線性地址空間,因為它只處理物理地址。此外,只有內核能夠修改進程的頁表,所以在用戶態下運行的進程不能使用大於4GB的物理地址空間。另一方面,PAE允許內核使用容量高達64GB的RAM,從而顯著增加了系統中的進程數量。
64位系統中的分頁
平臺名稱 | 頁大小 | 尋址使用的位數 | 分頁級別數 | 線性地址分級 |
---|---|---|---|---|
alpha | 8KB | 43 | 3 | 10+10+10+13 |
ia64 | 4KB | 39 | 3 | 9+9+9+12 |
ppc64 | 4KB | 41 | 3 | 10+10+9+12 |
sh64 | 4KB | 41 | 3 | 10+10+9+12 |
x86_64 | 4KB | 48 | 4 | 9+9+9+9+12 |
轉換後援緩沖器(TLB)
x86處理器包含了一個稱為轉換後援緩沖器或TLB(Translation Lookaside Buffer)的高速緩存用於加快線性地址的轉換。當一個線性地址被第一次使用時,通過慢速訪問RAM中的頁表計算出相應的物理地址。同時,物理地址被存放在一個TLB表項(TLB entry)中,以便以後對同一個線性地址的引用可以快速地得到轉換。
在多處理系統中,每個CPU都有自己的TLB,叫做該CPU的本地TLB。
當CPU的cr3控制寄存器被修改時,硬件自動使本地TLB中的所有項都無效,這是因為新的一組頁表被啟用而TLB指向的是舊數據。
Linux中的分頁
Linux采用了一種同時適用於32位和64位系統的普通分頁模型。從2.6.11版本開始,采用了四級分頁模型。下圖中展示的4種頁表分別被為:
-
頁全局目錄(Page Global Directory )
-
頁上級目錄(Page Upper Directory )
-
頁中級目錄(Page Middle Directory )
-
頁表(Page Table)
對於沒有啟用物理地址擴展的32位系統,兩級頁表已經足夠了。Linux通過使“頁上級目錄”位和“頁中間目錄”位全為0,從根本上取消了頁上級目錄和頁中間目錄字段。不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在32位系統和64位系統下都能使用。內核為頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數設置為1,並把這兩個目錄項映射到頁全局目錄的一個適當的目錄項而實現的。
啟用了物理地址擴展(PAE)的32位系統使用了三級頁表。Linux的頁全局目錄對應x86的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應x86的頁目錄,Linux的頁表對應x86的頁表。
最後,64位系統使用二級還是四級分頁取決於硬件對線性地址的位的劃分。
Linux的進程處理很大程度上依賴於分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目標變得可行:
-
給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。
-
區別頁(即一組數據)和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然後保存到磁盤上,以後重新裝入這同一頁時又可以被裝在不同的頁框中。這就是虛擬內存機制的基本要素。
每個進程有它自己的頁全局目錄和自己的頁表集。當發生進程切換時,Linux把cr3控制寄存器的內存保存在前一個執行進程的描述符中,然後把下一個要執行進程的描述符的值裝入cr3寄存器中。因此,當新進程重新開始在CPU上執行時,分頁單元指向一組正確的頁表。
物理內存布局
可參考 地址空間布局
在初始化階段,內核必須建立一個物理地址映射來指定哪些物理地址範圍對內核可用而哪些不可用。
內核將下列頁框記為保留:
-
在不可用的物理地址範圍內的頁框。
-
含有內核代碼和已初始化的數據結構的頁框。
保留頁框中的頁絕不能被動態分配或交換到磁盤上。
一般來說,Linux內核安裝在RAM中從物理地址0x00100000開始的地方,也就是說,從第二個MB開始。所需頁框總數依賴幹內核的配置方案:典型的配置所得到的內核可以被安裝在小於3MB的RAM中。
為什麽內核沒有安裝在RAM第一個MB開始的地方?因為PC體系結構有幾個獨特的地方必須考慮到。例如:
-
頁框0由BIOS使用,存放加電自檢(Power-On Self-Test, POST)期間檢查到的系統硬件配置。
-
物理地址從0x000a0000到0x000fffff的範圍通常留給BIOS例程,並且映射ISA圖形卡上的內部內存。這個區域就是所有IBM兼容PC上從640KB到1MB之間著名的洞:物理地址存在但被保留,對應的頁框不能由操作系統使用。
-
第一個MB內的其他頁框可能由特定計算機模型保留。例如,IBM Thinkpnd把0xa0頁框映射到0x9f頁框。
在啟動過程的早期階段,內核詢問BIOS並了解物理內存的大小。在新近的計算機中,內核也調用BIOS過程建立一組物理地址範圍和其對應的內存類型。
隨後,內核執行machine_specific_memory_setup()函數,該函數建立物理地址映射。當然,如果這張表是可獲取的,那是內核在BIOS列表的基礎上構建的。否則,內核按保守的缺省設置構建這張表:從0x9f000(LOWMEMSIZE())到0x100000(HIGH_MEMORY)號的所有頁框都標記為保留。
開始 | 結束 | 類型 |
---|---|---|
0x0000 0000 | 0x0009 ffff | Usable |
0x000f 0000 | 0x000f ffff | Reserved |
0x0010 0000 | 0x07fe ffff | Usable |
0x07ff 0000 | 0x07ff 2ffff | ACPI data |
0x07ff 3000 | 0x07ff ffff | ACPI NVS |
0xffff 0000 | 0xffff ffff | Reserved |
上表顯示了具有128MB(0x0800 0000) RAM計算機的典型配置。從0x07ff 0000到0x07ff 2fff 的物理地址範圍中存有加電自檢(POST)階段由BIOS寫入的系統硬件設備信息。在初始化階段,內核把這些信息拷貝到一個合適的內核數據結構中,然後認為這些頁框是可用的。相反,從0x07ff3000到0x07ff ffff的物理地址範圍被映射到硬件設備的ROM芯片。從0xffff 0000開始的物理地址範圍標記為保留,因為它由硬件映射到BIOS的ROM芯片。註意BIOS也許並不提供一些物理地址範圍的信息(在上述表中,範圍是0x000a 0000到0x000e ffff)。為安全可靠起見,Linux假定這樣的範圍是不可用的。
內核可能不會見到BIOS報告的所有物理內存:例如,如果未使用PAE支持來編譯,即使有更大的物理內存可供使用,內核也只能尋址4GB大小的RAM。setup_memory()函數在machine_specific_memory_setup()執行後被調用:它分析物理內存區域表並初始化一些變量來描述內核的物理內存布局。
為了避免把內核裝入一組不連續的頁框裏,Linux更願跳過RAM的第一個MB。明確地說,Linux用PC體系結構未保留的頁框來動態存放所分配的頁。下圖顯示了Linux怎樣填充前3MB的RAM:
符號_text對應於物理地址0x0010 0000 (16MB),表示內核代碼第一個字節的地址。內核代碼的結束位代由另外一個類似的符號_etext表示。內核數據分為兩組:初始化過的數據的和沒有初始化的數據。初始化過的數據在_etext後開始,在_edata處結束。緊接著是未初始化的數據並以_end結束。
圖中出現的符號並沒有在Linux源代碼中定義,它們是編譯內核時產生的(可以在System.map文件中找到這些符號,System.map是編譯內核以後所創建的)。
進程頁表
進程的線性地址空間分成兩部分:
-
從0x0000 0000——0xbfff ffff的線性地址,無論進程運行在用戶態還是內核態都可以尋址(0—3GB)。
-
從0xc000 0000——0xffff ffff的線性地址,只有內核的進程才能尋址。
進程運行在用戶態時,所產生的線性地址小於0xc000 0000,而運行在內核態時,執行內核代碼,所產生的地址大於等於0xc000 0000。但是,在某些情況下,內核為了檢索或存放數據必須訪問用戶態線性地址空間。
宏PAGE_OFFSET產生的值是0xc000 0000,這就是進程在線性地址空間中的偏移量,也是內核生存空間的開始之處。
內核頁表
內核維持著一組自己使用的頁表,駐留在所謂的主內核頁全局目錄(master kernel Page Global Directory)中。系統初始化後,這組頁表還從未被任何進程或任何內核線程直接使用;更確切地說,主內核頁全局目錄的最高目錄項部分作為參考模型,為系統中每個普通進程對應的頁全局目錄項提供參考模型。
內核初始化自己的頁表,這個過程分為兩個階段。事實上,內核映像剛剛被裝入內存後,CPU仍然運行於實模式,所以分頁功能沒有被啟用。
第一個階段,內核創建一個有限的地址空間,包括內核的代碼段和數據段、初始頁表和用於存放動態數據結構的共128KB大小的空間。這個最小限度的地址空間僅夠將內核裝入RAM和對其初始化的核心數據結構。
第二個階段,內核充分利用剩余的RAM並適當地建立分頁表。下一節解釋這個方案是怎樣實施的。
臨時內核頁表
臨時頁全局目錄是在內核編譯過程中靜態地初始化的,而臨時頁表是由startup_32()匯編語言函數(定義於arch/i386/kernel/head.S)初始化的。不再過多提及頁上級目錄和頁中間目錄,因為它們相當於頁全局目錄項。在這個階段PAE支持並未激活。
臨時頁全局目錄放在swapper_pg_dir變量中。臨時頁表在pg0變量處開始存放,緊接在內核未初始化的數據段(_end符號)後面。為簡單起見,我們假設內核使用的段、臨時頁表和128KB的內存範圍能容納於RAM前8MB空間裏。為了映射RAM前8MB的空間,需要用到兩個頁表。
分頁第一個階段的目標是允許在實模式下和保護模式下都能很容易地對這8MB尋址。因此,內核必須創建一個映射,把從0x0000 0000到0x007f ffff的線性地址和從0xc000 0000到0xc07f ffff的線性地址映射到從0x0000 0000到0x007f ffff的物理地址。換句話說,內核在初始化的第一階段,可以通過與物理地址相同的線性地址或者通過從0xc000 0000開始的8MB線性地址對RAM的前8MB進行尋址。
內核通過把swapper_pg_dir所有項都填充為0來創建期望的映射,不過,0、1、0x300(十進制768)和0x301(十進制769)這四項除外。後兩項包含了從0xc000 0000到0xc07f ffff間的所有線性地址。0、1、0x300和0x301按以下方式初始化:
-
0項和0x300項的地址字段置為pg0的物理地址,而1項和0x301項的地址字段 置為緊隨pg0後的頁框的物理地址。
-
把這四個項中的Present、Read/Write和User/Supervisor標誌置位。
-
把這四個項中的Accessed、Dirty、PCD、PWD和Page Size標誌清0。
匯編語言函數startup_32()也啟用分頁單元,通過向cr3控制寄存器裝入swapper_pg_dir的地址及設置cr0控制寄存器的PG標誌來達到這一目的。下面是等價的代碼片段:
movl $swapper_pg_dir-0xc0000000,%eax movl %eax,%cr3 /*設置頁表指針*/ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /*設置分頁(PG)位“/
Linux 內存尋址