操做系統:x86下內存分頁機制 (1)

前置知識:

分段的概念(固然手寫過確定是墜吼的程序員

爲何要分頁

當咱們寫程序的時候,老是傾向於把一個完整的程序分紅最基本的數據段,代碼段,棧段。而且普通的分段機制就是在進程所屬的LDT中把每個段給標識出來。可是在實際運用中,大多數進程不會無限地運行下去。當進程結束以後它佔有的內存空間也會被釋放。可是這樣就會出現一個問題:內存碎片致使的內存使用效率低下算法

1

當進程A準備載入內存的時候,實際上內存的剩餘空間是足夠放下的。可是進程A中的藍色段沒法直接放入內存中(假設這一段是代碼段)。也就是說咱們必須等待內存中的進程被釋放的時候才能載入進程A。很明顯,等待的工做是很是使人厭煩的,因此咱們必須得想出一種辦法能夠避免這種等待。windows

分頁基本思想

其實咱們能夠類比分段的思想——分段實際上是站在程序員的角度來解讀程序:代碼段,數據段,堆棧段等等等等,每個段都不定長,可是都有着很明顯的用途。分段實際上是站在操做系統的角度來看程序:咱們直接把程序分紅一個個固定長度的頁,同時也把物理內存也分紅同等大小的頁,而後經過一個進程內部的表來把頁和頁映射起來。這種映射並不保證在物理內存上,頁和頁是連續的。可是會保證在程序的角度的內存,也就是虛擬內存上是連續的。經過一個表把連續的虛擬內存映射到不連續的物理內存上去來解決上面的問題。就像這樣:緩存

2

特別地,咱們稱在虛擬內存頁面中每個頁叫作「頁面」,物理內存中每個也叫作「頁框」。程序在執行的時候一般只會提供虛擬內存地址,而後cpu經過MMU(內存管理單元)來實現從虛擬地址到物理地址的映射查詢。程序對這個過程徹底不知道,程序只知道本身給出了一個地址,cpu返回了地址上的值。oop

打個比方操作系統

程序須要訪問8745的虛擬內存地址,8745=2 * 4096+553,假設分頁表裏面2號頁面對應着13號頁框。cpu會訪問13號頁框下的553偏移處的數據,也就是13 * 4096+553=53801處的內存。每個進程都會保留一個分頁表,也就是說對於一開始的例子,咱們只用把這些零散的內存映射到連續的虛擬內存中去就行了。指針

頁的大小一般爲4k,也就是4096個字節。code

可是此時又會有一個問題,就是咱們存儲頁表自己所佔據的空間會被拉大。假設每個進程所附帶的頁表中頁的數量爲1M,而且每一頁的大小爲4k,也就是說一個進程會使用大概4M的空間用來尋址。一半相似於windows的大型操做系統在初始化的時候會同時加載50多個進程,也就是說光用來尋址的內存佔用就有大概200M。這個開銷仍是比較大的,因此咱們經過使用二級頁表來縮小這種內存上的開銷。blog

層次化的分頁結構

這裏我須要把上面所說到了"頁表"的概念拆開成兩個東西——"頁目錄表"和"頁表"。32位操做系統能夠訪問的內存有4GB,也就是1024 * 1024 * 4k,也就是說對應着1024 * 1024個頁表。咱們仍是每1024頁分一個頁表,而後經過一個新的特殊頁表(叫作頁目錄)來存放這些頁表的基址(頁目錄的基址存放在cr3寄存器中,而且每個進程都有一個本身的頁目錄)。索引

表面上來看這樣並不會節省空間,可是實際上每個進程只用保證頁目錄表在物理內存中就行了,頁表能夠在後續操做中分配,也就是說不用一次性存儲全部的頁表。

能夠把頁目錄表當作頁表的索引,或者相似於二級指針的東西。

對於一個32位地址,若是咱們採起二級頁表的方式尋址,則其尋址規則是這樣的:

3

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)。

下一篇博客應該會講到快表和內存換入/換出算法

相關文章
相關標籤/搜索