關於虛擬內存這部分的內容,我沒有選擇課本來進行整理,課本在這一塊探究的並不是很深,所以打算從《深入理解計算機系統》的第九章來進行整理,內容僅是一些基礎理論,沒有涉及到案例的探究。
參考整理自https://www.jianshu.com/p/e1b82b230917
了更加有效地管理內存並且少出錯,現代系統提供了一種對主存的抽象概念,叫做虛擬內存(VM)。
虛擬內存提供了三個重要的能力:
(1)它將主存看成是一個存儲在磁盤上的地址空間的告訴緩存,在主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據,通過這種方式,它高效地使用了主存
(2)它為每個進程提供了一致的地址空間,從而簡化了內存管理
(3)它保護了每個進程的地址空間不被其它進程破壞
虛擬內存在工作中,不需要程序員做任何干涉,但是程序員還是需要理解它,原因如下:
(1)虛擬內存是核心的
虛擬內存遍及計算機系統的所有層面,在硬件異常、匯編器、鏈接器、加載器、共享對象、文件和進程的設計中扮演着重要角色。理解虛擬內存將幫助我們更好地理解系統通常是如何工作的。
(2)虛擬內存是強大的
虛擬內存給予應用程序強大的能力,可以創建和小灰內存片、將內存片映射到磁盤文件的某個部分,以及與其他進程共享內存。比如,你知道可以通過讀寫內存位置讀或者修改一個磁盤文件的內容嗎?或者可以加載一個文件到內存中,而不需要進行任何顯示地復制嗎?理解虛擬內存將幫助你利用它的強大功能在應用程序中添加動力
(3)虛擬內存是危險的
每次應用功能程序引用一個變量、間接引用一個指針,或者調用一個諸如 malloc 這樣的動態分配程序時,它就會和虛擬內存發生交互。如果虛擬內存使用不當,應用將遇到復雜危險的與內存有關的錯誤。例如,一個帶有錯誤指針的程序可以立即崩潰於“段錯誤”或者“保護錯誤”,它可能在崩潰之前還默默地運行了幾個小時,或者最令人驚慌地,運行完成卻產生不正確的結果。理解虛擬內存以及諸如 malloc 之類的管理虛擬內存的分配程序,可以幫助你避免這些錯誤。
1.1 物理尋址和虛擬尋址
虛擬內存主要是一種地址擴展技術,主要是建立和管理兩套地址系統:物理地址和虛擬地址。由虛擬地址空間(硬盤上)裝入進程,其實際執行是在物理地址空間(內存上)承載進程的執行。虛擬地址空間比物理地址空間要大的多,操作系統同時承擔着管理者兩套地址空間的轉換。我們來看看什么是物理尋址:
主存的每個地址都是唯一的,第一個字節地址為0,接下來為2,以此類推。CPU使用這種訪問方式就是物理尋址。上圖所示就是CPU通過地址總線傳遞讀取主存中4號地址開始處的內容並通過數據總線傳送到CPU的寄存器中。
當然地址總線也不是無限大的,我們通常所說的32位系統,其尋址能力是2^32 = 4 294 967 296B(4GB)也就是說內存條插的再多也沒有用,地址總線只能最多訪問到4GB的地址內容。我們前面說過4GB的物理內存空間其實並不大(如果是獨占的話)。這時候科學家們想到了一個很好的方法,建立虛擬尋址方式,使用一個成為MMU的地址翻譯工具將虛擬地址翻譯成物理地址在提供訪問,如下圖:
使用虛擬尋址的時候,cpu先是生成一個虛擬地址:4100再經過地址翻譯器,將4100翻譯成物理地址。
我們說過虛擬地址要比物理地址大的多,為啥還要麻煩的將物理地址轉成虛擬地址呢?虛擬地址的發明究竟是為了什么,我們知道對內存的訪問要比硬盤的訪問快10000倍,如果我們在內存中沒有找到相應的內容(不命中),而需要到硬盤上找的話,我們必須要提供相對來說高效率的訪問方式。這時候就創建了一個虛擬存儲器,管理着磁盤,以每頁的方式進行整合,每個頁面的大小4kb-2mb不等,加上偏移量就成為了一個虛擬地址。比如4100,說明的就是頁4編號,偏移100處的位置。這就比挨個挨個單獨尋址要快的多。
1.2 地址空間
地址空間是一個非負整數的集合{0,1,2,……},一個32位的系統中有:2^32 = 4 294 967 296B(4GB)個有效地址。地址空間的概念很重要,我們必須要清楚數據對象(字節)和它的屬性(地址)的區別, 舉個例子:我和我老婆住在蒼溪縣xx小區7棟1單元,這個就是我的屬性:地址。另外,住在家的我和我老婆就是數據對象(字節)。虛擬存儲器的基本思想是:主存中的每個字節都有一個選自虛擬地址空間的虛擬地址和一個選自物理地址空間的物理地址。
1.3 虛擬存儲器的工作原理
我們先來看看虛擬內存,就windows系統而言是保存�在磁盤上的一個文件,存放於C盤的pagefile.sys點擊屬性可以看到其大小為3.96G,這相當於一個倉庫,保存着臨時需要又還沒用到的數據。
這里所說的虛擬內存其實應該叫做頁面文件,真正意義上的虛擬內存包含了物理內存和頁面文件的大小。

上圖所示的是一個有8個虛擬頁的小虛擬存儲器(建立在硬盤上),虛擬頁0和3還未分配,因此在磁盤上還不存在。虛擬頁1、4和6被緩存在右邊的主存中。
(內存訪問速度要比硬盤快10000倍,因此不命中的話代價要昂貴的多。我們前面說過是以虛擬頁來緩存的,也就是分成塊,每個塊(虛擬頁)的大小4kb-2mb不等。)
我們現在來看看地址翻譯MMU(內存管理單元)是如何完成虛擬地址到物理地址的轉換的,學習這個知識是幫助我們理解虛擬存儲器是如何將虛擬也緩存到主存(內存)中去的。
① 頁表
頁表是一個存放在內存中的數據結構,MMU就是通過頁表來完成虛擬地址到物理地址的轉換。這個數據結構每一個條目稱為PTE(Page Table Entry),由兩部分組成:有效位和n位地址段。有效位如果是1,那么n位地址就指向已經在內存中緩存好了的地址;如果為0,地址為null的話表示為分配,地址指向磁盤上的虛擬內存(pagefile.sys)的話就是未緩存。我們來看一個典型的頁表圖:
虛擬頁vp1,2,7,4當前被緩存在內存中,頁表上有效位設置成1,分別用PTE1,2,4,7表示。VP0和VP5(PTE0、5)未被分配,VP3和VP6被分配並指向虛擬內存,但未被緩存。
② 頁命中
當我們使用2100虛擬地址來訪問虛擬頁2的內容的時候,就是一個頁命中。地址翻譯將指向PTE2上,由於有效位1,地址翻譯器MMU就知道VP2已經緩存在內存中了。就使用頁表中保存的物理地址進行訪問。
③ 缺頁
我們再來看看不命中,也就是缺頁的情況,當CPU需要VP3的一個字時,初始化是這樣的:
PTE3有效位是0,同時地址位指向了虛擬內存(pagefile.sys),就會觸發缺頁異常。
異常處理程序會選擇犧牲一個內存(DRAM)中的頁,本例中選擇的是內存中的PP3頁的VP4,如果VP4已經被修改了,那么內核就會將它復制回磁盤。無論哪種情況,內核都會修改VP4的頁表條目,反映VP4不再緩存在內存這一事實。接下來內核就從虛擬內存中拷貝VP3到內存中的PP3,並使得PTE3指向內存中的PP3,形成如下:
(注:虛擬存儲器出現早於高速緩存,按照習慣的說法塊被叫做頁。從虛擬內存到物理內存傳送頁的活動就叫做頁面交換。)
1.4 虛擬存儲器的作用
進程i將VP1映射到了內存的PP2處,VP2映射到了內存的PP7處。進程j將VP1映射到了內存的PP7,將VP2映射到了PP10處。
簡化鏈接:獨立的地址空間允許每個進程的內存映像使用相同的基本格式。每個進程一個頁表后,這個進程就會覺得全世界都是它的(頁表模擬出一個虛擬存儲器),那什么符號鏈接的時候(也就是符號映射到地址的時候),不再會受到內存中還有其他應用程序的干擾,因為我們面向的是虛擬存儲器,我們的進程的地址空間是獨立的,我這個符號放到離0偏移100的地方,那個放到離0偏移200的地方很容易就搞定了。
簡化加載:在硬盤中雙擊一個圖標,啟動一個應用程序時,實際上你都不需要將這個程序從硬盤給加載到內存,只需要建個頁表,然后頁表里的編號指向的是硬盤,然后CPU訪問到具體代碼的時候,再按照上一節的尋址的方式,按需的將硬盤上的東東加載到內存。加載過程及其簡單了。
簡化共享:一般而言,每一個進程都有自己私有的代碼、數據、堆以及棧區域,是和其他進程不共享的。但是有些代碼,比如調用操作系統的API,這些API可能許多進程都要使用比如printf,這就要共享一部分內存,我們不需要將這部分內存在每個進程空間都拷貝一份,實際上每個進程都有一個頁表,而不是全局只有一個,頁表把共享內存映射到同一個地方。
簡化存儲器分配:當一個進程使用malloc要求額外的空間時,操作系統只需要保證形成了一個連續的虛擬頁面,但可以映射到物理內存中任意的位置,可以隨機分散在內存的不同位置。
簡化保護:我們可以通過為PTE添加額外的標識位提供對存儲器的保護。
通過新添加的三個標識位:SUP:內核or用戶;READ:讀;WRITE:寫。運行在用戶模式下的進程只允許訪問SUP為否的頁面,如果一個指令違法了訪問的設置條件,就會轉到保護故障,引起一個段錯誤。
1.5 虛擬存儲器工作原理詳解:地址翻譯
地址翻譯從形式上來說就是建立一個虛擬地址空間到物理地址空間的映射關系,我們前面說過MMU使用的是頁表來實現這種映射。CPU中有一個專門的頁表基址寄存器(PTBR)指向當前頁表,使用頁表進行翻譯的時候方法如下:
每個虛擬地址由兩部分組成:虛擬頁號(VPN)+虛擬頁偏移量(VPO),當CPU生成一個虛擬地址並傳遞給MMU開始翻譯的時候,MMU利用虛擬地址的VPN來選擇相應的PTE,同時將頁表中的物理頁號(PPN)+虛擬地址的VPO就生成了相應的物理地址。(物理地址是由頁表中的物理頁號+虛擬地址中的偏移量構成)
頁面命中是一個簡單的過程,我們就不做詳解,這里來跟蹤看一下缺頁的情況:
說明:
①CPU生成虛擬地址;
②MMU生成PTE地址從內存的頁表中請求內容;
③ 內存中的頁表返回相應的PTE值;
④ PTE的有效位是0,MMU觸發異常,轉到異常處理程序;
⑤ 異常處理程序確定內存中的犧牲頁,並將其寫會到磁盤上;
⑥缺頁處理程序頁面調入新的頁面,更新PTE。
⑦ 由於PTE已經被更新好了,從新發送虛擬地址到MMU(后面就和命中的過程一樣了)
我們講了大致的地址翻譯原理,有什么辦法能夠提高翻譯的速度嗎?
① 加入高速緩存
高速緩存被發明出來的一個重要原因就是提高對內存的訪問速度,我們來看看加入高速緩存后的訪問示意圖:

高速緩存被放在存儲器和MMU之間,可以緩存頁表條路。當MMU發送一個PTEA請求的時候,優先從高速緩存中尋找相應的PTE值,如果命中直接返回給MMU,如果不命中從內存中獲得並發送到高速緩存,再由高速緩存返回到MMU。(高速緩存使用的是物理尋址,不涉及地址保護問題,因為MMU已經加入了保護標識位)
② 加入翻譯后備緩沖器TLB
TLB是一個小的、虛擬尋址的緩存,其中每一行都保存一個PTE塊,高度相連。主要是提供虛擬地址到物理地址的翻譯速度。大致范圍示意圖如下:
說明:
①CPU生成一個虛擬地址並發送到MMU;
②③MMU從TLB中獲取相應的PTE
④翻譯成相應的物理地址后從內存中請求內容;
⑤ 數據從內存返回給CPU
當TLB不命中時,MMU必須從L1緩存中取出相應的PTE,新取出的PTE存放到TLB中,可能會覆蓋掉之前已經存在過的一個條目。

③ 加入多級頁表
我們來分析一下單級頁表的弱勢之處,然后指出改進的方法。我們雙擊圖標運行一個程序的時候,在單級頁表模式下,其實是在內存中為這個程序創建了一個頁表,使得程序有了獨立的地址空間。我們以32位系統4GB地址空間為例,我們將物理內存分割為虛擬的頁面,每個頁面保存4KB大小的內容,這樣我們總共需要1048576個頁面,才能瓜分所有的4GB空間。那么我們的頁表要能夠完成所有物理內存的映射,就必須要1048576個頁表項,由於每個頁表項占用4B的空間,那么我們這個頁表就需要占用4194304B(4M)的內存空間,每個進程都有這樣的一個4M的頁表占用着內存空間,才能完成映射。
單級頁表:
下面我們加入分級頁表(二級):
我們加入分級的思想以后,每一級的頁表就都只有4KB的大小,數量也有原來的1048576變成了1024個,兩級相乘其實表示的數量還是原來那么多。上圖所示,一級頁表每條PTE負責映射二級頁表1024個PTE項,二級頁表的每個PTE在映射虛擬存儲器中4KB大小的位置。也就是說一級頁表每條PTE負責映射一塊4M大小的空間,而一級頁表總共有1024個頁表項,也就能用來映射完成所有物理內存空間。
這樣做的好處是,如果一級頁表中有未被分配的項目,那么這條PTE直接設置成null,不指向任何二級列表,也就不再占用空間。還有一個好處是不是所有的二級列表都需要常駐內存,每個進程只需要在內存中建立一級頁表(4kb)大小,二級列表按需要的時候創建調入,這樣就更省了。只有最近經常使用的二級頁表才需要緩存到內存中。
④ 綜合:一個從虛擬地址到物理地址並獲取數據的模擬
為了方便討論,我們以一個小的存儲系統作如下假設:
1> 虛擬地址大小14位:結構如下

2> 物理地址大小12位:結構如下

3> 內存大小為4KB,物理頁號為64個,每個頁面大小為64B,頁表如下:

4> TLB 翻譯后備緩存器分成4組,每組4條,一共16個條目:

5> 高速緩存64B大小,使用物理尋址、直接映射的方式,每行4B,共計16個組:
好了,有了這些假設以后我們來看一下,當CPU讀取0x03d4處內容會發生些什么:

此處是虛擬地址,0x03d4二進制表示就為:(0000 1111 0101 00)14位,由於虛擬地址的低6位用來表示偏移量(每個頁面64B大小:2^6=64),剩下的高8位用來表示虛擬頁號,一共有128個虛擬頁號(2^8)。
我們從虛擬地址中:
1> 抽取出虛擬頁號為:0x0f;
2> 將虛擬頁號與TLB進行對比,為了方便,我們形成TLBT標記位,TLBI組索引;
組索引在0x03號位置,標記也為0x03,這時候回到我們的假設“4>”處進行檢查,發現0x03組,標記位0x03處的有效位是1,所以命中。取出物理頁號(PPN)0D用於構造物理地址用。物理地址就為:PPN-VPO = 0x354(0011 0101 0100):
3 > 根據物理地址:0x354,我們在高速緩存中去碰碰運氣,前面假設的時候我們說過大小為64B,我們將其分成16個條目,由:標記位+有效位+塊0-3組成。其實際存放數據的塊每個條目只有4個(0-3)所以總大小為64B,我們的物理地址要到高速緩存中去尋找數據,就得有某種對應方式。其中物理地址的低2位用作偏移量(CO)因為每個條目只有4個數據塊,緊接着的4位表示組索引,因為一共是16個組,最后的高7位作為標記位。我們形成如下的:CO=0x0,偏移量為0也就是塊0的內容;CI = 0x05也就是第0x05組和CT:0x0d標志位。有了這些內容以后我們返回到假設5中去尋找,發現高速緩存中的5號索引,標記位為0x0d,並且有效,讀取塊0處的內容為36。這就是我們要返回給CPU的內容。至此完成了一個端到端地址翻譯並返回數據的手工模擬,當然我們還可能遇到很多不同的情況。如在高速緩存中不命中,TLB不命中等等,但大致原理幾乎類似,請自行腦補。
