前置知識:
分段的概念(當然手寫過肯定是墜吼的
為什麼要分頁
當我們寫程式的時候,總是傾向於把一個完整的程式分成最基本的資料段,程式碼段,棧段。並且普通的分段機制就是在程式所屬的LDT中把每一個段給標識出來。但是在實際運用中,大多數程式不會無限地執行下去。當程式結束之後它佔有的記憶體空間也會被釋放。但是這樣就會出現一個問題:記憶體碎片導致的記憶體使用效率低下
當程式A準備載入記憶體的時候,實際上記憶體的總剩餘空間是足夠放下的。但是程式A中的藍色段無法直接放入記憶體中(假設這一段是程式碼段)。也就是說我們必須等待記憶體中的程式被釋放的時候才能載入程式A。很明顯,等待的工作是非常令人厭煩的,所以我們必須得想出一種辦法可以避免這種等待。
分頁基本思想
其實我們可以類比分段的思想——分段其實是站在程式設計師的角度來解讀程式:程式碼段,資料段,堆疊段等等等等,每一個段都不定長,但是都有著很明顯的用途。分段其實是站在作業系統的角度來看程式:我們直接把程式分成一個個固定長度的頁,同時也把實體記憶體也分成同等大小的頁,然後通過一個程式內部的表來把頁和頁對映起來。這種對映並不保證在實體記憶體上,頁和頁是連續的。但是會保證在程式的角度的記憶體,也就是虛擬記憶體上是連續的。通過一個表把連續的虛擬記憶體對映到不連續的實體記憶體上去來解決上面的問題。就像這樣:
特別地,我們稱在虛擬記憶體頁面中每一個頁叫做“頁面”,實體記憶體中每一個也叫做“頁框”。程式在執行的時候通常只會提供虛擬記憶體地址,然後cpu通過MMU(記憶體管理單元)來實現從虛擬地址到實體地址的對映查詢。程式對這個過程完全不知道,程式只知道自己給出了一個地址,cpu返回了地址上的值。
打個比方
程式需要訪問8745的虛擬記憶體地址,8745=2 * 4096+553,假設分頁表裡面2號頁面對應著13號頁框。cpu會訪問13號頁框下的553偏移處的資料,也就是13 * 4096+553=53801處的記憶體。每一個程式都會保留一個分頁表,也就是說對於一開始的例子,我們只用把這些零散的記憶體對映到連續的虛擬記憶體中去就好了。
頁的大小通常為4k,也就是4096個位元組。
但是此時又會有一個問題,就是我們儲存頁表本身所佔據的空間會被拉大。假設每一個程式所附帶的頁表中頁的數量為1M,並且每一頁的大小為4k,也就是說一個程式會使用大概4M的空間用來定址。一半類似於windows的大型作業系統在初始化的時候會同時載入50多個程式,也就是說光用來定址的記憶體佔用就有大概200M。這個開銷還是比較大的,所以我們通過使用二級頁表來縮小這種記憶體上的開銷。
層次化的分頁結構
這裡我需要把上面所說到了"頁表"的概念拆開成兩個東西——"頁目錄表"和"頁表"。32位作業系統可以訪問的記憶體有4GB,也就是1024 * 1024 * 4k,也就是說對應著1024 * 1024個頁表。我們還是每1024頁分一個頁表,然後通過一個新的特殊頁表(叫做頁目錄)來存放這些頁表的基址(頁目錄的基址存放在cr3暫存器中,並且每一個程式都有一個自己的頁目錄)。
表面上來看這樣並不會節省空間,但是實際上每一個程式只用保證頁目錄表在實體記憶體中就好了,頁表可以在後續操作中分配,也就是說不用一次性儲存所有的頁表。
可以把頁目錄表看成頁表的索引,或者類似於二級指標的東西。
對於一個32位地址,如果我們採取二級頁表的方式定址,則其定址規則是這樣的:
CR3儲存的是頁目錄表的基地址,地址前10位儲存的是頁目錄表內的偏移(具體指向了某一個頁表的基地址),中10位儲存的是頁表內的偏移,通過訪問具體的頁表項得到實體記憶體中某一個頁框的基地址,然後最後12位用來儲存基址向上的偏移。這個過程相信通過圖片已經可以很清晰地看出來了,這裡就不再多說了。
頁表項的構成
其實頁表中的頁表項並不是完全只儲存頁框基地址的,在裡面還會儲存頁框的屬性。
保護位
顧名思義,保護位就是代表著某個表項允許什麼型別的訪問,最簡單的就是讀或者寫(0是隻讀,1是讀寫),再就是是否可執行。一個保護位一般有2bit。
修改位 & 訪問位
這一位在計算機對某一個頁面進行訪問/修改的時候會發生變化,它們主要被用來為記憶體換入/換出演算法提供一個參考。
禁止快取記憶體位
當記憶體中的某些頁面被對映到IO裝置,並且系統正在等待著IO裝置響應時,這些頁面不能被載入到快取記憶體中去,否則系統訪問的就是一箇舊的,在快取記憶體中的副本而不是源源不斷地從裝置處獲取資料。
開啟分頁功能(程式碼來自《Orange's 一個作業系統的實現》):
PageDirBase equ 200000h ; 頁目錄開始地址: 2M
PageTblBase equ 201000h ; 頁表開始地址: 2M+4K
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, DA_DRW;Page Directory
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, DA_DRW|DA_LIMIT_4K;Page Tables
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SetupPaging:
; 為簡化處理, 所有線性地址對應相等的實體地址.
; 首先初始化頁目錄
mov ax, SelectorPageDir ; 此段首地址為 PageDirBase
mov es, ax
mov ecx, 1024 ; 共 1K 個表項
xor edi, edi
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 為了簡化, 所有頁表在記憶體中是連續的.
loop .1
; 再初始化所有頁表 (1K 個, 4M 記憶體空間)
mov ax, SelectorPageTbl ; 此段首地址為 PageTblBase
mov es, ax
mov ecx, 1024 * 1024 ; 共 1M 個頁表項, 也即有 1M 個頁
xor edi, edi
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一頁指向 4K 的空間
loop .2
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
除了一開始初始化了段和段選擇子(用作正常的記憶體訪問),其實就是初始化了頁目錄表和頁表,同時用頁目錄表基址填充cr3暫存器。這裡為了方便起見,頁目錄表和頁表的位置都是連續的(畢竟只是一個demo)。
下一篇部落格應該會講到快表和記憶體換入/換出演算法