前置知識:
分段的概念(當然手寫過肯定是墜吼的
為什么要分頁
當我們寫程序的時候,總是傾向於把一個完整的程序分成最基本的數據段,代碼段,棧段。並且普通的分段機制就是在進程所屬的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)。