作者:Yngz_Miao
來源:CSDN
原文:https://blog.csdn.net/qq_38410730/article/details/81036768
一、概要(頁面的加載):
當處理器試圖訪問一個虛存頁面時,首先到頁表中去查詢該頁是否已映射到物理頁框中,並記錄在頁表中。如果在,則MMU會把頁碼轉換成頁框碼,並加上虛擬地址提供的頁內偏移量形成物理地址后去訪問物理內存;如果不在,則意味着該虛存頁面還沒有被載入內存,這時MMU就會通知操作系統:發生了一個頁面訪問錯誤(頁面錯誤),接下來系統會啟動所謂的“請頁”機制,即調用相應的系統操作函數,判斷該虛擬地址是否為有效地址。
如果是有效的地址,就從虛擬內存中將該地址指向的頁面讀入到內存中的一個空閑頁框中,並在頁表中添加上相對應的表項,最后處理器將從發生頁面錯誤的地方重新開始運行;如果是無效的地址,則表明進程在試圖訪問一個不存在的虛擬地址,此時操作系統將終止此次訪問。
當然,也存在這樣的情況:在請頁成功之后,內存中已沒有空閑物理頁框了。這是,系統必須啟動所謂地“交換”機制,即調用相應的內核操作函數,在物理頁框中尋找一個當前不再使用或者近期可能不會用到的頁面所占據的頁框。找到后,就把其中的頁移出,以裝載新的頁面。對移出頁面根據兩種情況來處理:如果該頁未被修改過,則刪除它;如果該頁曾經被修改過,則系統必須將該頁寫回輔存。
二、Linux的虛擬內存詳解(MMU、頁表結構)
虛擬內存
為了運行比實際物理內存容量還要大的程序,包括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會把頁碼轉換成頁框碼,並加上虛擬地址提供的頁內偏移量形成物理地址后去訪問物理內存;如果不在,則意味着該虛存頁面還沒有被載入內存,這時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中定義了二級頁表的基本結構,如下圖:
其中還定義了:
#define PGDIR_SHIFT 22 //PGD在線性地址中的起始地址為bit22 #define PTRS_PER_PGD 1024 //PGD共有1024個表項 #define PTRS_PER_PTE 1024 //PTE共有1024個表項 #endif
在文件include/asm-i386/pgtable.h中定義了頁目錄和頁表項的數據結構,如下:
typedof struct { unsigned long pte_low; } pte_t; //頁表中的物理地址,頁框碼 typedof struct { unsigned long pgd; } pgd_t; //指向一個頁表 typedof struct { unsigned long pgprot; } pgprot_t; //頁表中的各個狀態信息和訪問權限
從定義可知,它們都是只有一個長整型類型(32位)的結構體。
注意:如上文的“頁的保護”部分,頁框碼代表物理地址,只需要高20位就夠了(因為頁框的長度為4KB,因此頁內偏移12位)。而后12位可以存放各個狀態信息和訪問權限。但是Linux並沒有這樣做,反而重新定義了一個結構體來存放,通過“或”運算來將兩者結合。