1. 程式人生 > 實用技巧 >Linux虛擬記憶體

Linux虛擬記憶體

記憶體是程式得以執行的重要物質基礎。如何在有限的記憶體空間執行較大的應用程式,曾是困擾人們的一個難題。為解決這個問題,人們設計了許多的方案,其中最成功的當屬虛擬記憶體技術。Linux作為一個以通用為目的的現代大型作業系統,當然也毫不例外的採用了優點甚多的虛擬記憶體技術。

虛擬記憶體

為了執行比實際實體記憶體容量還要大的程式,包括Linux在內的所有現代作業系統幾乎毫無例外的都採用了虛擬記憶體技術。虛擬記憶體技術,可讓系統看上去具有比實際物理意義記憶體大的多的記憶體空間,併為實現多道程式的執行創造了條件。

虛擬記憶體的概念

總所周知,為了對記憶體中的儲存單元進行識別,記憶體中的每一個儲存單元都必須有一個確切的地址。而一臺計算機的處理器能訪問多大的記憶體空間就取決於處理器的程式計數器,該計數器的字長越長,能訪問的空間就越大。

例如:對於程式計數器位數為32位的處理器來說,他的地址發生器所能發出的地址數目為2^32=4G個,於是這個處理器所能訪問的最大記憶體空間就是4G。在計算機技術中,這個值就叫做處理器的定址空間或定址能力。

照理說,為了充分利用處理器的定址空間,就應按照處理器的最大定址來為其分配系統的記憶體。如果處理器具有32位程式計數器,那麼就應該按照下圖的方式,為其配備4G的記憶體:

這樣,處理器所發出的每一個地址都會有一個真實的物理儲存單元與之對應;同時,每一個物理儲存單元都有唯一的地址與之對應。這顯然是一種最理想的情況。

但遺憾的是,實際上計算機所配置記憶體的實際空間常常小於處理器的定址範圍

,這是就會因處理器的一部分定址空間沒有對應的物理儲存單元,從而導致處理器定址能力的浪費。例如:如下圖的系統中,具有32位定址能力的處理器只配置了256M的記憶體儲器,這就會造成大量的浪費:

另外,還有一些處理器因外部地址線的根數小於處理器程式計數器的位數,而使地址匯流排的根數不滿足處理器的定址範圍,從而處理器的其餘定址能力也就被浪費了。例如:Intel8086處理器的程式計數器位32位,而處理器晶片的外部地址匯流排只有20根,所以它所能配置的最大記憶體為1MB:

在實際的應用中,如果需要執行的應用程式比較小,所需記憶體容量小於計算機實際所配置的記憶體空間,自然不會出什麼問題。但是,目前很多的應用程式都比較大,計算機實際所配置的記憶體空間無法滿足。

實踐和研究都證明:一個應用程式總是逐段被執行的,而且在一段時間內會穩定執行在某一段程式裡。

這也就出現了一個方法:如下圖所示,把要執行的那一段程式自輔存複製到記憶體中來執行,而其他暫時不執行的程式段就讓它仍然留在輔存。

當需要執行另一端尚未在記憶體的程式段(如程式段2),如下圖所示,就可以把記憶體中程式段1的副本複製回輔存,在記憶體騰出必要的空間後,再把輔存中的程式段2複製到記憶體空間來執行即可:

在計算機技術中,把記憶體中的程式段複製回輔存的做法叫做“換出”,而把輔存中程式段對映到記憶體的做法叫做“換入”。經過不斷有目的的換入和換出,處理器就可以執行一個大於實際實體記憶體的應用程式了。或者說,處理器似乎是擁有了一個大於實際實體記憶體的記憶體空間。於是,這個儲存空間叫做虛擬記憶體空間,而把真正的記憶體叫做實際實體記憶體,或簡稱為實體記憶體。

那麼對於一臺真實的計算機來說,它的虛擬記憶體空間又有多大呢?計算機虛擬記憶體空間的大小是由程式計數器的定址能力來決定的。例如:在程式計數器的位數為32的處理器中,它的虛擬記憶體空間就為4GB。

可見,如果一個系統採用了虛擬記憶體技術,那麼它就存在著兩個記憶體空間:虛擬記憶體空間和實體記憶體空間。虛擬記憶體空間中的地址叫做“虛擬地址”;而實際實體記憶體空間中的地址叫做“實際實體地址”或“實體地址”。處理器運算器和應用程式設計人員看到的只是虛擬記憶體空間和虛擬地址,而處理器片外的地址匯流排看到的只是實體地址空間和實體地址。

由於存在兩個記憶體地址,因此一個應用程式從編寫到被執行,需要進行兩次對映。第一次是對映到虛擬記憶體空間,第二次時對映到實體記憶體空間。在計算機系統中,第兩次對映的工作是由硬體和軟體共同來完成的。承擔這個任務的硬體部分叫做儲存管理單元MMU,軟體部分就是作業系統的記憶體管理模組了。

在對映工作中,為了記錄程式段佔用實體記憶體的情況,作業系統的記憶體管理模組需要建立一個表格,該表格以虛擬地址為索引,記錄了程式段所佔用的實體記憶體的實體地址。這個虛擬地址/實體地址記錄表便是儲存管理單元MMU把虛擬地址轉化為實際實體地址的依據,記錄表與儲存管理單元MMU的作用如下圖所示:

綜上所述,虛擬記憶體技術的實現,是建立在應用程式可以分成段,並且具有“在任何時候正在使用的資訊總是所有儲存資訊的一小部分”的區域性特性基礎上的。它是通過用輔存空間模擬RAM來實現的一種使機器的作業地址空間大於實際記憶體的技術。

從處理器運算裝置和程式設計人員的角度來看,它面對的是一個用MMU、對映記錄表和實體記憶體封裝起來的一個虛擬記憶體空間,這個儲存空間的大小取決於處理器程式計數器的定址空間。

可見,程式對映表是實現虛擬記憶體的技術關鍵,它可給系統帶來如下特點:

  • 系統中每一個程式各自都有一個大小與處理器定址空間相等的虛擬記憶體空間;
  • 在一個具體時刻,處理器只能使用其中一個程式的對映記錄表,因此它只看到多個程式虛存空間中的一個,這樣就保證了各個程式的虛存空間時互不相擾、各自獨立的;
  • 使用程式對映表可方便地實現實體記憶體的共享。

Linux的虛擬記憶體技術

以儲存單元為單位來管理顯然不現實,因此Linux把虛存空間分成若干個大小相等的儲存分割槽,Linux把這樣的分割槽叫做頁。為了換入、換出的方便,實體記憶體也就按也得大小分成若干個塊。由於實體記憶體中的塊空間是用來容納虛存頁的容器,所以實體記憶體中的塊叫做頁框。頁與頁框是Linux實現虛擬記憶體技術的基礎。

虛擬記憶體的頁、實體記憶體的頁框及頁表

在Linux中,頁與頁框的大小一般為4KB。當然,根據系統和應用的不同,頁與頁框的大小也可有所變化。

實體記憶體和虛擬記憶體被分成了頁框與頁之後,其儲存單元原來的地址都被自然地分成了兩段,並且這兩段各自代表著不同的意義:高位段分別叫做頁框碼和頁碼,它們是識別頁框和頁的編碼;低位段分別叫做頁框偏移量和頁內偏移量,它們是儲存單元在頁框和頁內的地址編碼。下圖就是兩段虛擬記憶體和實體記憶體分頁之後的情況:

為了使系統可以正確的訪問虛存頁在對應頁框中的映像,在把一個頁對映到某個頁框上的同時,就必須把頁碼和存放該頁映像的頁框碼填入一個叫做頁表的表項中。這個頁表就是之前提到的對映記錄表。一個頁表的示意圖如下所示:

頁模式下,虛擬地址、實體地址轉換關係的示意圖如下所示:

也就是說:處理器遇到的地址都是虛擬地址。虛擬地址和實體地址都分成頁碼(頁框碼)和偏移值兩部分。在由虛擬地址轉化成實體地址的過程中,偏移值不變。而頁碼和頁框碼之間的對映就在一個對映記錄表——頁表中。

請頁與交換

虛存頁面到物理頁框的對映叫做頁面的載入。

當處理器試圖訪問一個虛存頁面時,首先到頁表中去查詢該頁是否已對映到物理頁框中,並記錄在頁表中。如果在,則MMU會把頁碼轉換成頁框碼,並加上虛擬地址提供的頁內偏移量形成實體地址後去訪問實體記憶體;如果不在,則意味著該虛存頁面還沒有被載入記憶體,這時MMU就會通知作業系統:發生了一個頁面訪問錯誤(頁面錯誤),接下來系統會啟動所謂的“請頁”機制,即呼叫相應的系統操作函式,判斷該虛擬地址是否為有效地址。

如果是有效的地址,就從虛擬記憶體中將該地址指向的頁面讀入到記憶體中的一個空閒頁框中,並在頁表中新增上相對應的表項,最後處理器將從發生頁面錯誤的地方重新開始執行;如果是無效的地址,則表明程序在試圖訪問一個不存在的虛擬地址,此時作業系統將終止此次訪問。

當然,也存在這樣的情況:在請頁成功之後,記憶體中已沒有空閒物理頁框了。這是,系統必須啟動所謂地“交換”機制,即呼叫相應的核心操作函式,在物理頁框中尋找一個當前不再使用或者近期可能不會用到的頁面所佔據的頁框。找到後,就把其中的頁移出,以裝載新的頁面。對移出頁面根據兩種情況來處理:如果該頁未被修改過,則刪除它;如果該頁曾經被修改過,則系統必須將該頁寫回輔存。

系統請頁的處理過程如下所示:

為了公平地選擇將要從系統中拋棄的頁面,Linux系統使用最近最少使用(LRU)頁面的衰老演算法。這種策略根據系統中每個頁面被訪問的頻率,為物理頁框中的頁面設定了一個叫做年齡的屬性。頁面被訪問的次數越多,則頁面的年齡最小;相反,則越大。而年齡較大的頁面就是待換出頁面的最佳候選者。

快表

在系統每次訪問虛存頁時,都要在記憶體的所有頁表中尋找該頁的頁框,這是一個很費時間的工作。但是,人們發現,系統一旦訪問了某一個頁,那麼系統就會在一段時間內穩定地工作在這個頁上。所以,為了提高訪問頁表的速度,系統還配備了一組正好能容納一個頁表的硬體暫存器,這樣當系統再訪問虛存時,就首先到這組硬體暫存器中去訪問,系統速度就快多了。這組存放當前頁表的暫存器叫做快表。

總之,使用虛擬儲存技術時,處理器必須配備一些硬體來承擔記憶體管理的一部分任務。承擔記憶體管理任務的硬體部分叫做儲存管理單元MMU。儲存管理單元MMU的工作過程如下圖所示:

頁的共享

在多程式系統中,常常有多個程式需要共享同一段程式碼或資料的情況。在分頁管理的儲存器中,這個事情很好辦:讓多個程式共享同一個頁面即可。

具體的方法是:使這些相關程式的虛擬空間的頁面在頁表中指向記憶體中的同一個頁框。這樣,當程式執行並訪問這些相關頁面時,就都是對同一個頁框中的頁面進行訪問,而該頁框中的頁就被這些程式所共享。下圖是3個程式共享一個頁面的例子:

頁的保護

由上可知,頁表實際上是由虛擬空間轉到物理空間的入口。因此,為了保護頁面內容不被沒有該頁面訪問許可權的程式所破壞,就應在頁表的表項中設定一些訪問控制欄位,用於指明對應頁面中的內容允許何種操作,從而禁止非法訪問。

下圖是頁表項中存放控制資訊的一種可能的形式:

注意:其中的PCD位表示著是否允許快取記憶體(cache)

如果程式對一個頁試圖進行一個該頁控制欄位所不允許的操作,則會引起作業系統的一次中斷——非法訪問中斷,並拒絕這種操作,從而保護該頁的內容不被破壞。

多級頁表

需要注意的是,頁表是作業系統建立的用於記憶體管理的表格。因此,一個程式在執行時,其頁表也要存放到記憶體空間。如果一個程式只需要一個頁表,則不會有什麼問題。但如果,程式的虛擬空間很大的話,就會出現一個比較大的問題。

比如:一個程式的虛擬空間為4GB,頁表以4KB為一頁,那麼這個程式空間就是1M頁。為了儲存這1M頁的頁指標,那麼這個頁表的長度就相當大了,對記憶體的負擔也很大了。所以,最好對頁表也進行分頁儲存,在程式執行時只把需要的頁複製到記憶體,而暫時不需要的頁就讓它留在輔存中。為了管理這些頁表頁,還要建立一個記錄頁表頁首地址的頁目錄表,於是單級頁表就變成了二級頁表。二級頁表的地址轉換如下圖所示:

當然,如果程式的虛擬空間更大,那麼也可以用三級頁表來管理。為了具有通用性,Linux系統使用了三級頁表結構:頁目錄(Page Directory,PGD)、中間頁目錄(Page Middle Directory,PMD)、頁表(Page Table,PTE)。

Linux的頁表結構

為了通用,Linux系統使用了三級頁表結構:頁目錄、中間頁目錄和頁表。PGD為頂級頁表,是一個pgd_t資料型別(定義在檔案linux/include/page.h中)的陣列,每個陣列元素指向一箇中間頁目錄;PMD為二級頁表,是一個pmd_t資料結構的陣列,每個陣列元素指向一個頁表;PTE則是頁表,是一個pte_t資料型別的陣列,每個元素中含有實體地址。

為了應用上的靈活,Linux使用一系列的巨集來掩蓋各種平臺的細節。使用者可以在配置檔案config中根據自己的需要對頁表進行配置,以決定是使用三級頁表還是使用二級頁表。

在系統編譯時,會根據配置檔案config中的配置,把目錄include/asm符號連線到具體CPU專用的檔案目錄中。例如,對於i386CPU,該目錄符號會連線到include/asm-i386,並在檔案pgable-2level-defs.h中定義了二級頁表的基本結構,如下圖:

其中還定義了:

  1. #define PGDIR_SHIFT 22//PGD線上性地址中的起始地址為bit22
  2. #define PTRS_PER_PGD 1024 //PGD共有1024個表項
  3. #define PTRS_PER_PTE 1024//PTE共有1024個表項
  4. #endif

在檔案include/asm-i386/pgtable.h中定義了頁目錄和頁表項的資料結構,如下:

  1. typedof struct { unsigned long pte_low; } pte_t;//頁表中的實體地址,頁框碼
  2. typedof struct { unsigned long pgd; } pgd_t;//指向一個頁表
  3. typedof struct { unsigned long pgprot; } pgprot_t;//頁表中的各個狀態資訊和訪問許可權

從定義可知,它們都是隻有一個長整型型別(32位)的結構體。

注意:如上文的“頁的保護”部分,頁框碼代表實體地址,只需要高20位就夠了(因為頁框的長度為4KB,因此頁內偏移12位)。而後12位可以存放各個狀態資訊和訪問許可權。但是Linux並沒有這樣做,反而重新定義了一個結構體來存放,通過“或”運算來將兩者結合。