什麼是虛存?為什麼需要它?
我們知道程式碼和資料必須駐留在記憶體中才能得以運行,然而系統內 存數量很有限,往往不能容納一個完整程式的所有代碼和資料,更何況在多任務系統中,可能需要同時開啟子處理常式,畫圖程式,瀏覽器等很多任務,想讓記憶體駐 留所有這些程式顯然不太可能。因此首先能想到的就是將程式分割成小份,只讓當前系統運行它所有需要的那部分留在記憶體,其它部分都留在硬碟。當系統處理完當 前任務片段後,再從外存中調入下一個待啟動並執行任務片段。的確,老式系統就是這樣處理大任務的,而且這個工作是由程式員自行完成。但是隨著程式語言越來越高 級,程式員對系統體系的依賴程度降低了,很少有程式員能非常清楚的駕馭系統體系,因此放手讓程式員負責將程式片段化和按需調入輕則降低效率,重則使得機器 崩潰;再一個原因是隨著程式越來越豐富,程式的行為幾乎無法準確預測,程式員自己都很難判斷下一步需要載入哪段程式。因此很難再靠預見性來靜態分配固定大 小的記憶體,然後再機械地輪換程式片進入記憶體執行。系統必須採取一種能按需分配而不需要程式員幹預的新技術。
虛擬記憶體(之所以稱為虛 擬記憶體,是和系統中的邏輯記憶體和實體記憶體相對而言的,邏輯記憶體是站在進程角度看到的記憶體,因此是程式員關心的內容。而實體記憶體是站在處理器角度看到的內 存,由作業系統負責管理。虛擬記憶體可以說是映射到這兩種不同視角記憶體的一個技術手段。)技術就是一種由作業系統接管的按需動態記憶體分配的方法,它允許程式 不知不覺中使用大於實際物理空間大小的儲存空間(其實是將程式需要的儲存空間以頁的形式分散儲存在實體記憶體和磁碟上),所以說虛擬記憶體徹底解放了程式員, 從此程式員不用過分關心程式的大小和載入,可以自由編寫程式了,繁瑣的事情都交給作業系統去做吧。
實現虛擬記憶體
虛擬記憶體是將系統硬碟空間和系統實際記憶體聯合在一起供進程使用,給進程提供了一個比記憶體大得多的虛擬空間。在程式運行時,只要把虛擬位址空間的一小部分 映射到記憶體,其餘都儲存在硬碟上(也就是說程式虛擬空間就等於實際實體記憶體加部分硬碟空間)。當被訪問的虛擬位址不在記憶體時,則說明該地址未被映射到內 存,而是被存貯在硬碟中,因此需要的虛擬儲存地址隨即被調入到記憶體;同時當系統記憶體緊張時,也可以把當前不用的虛擬儲存空間換出到硬碟,來騰出實體記憶體空 間。系統如此周而復始地運轉——換入、換出,而使用者幾乎無法查覺,這都是拜虛擬記憶體機制所賜。
Linux的swap分區就是硬碟專門為虛擬儲存空間預留的空間。經驗大小應該是記憶體的兩倍左右。有興趣的話可以使用 swapon -s 查看交換分區大小。
大道理很好理解,無非是用記憶體和硬碟空間合成為虛擬記憶體空間。但是這一過程中反覆啟動並執行地址映射(虛擬位址映射到物理地址)和虛擬位址換入換出卻值得仔細推敲。系統到底是怎麼樣把虛擬位址映射到物理地址上的呢?記憶體又如何能不斷地和硬碟之間換入換出虛擬位址呢?
利用段機制能否回答上述問題呢?邏輯地址通過段機制後變為一個32位的地址,足以覆蓋4G的記憶體空間,當程式需要的虛擬位址不在記憶體時,只依靠段機制很 難進行虛擬空間地換入換出,因為不大方便把整段大小的虛擬空間在記憶體和硬碟之間調來調去(老式系統中,會笨拙地換出整段記憶體甚至整個進程,想想這樣做會有 那些惡果吧!)。所以很有必要尋找一個更小更靈活的儲存表示單位,這樣才方便虛擬位址在硬碟和記憶體之間調入調出。這個更小的儲存管理單位便是頁(4K大 小)。管理頁換入換出的機制被稱為頁機制。
因為使用頁機制的原因,通過段機制轉換得到的地址僅僅是作為一個中間地址——線性地址, 該地址不代表實際物理地址,而是代表整個進程的虛擬空間地址。線上性地址的基礎上,頁機制接著會處理線性地址映射:當需要的線性地址(虛擬空間地址)不在 記憶體時,便以頁為單位從磁碟中調入需要的虛擬記憶體;當記憶體不夠時,又會以頁為單位把記憶體中虛擬空間的換出到磁碟上。可見,利用頁來管理記憶體和磁碟(虛擬內 存)大大方便了記憶體管理的工作。毫無疑問,頁機制和虛擬記憶體管理簡直是“絕配”。
使用頁機制,4G空間被分成2的20次方個4K大小的 頁面(頁面也可定為4M大小),因此定位頁面需要的索引表(頁表)中每個索引項目至少需要20位,但是在頁表項中往往還需要附加一些頁屬性,所以頁表項實際 為32位,其中12位用來存放諸如“頁是否存在於記憶體”或“頁的許可權”等資訊。
前面我們提到了線性地址是32位。它其中高20位是對頁表的索引,低12位則給出了頁面中的位移。線性地址經過頁表找到頁面基地址後和低12位位移量相加就形成了最終需要的物理地址了。
在實際使用中,並非所有頁表項都是被存放在一個大頁表裡,因為每個頁表項佔4個位元組,如果要在一個表中存放2的20次方個頁表項,就需要4M的連續儲存空間。這麼大的連續空間可不好找,因此往往會把頁表分級儲存,比如分兩級,那麼每級頁表只需要4k連續空間了。
兩級頁表搜尋如同看章回小說,先找到在哪一章裡,然後在找在該章下的哪一節。具體過程看看下圖:
綜上所述,地址轉換工作需要兩種技術,一是段機制,二是頁機制。段機制處理邏輯地址向線性地址的映射;頁機制則負責把線性地址映射為物理地址。兩級映射共同完成了從程式員看到的邏輯地址轉換到處理器看到的物理地址這一艱巨任務。
你可以將這兩種機制分別比作一個地址轉換函式,段機制的變數是邏輯地址,函數值是線性地址;頁機制的變數是線性地址,函數值是物理地址。地址轉換過程如下所示。
邏輯地址——(段函數)——>線性地址——(頁函數)——>物理地址。
雖然段機制和頁機制都參與映射,但它們分工不同,而且相互獨立互不干擾,彼此之間不必知道對方是否存在。
下面我們結合Linux執行個體簡要地看看段頁機制如何使用。
Linux中的分段策略
段機制在Linux裡用得有限,並沒有被完全利用。每個任務並未分別安排各自獨立的資料區段,程式碼片段,而是僅僅最低限度的利用段機制來隔離使用者資料和系統 資料——Linux只安排了四個範圍一樣的段,核心資料區段,核心程式碼片段,使用者資料區段,使用者程式碼片段,它們都覆蓋0-4G的空間,所不同的是各段屬性不同,內 核段特權級為0,使用者段特權級為3。這樣分段,避免了邏輯地址到線性地址的轉換步驟(邏輯地址就等於線性地址),但仍然保留了段的等級這層最基本保護。
每個使用者進程都可以看到4G大小的線性空間,其中0-3G是使用者空間,使用者態進程可以直接存取;從3G-4G空間為核心空間,存放核心代碼和資料,只有核心態進程能夠直接存取,使用者態進程不能直接存取,只能通過系統調用和中斷進入核心空間,而這時就要進行的特權切換。
說 到特權切換,就離不開任務門,陷阱門/中斷門等概念。陷阱門和中斷門是在發生陷阱和中斷時,進入核心空間的通道。調用門是使用者空間程式相互訪問時所需要的 通道,任務門比較特殊,它不含任何地址,而是服務於任務切換(但linux任務切換時並未真正採用它,它太麻煩了)。
對於各種門系統都會有對應的門描述符,和段描述符結構類似,門描述符也是由對應的門選擇字索引,並且最終會產生一個指向特定段內位移地址的指標。這個指標指向的就是將要進入的入口。利用門的目的就是保證入口可控,不至於進入到核心中不該訪問的位置。
Linux中的分頁策略
看看linux中如何使用分頁。
Linux中每個進程都會有各自不同的頁表,也就是說進程的映射函數互不相同,保證每個進程虛擬位址不會映射到相同的物理地址上。這是因為進程之間必須相互獨立,各自的資料必須隔離,防止資訊洩漏。
需要注意的是,核心作為必須保護的單獨部分,它有自己獨立的頁表來映射核心空間(並非全部空間,僅僅是實體記憶體大小的空間),該頁表 (swapper_pg_dir)被靜態分配,它只來映射核心空間(swapper_pg_dir只用到768項以後的項——768個頁目錄可映射3G空 間)。這個獨立頁表保證了核心虛擬空間獨立於其他使用者程式空間,也就是說其他進程通常狀態下和核心是沒有聯絡的(在編譯核心的時候,核心代碼被指定連結到 3G以上空間),因而核心資料也就自然被保護起來了。
那麼在使用者進程需要訪問核心空間時如何做呢?
Linux採用了個巧妙的方法:使用者進程頁表的前768項映射進程空間(<3G,因為LDT 中只指定基地址為0,範圍只能到0xc0000000),如果進程要訪問核心空間,如調用系統調用,則進程的頁目錄中768項後的表項將指向 swapper_pg_dir的768項後的項,所以一旦使用者陷入核心,就開始使用核心的頁表swapper_pg_dir了,也就是說可以訪問核心空間 了。