什麼是記憶體(二):虛擬記憶體
通過上一篇文章的扯淡,我們應該已經明白了儲存器的層次結構,技術細節很複雜,但是思想卻不難理解,因為就是很簡單的快取思想。那麼本文我們開始討論關於記憶體的另一個話題.虛擬記憶體。其實思想也是很容易理解的。
我不知道有多少人聽過虛擬記憶體這個概念,但是虛擬記憶體是計算機系統最重要的概念之一,並且它成功的主要原因就是它一直在沉默的,自動的工作,換句話說,我們這些做應用的程式設計師根本不需要干涉它的工作過程,但是一個沒追求的碼農不是好的搬磚民工,所以作為一個有理想有抱負的程式設計師,我們還是要去理解虛擬記憶體,甚至可以這樣說,如果不理解虛擬記憶體,你根本不可能理解程式的深層次執行原理。也不可能去理解彙編器,連結器,載入器,共享物件,檔案和程序等概念。
上篇文章中提出了幾個讓大家思考的問題:
- 不管什麼程式,最後的直接/間接的編譯結果都是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
注意:在訪問記憶體時,對於任意一個地址,(不管是第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交換區的確可以稱為虛擬記憶體,但是此虛擬記憶體非彼虛擬記憶體。百度百科關於這點的介紹比較混亂,百度百科的內容比較多,但是沒分清這一點,只會越來越混亂。我又查了維基百科的內容,該詞條內容不長,但是下面這段話很重要。
注意:虛擬記憶體不只是“用磁碟空間來擴充套件實體記憶體”的意思——這只是擴充記憶體級別以使其包含硬碟驅動器而已。把記憶體擴充套件到磁碟只是使用虛擬記憶體技術的一個結果,它的作用也可以通過覆蓋或者把處於不活動狀態的程式以及它們的資料全部交換到磁碟上等方式來實現。對虛擬記憶體的定義是基於對地址空間的重定義的,即把地址空間定義為“連續的虛擬記憶體地址”,以藉此“欺騙”程式,使它們以為自己正在使用一大塊的“連續”地址。
所以我認為百度百科的解釋是混亂的,而維基百科上的應該才是正確的。
兩篇關於記憶體的文章都寫完了。因為本人才疏學淺,若有理解錯誤或解釋不清楚的地方,希望各位讀者打臉批評。