什么是内存(二):虚拟内存
通過上一篇文章的扯淡,我們應該已經明白了存儲器的層次結構,技術細節很復雜,但是思想卻不難理解,因為就是很簡單的緩存思想。那么本文我們開始討論關于內存的另一個話題.虛擬內存。其實思想也是很容易理解的。
我不知道有多少人聽過虛擬內存這個概念,但是虛擬內存是計算機系統最重要的概念之一,并且它成功的主要原因就是它一直在沉默的,自動的工作,換句話說,我們這些做應用的程序員根本不需要干涉它的工作過程,但是一個沒追求的碼農不是好的搬磚民工,所以作為一個有理想有抱負的程序員,我們還是要去理解虛擬內存,甚至可以這樣說,如果不理解虛擬內存,你根本不可能理解程序的深層次運行原理。也不可能去理解匯編器,鏈接器,加載器,共享對象,文件和進程等概念。
上篇文章中提出了幾個讓大家思考的問題:
- 不管什么程序,最后的直接/間接的編譯結果都是0和1,(我們直接理解為匯編)。(這點不知道的,歡迎閱讀我的另一篇文章關于跨平臺的一些認識),比如這句匯編代碼:mov eax,0x123456;它的意思是將內存0x123456處的內容送往eax這個寄存器。各個應用的數據共同存在內存中的。假設有一個音樂播放器應用的匯編代碼中,引用了0x123456這個內存地址。但是同時運行的應用有很多,那其他應用也完全有可能引用 0x123456這個地址。那為什么竟然沒起沖突和錯誤呢?
- 進程是計算機領域最重要的概念之一,什么是進程?進程是關于某次數據集合的一次運行活動, 是運行在它自己地址空間的一段自包容程序, 解釋的通俗的點, 一個程序在運行時,我們會得到一個假象,該進程好像是獨占地使用CPU和內存,CPU是沒有間斷地一條接一條的執行該程序的指令,所有的內存空間都是供該進程的代碼和數據分配使用的。(這點不嚴謹,其實內存還有一部分要分給內核kernel)。說起來,這個程序就好像得到了全世界一樣。,CPU是我的,內存也全部我的,妹子們還是我的。當然這是假象而已。但是這些假象又是怎么做到的呢?
- 程序中都會引用庫API,比如每個C程序都要引用stdio.h庫的printf(),在程序運行時,庫代碼也要被加入到內存,這么多程序都引用了這個庫,難道我內存中需要加很多份嗎?這自然不可能,那么庫代碼又是怎么被所有進程共享的呢?
這些讓我們細思恐極的疑問,都將通過這篇文章來給大家解答。
物理和虛擬尋址
在訪問者看來,主存就是一個有M個字節大小的單元組成的數組,每字節都有一個唯一的物理地址(Physical Address, PA)。 它的訪問地址和數組一樣,第一個地址為0,后面地址依次為1,2,3-----M-2, M-1;這叫做線性地址空間。這種自然的訪問內存的方式我們稱之為物理尋址(physical addressing)。
注意:在訪問內存時,對于任意一個地址,(不管是第0個還是第M-1個),訪問該地址的時間總是相同的。
在各種數據結構中,我們都說hash表是最快的,比紅黑樹之類的都要快,那hash表為什么最快?那是因為hash表內部本質上是使用了數組。所以還是數組最快,那數組為什么最快?這是因為我們知道數組的起始地址以及某個元素的序號,就可以得到該元素在內存中的地址,而對于內存,訪問任意一個地址,訪問時間總是相同的。而類似鏈表,樹等結構,卻只能靠遍歷了。(不過好的hash算法還是很難設計的,這是另外一個話題了)。
圖10:一個使用物理尋址的系統
上圖是一個物理尋址的示例,這是一條加載指令,它讀取從物理地址4開始的4個字節,CPU通過內存總線,將指令和地址傳遞給主存,主存讀取從物理地址4處開始的4個字節,返回給CPU。
因為這篇文章主要討論 虛擬內存,是關于L4級主存和磁盤之間的交互問題,為行文方便,文章中有時候直接說內存代指主存。所以這些不要誤以為是指L1,L2之類的緩存。如果看不懂這段話啥意思,務必看看我的上一篇文章什么是內存(一):存儲器層次結構,然后再來看這篇文章。
早期計算機使用物理尋址方式,但是到了現在的多任務計算機時代,普遍使用的是虛擬尋址(virtual addressing)。如下圖所示:
圖11:一個使用虛擬尋址的系統
CPU 通過一個虛擬地址(virtual address,VA)來訪問主存,這個虛擬地址在被送到主存之前會先轉換成一個物理地址。將虛擬地址轉換成物理地址的任務叫做地址翻譯(address translation)。
地址翻譯需要 CPU 硬件和操作系統之間的配合。 CPU 芯片上叫做內存管理單元(Menory Management Unit, MMU)的專用硬件,利用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容由操作系統管理。
有少數現代計算機系統依舊在使用物理尋址方式,比如DSP,嵌入式系統,超級計算機系統。這些系統的主要任務是執行單一任務,不像通用性計算機那樣需要執行多任務。可以想象到,物理尋址方式更快。這個道理和關于跨平臺的一些認識文章中,理論上java比C++慢的道理是一樣的。
前面解釋完虛擬地址,那么關于文章開頭時提的那些疑問,可能有些人心里面都有數了。因為那些地址都是虛擬地址,并非真實的物理內存當中的地址。基本思想已經懂了,那么剩下的我們就更具體的討論細節。
進程地址空間
圖12:進程地址空間
上圖是一個64位的進程地址空間,編譯器在編譯程序時,將結果編譯成32/64位的地址空間。虛擬尋址方式簡化了編譯器,鏈接器的工作。同樣也因為虛擬內存,每個進程才能有很大的,一致的,私有的的地址空間。這方便了內存管理,保護了每個進程的地址空間不被其他進程破壞。同時也方便了共享庫。
虛擬內存也是一種緩存思想
虛擬內存將主存看成是一個磁盤的高速緩存,主存中只保存活動區域,并根據需要在磁盤和主存之間來回傳送數據。
從概念上來說,虛擬內存被組織成為一個由存放在磁盤上的 N 個連續的字節大小的單元組成的數組,也就是字節數組。每個字節都有一個唯一的虛擬地址作為數組的索引。虛擬內存的地址和磁盤的地址之間建立影射關系。磁盤上活動的數組內容被緩存在主存中。在存儲器層次結構中,磁盤(較低層L5,參見我們上篇文章圖4)的數據被分割成塊(block),這些塊作為和主存(較高層,L4)之間的傳輸單元。主存作為虛擬內存(或者說磁盤)的緩存。
虛擬內存(VM)系統將虛擬內存分割成稱為大小固定的虛擬頁(Virtual Page,VP),每個虛擬頁的大小為固定字節。同樣的,物理內存被分割為物理頁(Physical Page,PP),大小也為固定字節(物理頁也稱作頁幀,page frame)。
在任意時刻,虛擬頁面都分為三個不相交的部分:
- 未分配的(Unallocated):VM 系統還未分配(或者創建)的頁,未分配的頁沒有任何數據和它們關聯,因此不占用任何內存/磁盤空間。
- 緩存的(Cached):當前已緩存在物理內存中的已分配頁。
- 未緩存的(UnCached):該頁已經映射到磁盤上了,但是還沒緩存在物理內存中。
其中未分配的VP不占用任何的實際物理空間,這點要理解。32位程序地址空間就有4G,至于64G的程序它的地址空間是一個非常大的天文數字(貌似是16777216T),而目前我們的電腦高配的也就2T磁盤,16G內存。如果64位程序每個VP都映射著實際的PP。無論如何也對應不上的。并且也完全沒必要一一映射,"圖12:進程地址空間"中可以看到,地址空間內有大量的空白。畢竟程序不可能實際使用那么大的地址空間。
圖13:VM使用主存來作為緩存
上圖展示了在一個有 8 個頁面的虛擬內存中,虛擬頁 0 和 3 還沒有被分配,所以在磁盤上不存在。虛擬頁 1,4,6 被緩存在物理內存中。虛擬頁 2,5,7 已經被映射分配了,但是還沒有緩存在主存中。
當然,那個圖上標注的不對,VP 部分, n-p和N-1應該分別標注為3和7,不過我們找不到更合適的圖了,(這種圖自己畫壓力太大了)。所以大家知道我們假設共有8個VP就好了。
頁表(page table)
系統必須得有辦法判定某個虛擬頁是否緩存在主存的某個地方。這具體可分為兩種情況。
- 已經在主存中,就需要判斷出該虛擬頁存在于哪個物理頁中。
- 不在主存中,那么系統必須判斷虛擬頁存放在磁盤的哪個位置,并且在物理主存中選擇一個犧牲頁,并將該虛擬頁從磁盤復制到 主存,替換這個犧牲頁。
這些功能由軟硬件聯合提供,包括操作系統,CPU中的內存管理單元(Memory Management Unit,MMU)和一個存放在物理內存中叫頁表(page table)的數據結構,頁表將虛擬頁映射到物理頁。每次地址翻譯硬件將一個虛擬地址轉換成物理地址時都會讀取頁表。
圖14:頁表
上圖展示了一個頁表的基本結構,頁表就是一個頁表條目(Page Table Entry,PTE)的數組。虛擬地址的每個頁在頁表中都有一個對應的PTE。在這里我們假設每個 PTE 是由一個有效位(Valid bit)和一個 n 位地址字段組成的。有效位表明了該虛擬頁當前是否被緩存在 主存 中。
- 有效位為 1,則主存緩存了該虛擬頁。地址字段就表示主存中相應的物理頁的起始位置。
- 有效位為 0,則地址字段的null表示這個虛擬頁還未被分配,否則該地址就指向該虛擬頁在磁盤上的起始位置。
頁命中與缺頁
我們在上篇文章什么是內存(一):存儲器層次結構中說過緩存命中與不命中的問題,都是緩存思想,在這里肯定也會存在同樣的問題。并且磁盤與主存之間的緩存不命中代價肯定大的多。因為L0-L4之間,每級緩存的速度大約相差10倍左右,但是L4主存與L5磁盤之間,它們的速度相差約十萬倍。所以主存與磁盤之間交換的頁容量是最大的,盡可能的增加命中率。相應的替換策略,操作系統也使用了更加復雜精密的算法。
在上篇文章什么是內存(一):存儲器層次結構,每次替換的區域,我們用了塊(block),而這里我們卻在說頁(page), 其實同一個意思。只是因為歷史原因,叫法不同罷了。
當CPU想要讀取包含在某個虛擬頁的內容時,如果該頁已經緩存在主存中,也就是頁命中。perfect,很完美。但是如果該頁沒有緩存在主存中,則我們稱之為缺頁(page fault)
圖15:對VP3中的字的應用會引起不命中
如上圖所示,CPU 引用了 VP3 中的內容, VP3 并未緩存在主存中。系統從內存中讀取 PTE3,得知 VP3 未被緩存,這會觸發了一個缺頁異常。缺頁異常會調用kernel的缺頁異常處理程序,該程序會選擇一個犧牲頁。如下圖所示,犧牲頁選擇了存放在 PP3 中的 VP4。
圖16:VP4被犧牲了
此時如果 VP4 的內容被修改了,kernel會將它復制回磁盤。接下來,kernel從磁盤賦值 VP3 到內存中的 PP3并更新 PTE3。隨后返回用戶進程。當異常處理程序返回時,它會重啟執行導致缺頁的指令,當重新執行這條指令時,因為 VP3 已經在主存中了,此時就是頁命中了。
圖17:VP3被緩存到PP3
根據習慣性的叫法,我們在磁盤和內存之間傳送頁的活動叫做交換(swapping)或者頁面調度(paging)。這種交換活動,只有當不命中發生時才會發生,(也就說,系統并不會將磁盤內容預存到內存中)。這種策略被稱之為按需頁面調度(demand paging)。
我們剛才說,缺頁錯誤是一種異常,但是實際上,在計算機系統中,被0除,讀寫文件,還有上篇文章中我們所說的中斷(interrupt),甚至包括我們代碼中寫的try catch,都是一種異常。 比如被0除是intel 的CPU規定的的第0號故障(fault)類型的異常。而讀寫文件,分別是linux規定的第0號和第1號陷阱(trap)類型的異常。多任務的上下文切換,進程的創建回收等,等與系統中這種異常流的處理密切相關。當然,這是另外一個話題了。我們在這里不做累述。
虛擬內存作為內存管理和內存保護的工具
理所當然的,每個進程都有一個獨立的頁表和一個獨立的虛擬地址空間
回到文章開頭的問題,比如每個C程序都要調用的 stdio這個庫,不可能為每個進程都添加一份庫,內存中只有一份stdio庫的內容,供每個使用該庫的進程共享。
圖18:共享頁面
如上圖所示: 第一個進程的的頁表將 VP2 映射到 某個物理頁面。而第二個進程同樣將它的 VP2 映射到 該物理頁面。所以該物理頁面都被兩個進程共享了。
此時,大家再看一下"圖:12 進程地址空間",就會發現在地址空間當中,"共享庫的內存映射區域"對于每個進程起始地址都是相同的。再想想進程之間共享內存的通信方式, 所以說虛擬內存簡化了共享機制
大家知道,C語言中存在指針,可以直接進行內存操作。因為有了虛擬內存,所以我們的指針操作也不會訪問到其他進程的區域,但是哪怕是對于自己的地址空間,很多內存區域也應該是禁止訪問的,這不僅包括kernel的區域,也包括自己的只讀代碼段。那么虛擬內存就提供了這樣的一種內存保護工具。
地址翻譯機制可以使用一種自然的方式來提供內存的訪問控制。PTE 上添加一些額外的控制位來添加權限。每次 CPU 生成一個地址時,地址翻譯硬件都會讀一個 PTE 。
圖19:虛擬內存提供內存保護
在上圖中,每個 PTE 額外添加了三個控制位, SUP 位表示進程是否必須運行內核模式,READ和WRITE位分別控制頁面的讀寫權限。如果有指令違反了這些控制權限,那么 CPU 會觸發一個故障,并將控制傳遞給內核中的異常處理程序。該種異常一般稱為段錯誤(segmentation fault)。
段 和 頁
我們明白了頁,頁是操作系統為了管理主存方便而劃分的,對用戶不可見。但是思考這種情況,假設一個頁的大小是1M。但是某個程序數據加起來也就0.5M,所以在內存和磁盤進行頁交換明顯的浪費內存了。所以還一種劃分方式是分段。上面那個例子,我將該段劃分為0.5M,在內存和磁盤之間交換,這樣就避免了浪費。
段是信息的邏輯單元,是根據用戶需求而靈活劃分的,所以大小不固定,對用戶是可見的,提供的是二維地址空間。
對于段,我沒找到比較好的資料,所以也沒有理解的更清楚,網上的很多文章都相互抄襲。據我所了解,匯編程序員是可以直接操作段的,但是我們寫高級語言的程序員有相應的API能進行段操作嗎?所以對于段的相關知識,真心不了解,也希望了解的同學可以在留言區指點批評,或者留言相關的文章鏈接。我回頭會再補充這篇博客。謝謝
swap分區的作用
熟悉linux的同學,應該知道linux有一個swap分區。Swap空間的作用可簡單描述為:當系統的物理內存不夠用的時候,就需要將物理內存中的一部分空間釋放出來,以供當前運行的程序使用。那些被釋放的空間可能來自一些很長時間沒有什么操作的程序,這些被釋放的空間中的信息被臨時保存到Swap空間中,等到那些程序要運行時,再從Swap中恢復保存的數據到內存中。系統總是在物理內存不夠時,才進行Swap交換。
你電腦打開了一個音樂播放器,但是也沒播放歌曲,然后你幾天不關機,也一直沒關閉這個音樂播放器,隨著運行的程序越來越多,內存快不夠用了,所以操作系統就選擇將這個音樂播放器的內存狀態(包括堆棧狀態等)都寫到磁盤上的swap區進行保存。這樣就騰出來一部分內存供其他需要運行的程序使用。你啥時候想聽歌了,就找到了這個音樂播放器程序操作。此時, 系統會從磁盤中的swap區重新讀取該音樂播放器的相關信息,送回內存接著運行。
在window下也有類作用的硬盤空間,屬于對用戶不可見的匿名磁盤空間(在C盤)。
特別注意:按照字面意思,swap交換區也可以稱為虛擬內存
硬盤上的swap交換區,其實就相當于承擔了內存的作用(只是速度很慢罷了)。swap交換區起到了擴大內存的作用。所以從某些意義上來講,swap區也可以叫做虛擬內存,但是這個虛擬內存是字面意思。和我們本文當中站在計算機系統的角度來解釋的虛擬內存不是一個概念。所以特別注意這一點。因為有些人理解的虛擬內存,就是swap交互區。此虛擬內存非彼虛擬內存,所以明白各自的概念和作用。不然和其他人討論虛擬內存,可能出現驢頭不對馬嘴的情況。
linux環境下叫做swap分區,window下這塊區域沒叫做swap分區,就直接按照字面意思叫做"虛擬內存"了。所以兩個含義不同的虛擬內存,讀者一定要搞清楚了。
百度百科上對虛擬內存的解釋非常混亂
關于虛擬內存,看了百度百科的內容,有些地方解釋的比較混亂,有些地方是對的,但是有些地方解釋的是關于swap分區的內容。如果光從字面意思來看,swap交換區的確可以稱為虛擬內存,但是此虛擬內存非彼虛擬內存。百度百科關于這點的介紹比較混亂,百度百科的內容比較多,但是沒分清這一點,只會越來越混亂。我又查了維基百科的內容,該詞條內容不長,但是下面這段話很重要。
注意:虛擬內存不只是“用磁盤空間來擴展物理內存”的意思——這只是擴充內存級別以使其包含硬盤驅動器而已。把內存擴展到磁盤只是使用虛擬內存技術的一個結果,它的作用也可以通過覆蓋或者把處于不活動狀態的程序以及它們的數據全部交換到磁盤上等方式來實現。對虛擬內存的定義是基于對地址空間的重定義的,即把地址空間定義為“連續的虛擬內存地址”,以借此“欺騙”程序,使它們以為自己正在使用一大塊的“連續”地址。
所以我認為百度百科的解釋是混亂的,而維基百科上的應該才是正確的。
兩篇關于內存的文章都寫完了。因為本人才疏學淺,若有理解錯誤或解釋不清楚的地方,希望各位讀者打臉批評。
作者: www.yaoxiaowen.com
博客地址: www.cnblogs.com/yaoxiaowen/
github: https://github.com/yaowen369
歡迎對于本人的博客內容批評指點,如果問題,可評論或郵件(yaowen369@gmail.com)聯系
歡迎轉載,轉載請注明出處.謝謝
轉載于:https://www.cnblogs.com/yaoxiaowen/p/7805964.html
總結
以上是生活随笔為你收集整理的什么是内存(二):虚拟内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言——第六周作业
- 下一篇: Python 虚拟环境:Virtuale