一. 頁式記憶體管理介紹
80386能夠將記憶體分為不同屬性的段,並通過段描述符、段表以及段選擇子等機制,通過段基址和段內偏移量計算出線性地址進行訪問,這一記憶體管理方式被稱為段式記憶體管理。
這裡要介紹的是另一種記憶體管理的方式:80386在開啟了分頁機制後,便能夠將實體記憶體劃分為一個個大小相同且連續的實體記憶體頁,訪問時通過實體記憶體頁號和頁內偏移計算出最終需要訪問的線性地址進行訪問,由於記憶體管理單元由段變成了頁,因此這一記憶體管理方式被稱為頁式記憶體管理。
80386的分頁機制只能在保護模式下開啟。
為什麼需要頁式記憶體管理?
在介紹80386分頁機制前,需要先理解為什麼CPU在管理記憶體時,要在段式記憶體管理的基礎上再引入一種有很大差異的頁式記憶體管理方式?頁式記憶體管理與純段式記憶體管理相比到底具有哪些優點?
一個很重要的原因是為了解決多工環境下,段式記憶體管理中多工的建立與終止時會產生較多記憶體碎片,使得記憶體空間使用率不高的問題。
記憶體碎片分為外碎片和內碎片兩種。
外碎片
對於指令和資料的訪問通常都是連續的,所以需要為一個任務分配連續的記憶體空間。在段式記憶體管理中,通常為任務分配一個完整的記憶體段,或是按照任務內段功能的不同,分配包括程式碼段、資料段和堆疊段在內的多個完整連續段空間。支援多道任務的系統分配的記憶體空間,會在某些任務退出並釋放記憶體時,產生外部記憶體碎片。
舉個例子,假設當前存在10MB的記憶體空間,存在A/B/C/D四個任務,併為每個任務分配一整塊的記憶體空間,其所佔用的記憶體空間分別為3MB/2MB/4MB/1MB,如下圖所示(一個格子代表1MB記憶體)。
當任務B和任務D執行完成後,所佔用的記憶體空間被釋放,10MB的記憶體空間中出現了3MB大小的空閒記憶體。如果此時出現了一個任務E,需要為其分配3MB的記憶體空間,此時記憶體雖然存在3MB的記憶體空間,卻由於空閒記憶體的不連續,碎片化,導致無法直接分配給任務E使用。而這裡任務B、任務D結束後釋放的空餘記憶體空間就被視為外碎片。
這裡的例子任務數量少且記憶體空間也很小。而在實際的32位甚至64位的系統中,實體記憶體空間少則4GB,多則幾十甚至上百GB,由於任務記憶體的反覆分配和釋放,導致出現的外碎片的數量及浪費的記憶體空間會很多,很大程度上降低了記憶體空間的利用率。
雖然理論上能夠通過作業系統小心翼翼的挪動記憶體,使得外碎片能夠拼接為連續的大塊,得以被有效利用(記憶體緊縮)。但是作業系統挪動、複製記憶體本身很佔用CPU資源,且存在對指令進行地址重定位、暫時暫停對所挪動記憶體區域的訪問等附加問題,造成的效率降低程度幾乎是不可忍受的,因此這一解決方案並沒有被廣泛使用。
內碎片
外碎片指的是不同任務記憶體之間的碎片,而內碎片指的是一個任務內產生的記憶體碎片。
通常作業系統為了管理多工環境下的實體記憶體,會將記憶體分隔為固定大小的分割槽,使用系統表記錄對應分割槽記憶體的使用情況(如是否已分配等)。分割槽的大小必須適當,如果分割槽過小,則相同實體記憶體大小下,系統表項過多使得所佔用的空間過大;可如果分割槽過大,則會產生過大的內碎片,造成不必要的記憶體空間浪費。
以上述介紹外碎片的資料為例,系統中的記憶體分割槽固定大小為1MB,其中為任務C分配了4個記憶體分割槽,共4MB大小。可實際上任務C實際只需要3.5MB的空間即可滿足需求,但由於分割槽是記憶體管理的最小單元,只能為任務分配整數個的記憶體分割槽。3個分割槽3MB並不滿足任務C的3.5MB的記憶體需求,因此只能分配4個分割槽給任務C。而這裡任務C額外多佔用的0.5MB記憶體就是內碎片。
內碎片就是已經被分配出去,卻不能被有效利用的記憶體空間。
80386是如何解決記憶體碎片問題的?
外碎片的解決
外碎片問題產生的主要原因是程式所需要分配的記憶體空間是連續的。為此,80386提供了分頁機制,使得最終分配給任務的實體記憶體空間可以不連續。如果任務所使用的記憶體不必連續,前面外碎片例子中提到的任務E就能夠在1MB+2MB的離散實體記憶體上正常執行,外碎片問題自然就得到了解決。
內碎片的解決
內碎片從本質上來說是很難完全避免的(記憶體管理最小單元不能過小),主要的問題在於前面提到的記憶體分割槽管理單元大小的較優值不好確定。開啟了分頁管理的80386,允許將實體記憶體分割最小為4KB固定大小的管理單元,這個固定大小的記憶體管理單元被稱為頁,並由專門的被稱為頁表的資料結構來追蹤記憶體頁的使用情況。
對於頁表項過多的問題,80386的設計者提供了多級頁表機制,減少了頁表所佔用的空間。
對於內碎片過大的問題,由於80386所執行的任務所佔用的記憶體段一般遠大於一個記憶體頁的大小,因此頁機制下所產生的內部碎片是十分有限的,可以達到一個令人滿意的記憶體使用率。
二. 虛擬記憶體簡單介紹
為了解決應用程式高速增長的記憶體需求與實體記憶體增加緩慢的矛盾,電腦科學家們提供了虛擬記憶體的概念。使用了虛擬記憶體的系統,可以使得系統內執行的程式所佔用的記憶體空間總量,遠大於實際實體記憶體的容量。
能夠實現虛擬記憶體的關鍵在於程式在特定時刻所需要訪問的記憶體地址是符合區域性性原理的。通過作業系統和硬體的緊密配合,能夠將任務暫時不需要訪問的記憶體交換到外部硬碟中,而將實體記憶體留給真正需要訪問的那部分記憶體(工作集記憶體)。
虛擬記憶體和分頁機制是一對好搭檔,分頁機制提供了管理記憶體的基本單位:頁,80386的頁式虛擬記憶體實現在工作集記憶體排程時也依賴分頁機制提供的頁來進行。隨著程式的執行,程式的工作集記憶體在動態變化,當CPU檢測到當前所訪問的記憶體頁不在實體記憶體中時,便會通知作業系統(記憶體缺頁異常),作業系統的缺頁異常處理程式會將硬碟交換區中的對應記憶體頁資料寫回實體記憶體。如果實體記憶體頁已經滿了的情況下,則還需要根據某種演算法將另一個實體記憶體頁替換,來容納這一換入的記憶體頁。
三. 80386分頁機制原理
在介紹分頁機制原理之前,需要先理解關於80386保護模式下32位記憶體定址時幾種地址的概念。
實體地址(Physical Address):
實體地址就是32位的地址匯流排所對應的真實的硬體儲存空間。對於實體記憶體的訪問,無論中間會經過多少次轉換,最終必須轉換為最終的實體地址進行訪問。
邏輯地址(Logical Address):
在80386保護模式的程式指令中,對記憶體的訪問是由段選擇子和段內偏移決定的。段選擇子+段內偏移 --> 邏輯地址。
線性地址(Linear Address):
CPU在記憶體定址時,從指令中獲得段選擇子和段內偏移,即邏輯地址。由段選擇子在段表(GDT或LDT)中找到對應的段描述符,獲取段基址。段基址+段內偏移決定線性地址。
如果沒有開啟分頁,CPU就使用生成的線性地址直接作為最終的實體地址進行訪問;如果開啟了分頁,則還需要通過頁表等機制,將線性地址進一步處理才能生成實體地址進行訪問。
頁式虛擬記憶體實現原理
程式要求訪問一個段時,其線性地址必須是連續的。在純粹的段式記憶體管理中,線性地址等於實體地址的情況下,就會出現外碎片的問題。而在段式記憶體管理的基礎上,80386如果還開啟了頁機制,就能通過抽象出一層線性地址到實體地址的對映,使得最終分配給程式的實體記憶體段不必連續。
80386中的記憶體頁大小為4KB,在32位的記憶體定址空間中(4GB),存在著0x10000 = 1048576個頁。每個頁對應的起始地址低12位都為0,第一個實體記憶體頁的實體地址為0x00000000,第二個實體記憶體頁的實體地址為0x00001000,依此類推,最後一個物理頁的實體地址是0xFFFFF000。
頁表
在80386的分頁機制的實現中,是通過頁表來實現線性地址到實體地址對映轉換的。每個任務都有一個自己的頁表,記錄著任務的線性地址到實體地址的對映關係。
開啟了頁機制後的線性地址也被稱為虛擬地址,這是因為線性地址已經不再直接對應真實的實體地址,而是一個不承載真實資料的虛擬記憶體地址。開啟了分頁機制後,一個任務的虛擬地址空間依然是連續的,但所佔用的實體地址空間卻可以不連續。
頁表儲存著被稱為頁表項的資料結構集合,每一個頁表項都記載著一個虛擬記憶體頁到實體記憶體頁的對映關係。開啟了頁機制之後,CPU在記憶體定址時,在通過段表計算出了線性地址(虛擬地址)後,便可以在連續排布的虛擬地址空間中找到對應的頁表項,通過頁表項獲取虛擬記憶體頁所對應的實體記憶體頁地址,進行實體記憶體的訪問。虛擬地址到實體地址對映的細節會在後面進行展開。
由於是將不斷變化的虛擬記憶體頁裝載進相對不變的實體記憶體頁中,就像畫廊中展示的畫會不斷的更替,但畫框基本不變一樣。為了更好的區分這兩者,頁通常特指虛擬記憶體頁,而實體記憶體頁則被稱為頁框。
頁表項介紹
頁表項是32位的,其結構如下圖所示。
P位:
P(Present)位,存在位。標識當前虛擬記憶體頁是否存在於實體記憶體頁中。當P位為1時,表示當前虛擬記憶體頁存在於實體記憶體中,可以直接進行訪問。當P位為0時,表示對應的實體記憶體頁不存在,需要新分配實體記憶體頁或是從磁碟中將其排程回實體記憶體。
分頁模式下的記憶體定址,如果CPU發現對應的頁表項P位為0,會引發缺頁異常中斷,作業系統在缺頁異常處理程式中進行對應的處理,以實現虛擬記憶體。
RW位:
RW(Read/Write)位,讀寫位。標識當前頁是否能夠寫入。當RW為1時,代表當前頁可讀可寫;當RW為0時,代表當前頁是隻讀的。
US位:
US(User/Supervisor)位,使用者/管理位。當US為1時,標識當前頁是使用者級別的,允許所有當前特權級的任務進行訪問。當US為0時,表示當前頁是屬於管理員級別的,只允許當前特權級為0、1、2的任務進行訪問,而當前特權級為3的使用者態任務無法進行訪問。
PWT位/PCD位:
PWT(Page-level Write Through)位,頁級通寫位。PWT為1時,表示當前物理頁的快取記憶體採用通寫法;PWT為0時,表示當前物理頁的快取記憶體採用回寫法。
PCD(Page-level Cache Disable)位,頁級快取記憶體禁止位。PCD為1時,表示訪問當前物理頁禁用快取記憶體;PCD為0時,表示訪問當前物理頁時允許使用快取記憶體。
PWT與PCD位的使用,涉及到了80386快取記憶體的工作原理與記憶體一致性問題,限於篇幅不在這裡展開。
A位:
A(Access)位,訪問位。A位為1時,代表當前頁曾經被訪問過;A位為0時,代表當前頁沒有被訪問過。
A位的設定由CPU韌體在對應記憶體頁訪問時自動設定為1,且可以由作業系統在適當的時候通過程式指令重置為0,用以計算記憶體頁的訪問頻率。通過訪問頻率,作業系統能夠以此作為虛擬記憶體排程演算法中評估的依據,在實體記憶體緊張的情況下,可以選擇將最少使用的記憶體頁換出,以減少不必要的虛擬記憶體頁排程時的磁碟I/O,提高虛擬記憶體的效率。
D位:
D(Dirty)位,髒位。當D位為1時,表示當前頁被寫入修改過;D位為0時,代表當前頁沒有被寫入修改過。
髒位由CPU在對應記憶體頁被寫入時自動設定為1。作業系統在進行記憶體頁排程時,如果發現需要被換出的記憶體頁D位為1時,則需要將對應實體記憶體頁資料寫回虛擬頁對應的磁碟交換區,保證磁碟/記憶體資料的一致性;當發現需要被換出的實體記憶體頁的D位為0時,表示當前頁自從換入實體記憶體以來沒有被修改過,和磁碟交換區中的資料一致,便直接將其覆蓋,而不進行磁碟的寫回,減少不必要的I/O以提高效率。
PAT位:
PAT(Page Attribute Table),頁屬性表支援位。PAT位的存在使得CPU能夠支援更復雜的,不同頁大小的分頁管理。當PAT=0時,每一頁的大小為4KB;當PAT=1時,每一頁的大小是4MB,或是其它大小(分CPU的情況而定)。
G位:
G(Global),全域性位。表示當前頁是否是全域性的,而不是屬於某一特定任務的。G=1時,表示當前頁是全域性的;G=0時,表示當前頁是屬於特定任務的。
為了加速頁表項的訪問,80386提供了TLB快表,作為頁表訪問的快取記憶體。當任務切換時,TLB內所有G=0的非全域性頁將會被清除,G=1的全域性頁將會被保留。將作業系統核心中關鍵的,頻繁訪問的頁設定為全域性頁,使得其能夠一直儲存在TLB快表中,加速對其的訪問速度,提高效率。
AVL位:
AVL(Avaliable),可用位。和段描述符中的AVL位功能類似,CPU並不使用它,而是提供給作業系統軟體自定義使用。
頁物理基地址欄位:
頁物理基地址欄位用於標識對應的物理頁,共20位。
由於32位的80386的頁最小是4KB,而4GB的實體記憶體被分解為了最多0x10000個4KB的物理頁。20位的頁物理基地址欄位作為物理頁的索引標號與每一個具體的物理頁一一對應。通過頁物理基地址欄位,便能找到唯一對應的實體記憶體頁。
多級頁表
在32位的CPU中,作業系統可以給每個程式分配至多4GB的虛擬記憶體空間,如果一個記憶體頁佔4KB,那麼對應的每個程式的頁表中最多需要存放著0x10000個頁表項來進行對映。即使每個頁表項只佔小小的32位共4個位元組(4Byte),這依然是一個不小的記憶體開銷(0x10000個頁表項的大小為4MB)。
一個應用程式雖然可以被分配4GB的虛擬記憶體空間,但實際上可能只使用其中的一小部分,例如40MB的大小。通常程式的堆疊段和資料段都分別位於虛擬記憶體空間的高低兩端,並隨著程式的執行慢慢的向中間擴充套件,由於頁表項對應與虛擬地址空間的連續性,這就要求任務在執行時必須完整的定義整張頁表。
可以看到,一級的平面頁表結構存在著明顯的頁表空間浪費的問題。雖然可以要求應用程式不要一下子就以4GB的記憶體規格進行程式設計,而是一開始用較小的記憶體,並在需要更大記憶體時梯度的申請更大的記憶體空間,並重新構造資料段和堆疊段以減少每個任務的無用頁表項空間的浪費。但這將頁表空間優化的繁重任務強加給了應用程式,並不是一個好的解決辦法。
為此,電腦科學家們提出了多級頁表的方案來解決頁表項過多的問題。多級頁表顧名思義,頁表的結構不再是一個一級的平面結構(一級頁表),而是像一顆樹一樣,由頁目錄項節點和頁表項節點組成。目錄節點中儲存著下一級節點的物理頁地址等資訊,葉子節點中則包含著真正的頁表項資訊。查詢頁表項時,從一級頁目錄節點(根目錄)出發,按照一定的規則可以找到對應的下一級子目錄節點,直到查詢出對應的葉子節點為止。
80386頁目錄項介紹
80386採用的是二級頁表的設計,二級頁表由頁目錄表和頁表共同組成。頁目錄表中存放的是頁目錄項,頁目錄項的大小和頁表項一致,為4位元組。
通過80386指令得到的32位線性地址,其中高20位作為頁表項索引,低12位作為頁內偏移地址(4KB大小的物理頁)。如果採用的是一級頁表結構,20位的頁表項索引能直接找到4MB頁表中的對應頁表項。
而對於80386二級頁表的設計來說,由於一個物理頁大小為4KB,最多可以容納1024(2^10)個頁表項或者頁目錄項,所以將頁表項索引的高10位作為根目錄頁中頁目錄項的索引值,通過頁目錄項中的頁表項物理頁號可以找到對應的頁表物理頁;再根據頁表項索引的後10位找到頁表中對應的頁表項。
80386頁目錄項結構圖
80386的二級頁表的頁目錄項佔32位,其低12位的含義與頁表項一致。主要區別在於其高20位存放的是下一級頁表的物理頁索引,而不是虛擬地址對映的實體記憶體頁地址。
頁表基址暫存器
前面提到過,和LDT一樣,每個任務都擁有著自己獨立的頁表。為此80386CPU提供了一個專門的暫存器用於追蹤定位任務自己的頁表,這個暫存器的名稱叫做頁表基址暫存器(Page Directory Base Register,PDBR),也就是控制暫存器CR3。
由於80386分頁機制使用的是二級頁表,因此PDBR指向的是二級頁表結構中的頁目錄,通過頁目錄表便能夠間接的訪問整個二級頁表。為了效率其中存放的直接就是頁目錄表的32位實體地址,一般由作業系統負責在任務切換時將新任務對應的頁目錄表預先載入進實體記憶體。
由於PDBR是和當前任務有關的,在任務切換時會被新任務TSS中的PDBR欄位值所替換,指向新任務的頁目錄表,而舊任務的PDBR的值則在保護現場時被存入對應的TSS中。
多級頁表是如何解決頁表項浪費問題的?
以80386的二級頁表設計為例,最大4GB的虛擬記憶體空間下,無論如何一級頁目錄表是必須存在的。當不需要為應用程式分配過多的記憶體時,頁目錄表中的頁目錄項所指向的對應頁表可以不存在,即頁目錄項的P位為0,實際不使用的虛擬記憶體空間將沒有對應的二級頁表節點,相比一級頁表的設計其浪費的記憶體會少很多。
假設需要為一個虛擬地址首尾各需要分配20MB,共佔用40MB記憶體的任務構建對應的頁表。
1. 如果使用一級頁表,4GB的虛擬記憶體空間下需要提供0x10000個頁表項,共4MB,頁表的體積達到了任務自身所需40MB記憶體的10%,但其中絕大多數的頁表項都是沒用的(P位為0),不會對應實際的實體記憶體,空間效率很低。
2. 如果使用二級頁表,除了佔一個物理頁4KB大小的頁目錄表是必須存在的外,其頁目錄表中只有首尾兩項的P位為1,分別指向一個實際存在的頁表(二級節點),頁目錄表中間其它的頁目錄項P位都為0,不需要為這些不會使用到的虛擬地址分配頁表。對於這個40MB的程式來說,其頁表只佔了3個物理頁面,共12KB,空間效率相比一級頁表高很多。
TLB快表
前面提到了多級頁表所帶來的好處:通過頁表分層,可以減少順序排列的無效頁表項數量,節約記憶體空間;頁表的層級越多,空間效率也越高。
計算機領域中,通常並沒有免費的午餐,一個問題的解決,往往會帶來新的問題:多級頁表本質上是一個樹狀結構,每一個節點頁都是離散的,因此每一層級訪問都需要進行一次記憶體定址操作,頁表的層級越多,訪問的次數也就越多,虛擬頁地址對映過程也越慢。在32位的80386中,2級頁表下問題還不算特別嚴重;但64位CPU的出現帶來了更大的定址空間,也需要更多的頁表項,頁表的層級也漸漸的從2級變成了3級、4級甚至更多。頁機制開啟之後,所有的記憶體定址都需要經過CPU的頁部件進行轉化才能獲得最終的實體地址,因此這一過程必須要快,不能因為頁表的離散層次訪問就嚴重影響虛擬地址空間到實體地址空間的轉換速度。
要加快原本相對耗時的查詢操作,一個常用的辦法便是引入快取。為了加速通用記憶體的訪問,80386利用區域性性原理提供了快取記憶體;為了加速多級頁表的頁表項訪問,80386提供了TLB。
TLB(Translation Lookaside Buffer)直譯為地址轉換後援緩衝器,根據其作用也被稱為頁表快取或是快表(快速頁表)。TLB中存放著一張表,其中的每一項用於快取當前任務虛擬頁號和對應頁表項中的關鍵資訊,被稱為TLB項。
TLB的工作原理和快取記憶體類似:當CPU訪問某一虛擬頁時,通過虛擬頁號先在TLB中尋找,如果發現對應的TLB項存在,則直接以TLB項中的資料進行實體地址的轉換,這被稱為TLB命中;當發現對應的TLB項不存在時(TLB未命中),則進行記憶體的訪問,在獲取記憶體中頁表項資料的同時,也將對應頁表項快取入TLB中。如果TLB已滿則需要通過某種置換演算法選出一個已存在的TLB項將其替換。
TLB的查詢速度比記憶體快,但容量相對記憶體小很多,因此只能快取數量有限的頁表項。但由於記憶體訪問的區域性性,只要通過合理的設計提高TLB的命中率(通常可以達到90%以上),就能達到很好的效果。
四. 80386分頁機制下的記憶體定址流程
下面總結一下開啟了分頁機制的80386是如何進行記憶體定址的。
1. CPU首先從記憶體訪問指令中獲取段選擇子和段內偏移地址
2. 根據段選擇子從段表(GDT或LDT)中查詢出對應的段描述符
3. 根據段描述符中的段基址和指令中的段內偏移地址生成32位的線性地址(頁機制下的虛擬地址)
4. 32位的線性地址根據80386二級頁表的設計,拆分成三個部分:高10位作為頁目錄項索引,中間次高10位作為頁表項索引,低12位作為頁內偏移地址。
5. 通過高10位的頁目錄項索引從一級頁目錄表中獲取二級頁表的物理頁地址(通過物理頁框號可得),再根據中間10位的頁表項索引找到對應的物理頁框。根據物理頁框號與頁內偏移地址共同生成最終的實體地址,進行實體記憶體的訪問。
五. 總結
想要通過學習作業系統來更好的理解計算機程式底層的工作原理,基礎的硬體知識是必須要了解的。紙上得來終覺淺,絕知此事要躬行,在理解了基礎原理後,還需要通過實踐來加深對原理知識的理解,而閱讀相關作業系統的實現原始碼就是一個很好的將實踐與原理緊密結合的學習方式。
希望通過對硬體和作業系統的學習能幫助我開啟計算機程式底層執行的神祕黑盒子一窺究竟,在思考問題時能夠換一個角度從底層的視角出發,去更好的理解和掌握上層的應用技術,以避免迷失在快速發展的技術浪潮中。