操作系統虛擬內存發展史


混沌歲月

開天辟地之初,早期的內存並沒有什么復雜的抽象,物理內存簡單粗暴。

image-20210820115136129

想要寫什么?給,物理地址給你,隨便搞。這樣的操作系統並沒有擔負起它該有的責任,反而更像一個函數庫,給了你一些系統調用之類的函數,開發人員很自由。

過了一段時間,多程序時代來臨。怎么讓這些程序有條不紊地運行,成為了一個必須考慮的問題。例如是給一個程序所有地址空間還是一部分,地址空間如何分配,如果有程序不小心或惡意訪問、修改其他程序該怎么辦?操作系統,起來干活了!

CPU利用了時分共享來應對多程序時代,每個程序跑一段時間,自動讓出CPU,或者時鍾中斷之后CPU切換到操作系統,操作系統來調用程序,應用一系列策略來決定下一個時間片誰使用CPU。

問題到了內存這里,內存又將采用何種策略應對新的時代?

左右橫跳

一種方法是輪到哪個進程就把全部的內存都給它,結束之后將所有內存,寄存器全部保存到硬盤,然后把內存給下一個程序使用,問題在於,磁盤很慢,以前的機械硬盤更慢。

image-20210820113351579

內存和機械硬盤差了五個數量級,在內存和磁盤之間橫跳太慢了。

各自為王

與上一種方法作對比,很明顯,也可以一個進程只占用部分內存,每個進程可以在各自內存區間里進行活動。

image-20210820115136129

目前,我們的問題就到了程序這里了,不是占用整個內存了,而是由操作系統給你分配內存區間。那寫程序地址怎么辦?如上圖的進程C,如果C妄圖攻打進程B的地址區間怎么辦?CPU提供了兩個寄存器來解決,基址寄存器和界限寄存器。基址寄存器負責提供進程從哪開始,界限寄存器負責監督進程超界了沒有,超界了由CPU進行對應的處理,一般是直接掛掉它。上圖中,程序只需要當做自己的64KB地址是從0開始的就好,地址轉換由硬件進行。

上面的硬件地址轉換看起來不錯,可是仍舊有個問題:64KB對於有的進程太大了怎么辦?豈不是造成了內存浪費,內存可是十分昂貴的,經不起浪費啊!

64KB對於現在的進程來說太小了,那是因為現在內存相對之前便宜了,我們都被慣壞了,一張圖片都可以幾兆。鼎鼎大名的《超級馬里奧》才40KB。

進程及其地址空間如下:

image-20210820115136129

堆和棧之間可能存在大量的間隙沒有用到,但是在物理內存里卻占用了位置,這種浪費稱為內存碎片。所以我們需要更復雜的機制,來更好地利用內存。

拆分王權

典型地址空間有三個邏輯不同的段:代碼,堆,棧。既然堆、棧之間的未分配區域造成了內存碎片,那就更細化一點,不是將進程地址整個映射到物理地址,而是將不同的段分別映射到物理地址,當然,MMU(memory management unit)也就需要不止一對基址和界限寄存器。

那么,硬件地址轉換如何知道引用了哪個段呢?可以在虛擬地址里面加兩個標志位來表明;也可以通過地址產生方式來確定段,如是指令獲取,或者棧指針,或者其他(堆)。

一個分配的例子如下:

image-20210820115136129

這里面,棧的硬件地址轉換有所區別,因為它是反向增長。

為了節省內存(任何時候都是有必要的,內存在增加,但是應用的內存占用也在增加),出現了代碼共享,這也就需要硬件提供保護位,來標識程序是否能都讀寫或者執行,同時不破壞隔離的思想。

操作系統在任務切換時需要負責保存各個寄存器的內容,新的地址空間被創建時,操作系統需要在物理內存中為它的段找到空間。碎片問題依然存在,這里稱為外部碎片。

image-20210820115136129

如果要分配一個20KB的段,左圖明明有空間,卻無法分配。

一種方法是:CPU負責整理,將他們的數據復制到連續內存區域,改變段寄存器值,如右圖。但是這個操作成本很高。

更簡單的方法就是通過空閑列表管理算法實現,嘗試保留更大的內存塊。具體的算法很多,但都無法完全消除外部碎片。

管理的煩惱

就像去餐館吃飯,每個四人桌都坐了一個人,來了四個人一起吃飯,一看空位幾十個,但是就是沒有空閑的四人桌,啪的一下心情就不愉悅了。

所以這一節的主題就是如何管理空閑空間。

棧是自動管理的,壓進彈出不需要我們操心,我們要關心的是堆。在堆上管理空閑空間的數據結構就是上文提到的空閑列表。

image-20210820115136129

會堆空間的申請釋放都會體現在空閑列表上。當上圖used釋放時,列表會表現為三段空閑的區域,這個時候很自然的一個策略是合並相鄰空閑塊。記住,我們的總體目標是要有盡可能大的連續空閑區域。

分配策略

內存的分配釋放是任意的,內存可能會被搞得稀碎,理想的分配程序應該保證快速和碎片最小化。一個內存分配請求來了,該如何分配內存?

  • 最優分配:遍歷整個空閑鏈表,找到符合要求的地址區間最小的。避免了空間浪費,但是性能差。
  • 首次匹配:遍歷,找到了符合條件的地址區間就結束。空閑列表開頭會被分裂成很多小塊。
  • 下次匹配:比首次匹配多維護一個指針,指向上一次查找結束的位置,就是為了避免首次匹配空閑列表開頭頻繁分割的問題。

一些有趣的方式

  • 分離空閑列表

    對於應用程序頻繁申請的一種或幾種大小的內存空間,用一個獨立的列表來管理,其他的給通用內存分配程序。

  • 伙伴系統

    image-20210820115136129

    這個是為了合並空閑內存更加簡單,思路就是將空閑空間一分為二,直到找到小的不能再小的地址區間,返回給用戶。合並的時候只要查看相鄰區間,就可以直到是否可以合並。

沒有規矩,不成方圓

分段將空間切成不同大小的分片之后,空間會碎片化,再想合回來就難了。一種對應的解決方法是將空間分割成固定長度的分片,稱為分頁

image-20210820115136129

操作系統為每個進程保存一個數據結構,來記錄地址空間虛擬頁在物理內存的位置,稱為頁表。主要用來進行地址轉換。

上圖的例子中,地址空間為64字節,所以虛擬地址需要6位。頁大小為16字節,所以頁面號要兩位,偏移量4位。

進行地址轉換的時候就根據頁面號和偏移量查找頁表項,找到期望的物理幀號(PFN)。

如x86頁表項如下:

image-20210820115136129

20位的物理幀號,4KB的頁面,正好32位。頁表項后面幾位則是標志位,如讀寫為R/W,訪問位A等。

頁表放在哪里?當然是放在內存里,那樣每一個內存引用都要執行一個額外的內存引用來從頁表獲取地址轉換。時間翻倍!而且頁表內存占用很大,算一筆賬,以上面的x86為例,一個頁表項32位,4個字節。一個頁幀4KB,那樣就有2的20次方個頁幀,一個頁幀對應一個頁表項,也就是2的24次方字節,就是4MB。系統后台可能有上百個進程,光是頁表就占了400MB以上,這河狸嗎?

image-20210820115136129

問題擺在這,后面討論如何解決。

撥雲見日

使用分頁作為核心機制來實現虛擬內存,性能開銷較大,那如何加速地址轉換呢?

軟件上想不通的,就要由硬件來提供幫助,而且很多時候硬件的一些略微的改變,會帶來巨大的性能提升。這里就是要增加地址轉換旁路緩沖存儲器(translation-lookaside buffer, TLB),或者稱為地址轉換緩存。還記的上面的訪問速度圖嗎,cache超快的。每次內存訪問,先看一下TLB,如果有就很快完成轉換,不再訪問頁表,沒有就需要去查頁表了。redis和傳統數據庫的組合不就是很像這個做法嗎?

初學編程的時候,老師告訴你,內存最好是連續訪問,例如二維數組,你應該a[0][0],a[0][1],a[0][2]這樣遍歷,而不是a[0][0],a[1][0],a[2][0]這樣去遍歷,現在你知道原因了嗎?就是和地址轉換緩存的命中率有關。

典型的TLB有32項、64項或者128項。硬件會並行查找期望的轉換映射。

同樣,頁表,TLB都只是對一個進程有效,上下文切換時會進行切換。一種方法是簡單清空TLB,如有效位置0。如果操作系統頻繁切換進程,這種每次進程運行都會觸發TLB未命中。

或者增加硬件支持,增加進程標識符。這樣就不用了清零了。那新緩存來了,替換哪一個呢?

一種常見的策略是替換最少使用的項(LRU),另一種典型策略就是隨機替換,來防止極端情況。

速度問題有了解決辦法,那空間問題呢?4MB的問題還是存在啊!

之前我們默認說的都是線性頁表,一種方法是采用更大的頁。很明顯,頁更大了,頁表里面的項就更少了,內存占用也就少了。但是頁太大會造成內部碎片,空間浪費。

古語有言:取其精華去其糟粕。結合分頁和分段機制也是一種方法。這種方法就是拋去了堆和棧之間的空閑區域,不再映射物理內存,也沒有頁表,來節省空間。

數據結構的強大之處來了。線性列表解決不了,換個結構試試,出現了多級頁表,類似樹的結構。

image-20210820115136129

多級頁表增加了頁目錄的概念,用來標記一個頁是否是無效頁,無效就沒有頁表項。成本就是TLB未命中時需要從內存加載兩次,一次頁目錄,一次PTE。

頁目錄還是太大怎么辦?再加一層頁目錄。。。

更極端一點,有反向頁表。他就是一個頁表記錄所有進程的地址映射,無效的不記錄,頁表項里標識哪個進程或哪些進程在用這個地址映射。線性查找自然不現實,這種做法的查找應該是建立散列表來加速查找。

之前說過一句:頁表放在哪?當然是內存。這句話也不絕對,對於大部分內存都不絕對,內存不夠用時,操作系統會將一部分不常用的內存放入磁盤,稱為交換空間。

塵埃落地

至此,虛擬內存的大致脈絡已經清晰,它是軟硬件,數據結構,算法的結合,是空間和時間的權衡。

本文同步發布於orzlinux.cn


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM