我的內(nèi)存怎么不夠用了?
- 為什么內(nèi)存不夠用?
- 交換(Swap)技術(shù)
- 虛擬內(nèi)存
- 頁(Page)和頁表
- MMU
- 頁表?xiàng)l目
- 大頁面問題
內(nèi)存是稀缺的,隨著應(yīng)用使用內(nèi)存也在膨脹。當(dāng)程序越來復(fù)雜,進(jìn)程對內(nèi)存的需求會越來越大。從安全角度考慮,進(jìn)程間使用內(nèi)存需要隔離。另外還有一些特殊場景,存在不希望 CPU 進(jìn)行緩存的場景。這個(gè)時(shí)候,有一個(gè)虛擬化層承接各種各樣的訴求,統(tǒng)一進(jìn)行處理,就會有很大的優(yōu)勢。
為什么內(nèi)存不夠用?
要理解一個(gè)技術(shù),就必須理解它為何而存在。總體來說,虛擬化技術(shù)是為了解決內(nèi)存不夠用的問題,那么內(nèi)存為何不夠用呢?
主要是因?yàn)槌绦蛟絹碓綇?fù)雜。比如說我現(xiàn)在給你錄音的機(jī)器上就有 200 個(gè)進(jìn)程,目前內(nèi)存的消耗是 21G,我的內(nèi)存是 64G 的,但是多開一些程序還是會被占滿。另外,如果一個(gè)程序需要使用大的內(nèi)存,比如 1T,是不是應(yīng)該報(bào)錯(cuò)?如果報(bào)錯(cuò),那么程序就會不好寫,程序員必須小心翼翼地處理內(nèi)存的使用,避免超過允許的內(nèi)存使用閾值。以上提到的這些都是需要解決的問題,也是虛擬化技術(shù)存在的價(jià)值和意義。
那么如何來解決這些問題呢?歷史上有過不少的解決方案,但最終沉淀下的是虛擬化技術(shù)。接下來我為你介紹一種歷史上存在過的 Swap 技術(shù)以及虛擬化技術(shù)。
交換(Swap)技術(shù)
Swap 技術(shù)允許一部分進(jìn)程使用內(nèi)存,不使用內(nèi)存的進(jìn)程數(shù)據(jù)先保存在磁盤上。注意,這里提到的數(shù)據(jù),是完整的進(jìn)程數(shù)據(jù),包括正文段(程序指令)、數(shù)據(jù)段、堆棧段等。輪到某個(gè)進(jìn)程執(zhí)行的時(shí)候,嘗試為這個(gè)進(jìn)程在內(nèi)存中找到一塊空閑的區(qū)域。如果空間不足,就考慮把沒有在執(zhí)行的進(jìn)程交換(Swap)到磁盤上,把空間騰挪出來給需要的進(jìn)程。
上圖中,內(nèi)存被拆分成多個(gè)區(qū)域。內(nèi)核作為一個(gè)程序也需要自己的內(nèi)存。另外每個(gè)進(jìn)程獨(dú)立得到一個(gè)空間——我們稱為地址空間(Address Space)。你可以認(rèn)為地址空間是一塊連續(xù)分配的內(nèi)存塊。每個(gè)進(jìn)程在不同地址空間中工作,構(gòu)成了一個(gè)原始的虛擬化技術(shù)。
比如:當(dāng)進(jìn)程 A 想訪問地址 100 的時(shí)候,實(shí)際上訪問的地址是基于地址空間本身位置(首字節(jié)地址)計(jì)算出來的。另外,當(dāng)進(jìn)程 A 執(zhí)行時(shí),CPU 中會保存它地址空間的開始位置和結(jié)束位置,當(dāng)它想訪問超過地址空間容量的地址時(shí),CPU 會檢查然后報(bào)錯(cuò)。
上圖描述的這種方法,是一種比較原始的虛擬化技術(shù),進(jìn)程使用的是基于地址空間的虛擬地址。但是這種方案有很多明顯的缺陷,比如:
- 碎片問題:上圖中我們看到進(jìn)程來回分配、回收交換,內(nèi)存之間會產(chǎn)生很多縫隙。經(jīng)過反反復(fù)復(fù)使用,內(nèi)存的情況會變得十分復(fù)雜,導(dǎo)致整體性能下降。
- 頻繁切換問題:如果進(jìn)程過多,內(nèi)存較小,會頻繁觸發(fā)交換。
首先重新 Review 下我們的設(shè)計(jì)目標(biāo)。
- 隔離:每個(gè)應(yīng)用有自己的地址空間,互不影響。
- 性能:高頻使用的數(shù)據(jù)保留在內(nèi)存中、低頻使用的數(shù)據(jù)持久化到磁盤上。
- 程序好寫(降低程序員心智負(fù)擔(dān)):讓程序員不用關(guān)心底層設(shè)施。
現(xiàn)階段,Swap 技術(shù)已經(jīng)初步解決了問題 1。
關(guān)于問題 2,Swap 技術(shù)在性能上存在著碎片、頻繁切換等明顯劣勢。
關(guān)于問題3, 使用 Swap 技術(shù),程序員需要清楚地知道自己的應(yīng)用用多少內(nèi)存,并且小心翼翼地使用內(nèi)存,避免需要重新申請,或者研發(fā)不斷擴(kuò)容的算法——這讓程序心智負(fù)擔(dān)較大。
經(jīng)過以上分析,需要更好的解決方案,就是我們接下來要學(xué)習(xí)的虛擬化技術(shù)。
虛擬內(nèi)存
虛擬化技術(shù)中,操作系統(tǒng)設(shè)計(jì)了虛擬內(nèi)存(理論上可以無限大的空間),受限于 CPU 的處理能力,通常 64bit CPU,就是 264 個(gè)地址。
虛擬化技術(shù)中,應(yīng)用使用的是虛擬內(nèi)存,操作系統(tǒng)管理虛擬內(nèi)存和真實(shí)內(nèi)存之間的映射。操作系統(tǒng)將虛擬內(nèi)存分成整齊小塊,每個(gè)小塊稱為一個(gè)頁(Page)。之所以這樣做,原因主要有以下兩個(gè)方面。
一方面應(yīng)用使用內(nèi)存是以頁為單位,整齊的頁能夠避免內(nèi)存碎片問題。
另一方面,每個(gè)應(yīng)用都有高頻使用的數(shù)據(jù)和低頻使用的數(shù)據(jù)。這樣做,操作系統(tǒng)就不必從應(yīng)用角度去思考哪個(gè)進(jìn)程是高頻的,僅需思考哪些頁被高頻使用、哪些頁被低頻使用。如果是低頻使用,就將它們保存到硬盤上;如果是高頻使用,就讓它們保留在真實(shí)內(nèi)存中。
如果一個(gè)應(yīng)用需要非常大的內(nèi)存,應(yīng)用申請的是虛擬內(nèi)存中的很多個(gè)頁,真實(shí)內(nèi)存不一定需要夠用。
頁(Page)和頁表
接下來,我們詳細(xì)討論下這個(gè)設(shè)計(jì)。操作系統(tǒng)將虛擬內(nèi)存分塊,每個(gè)小塊稱為一個(gè)頁(Page);真實(shí)內(nèi)存也需要分塊,每個(gè)小塊我們稱為一個(gè) Frame。Page 到 Frame 的映射,需要一種叫作頁表的結(jié)構(gòu)。
上圖展示了 Page、Frame 和頁表 (PageTable)三者之間的關(guān)系。Page 大小和 Frame 大小通常相等,頁表中記錄的某個(gè) Page 對應(yīng)的 Frame 編號。頁表也需要存儲空間,比如虛擬內(nèi)存大小為 10G, Page 大小是 4K,那么需要 10G/4K = 2621440 個(gè)條目。如果每個(gè)條目是 64bit,那么一共需要 20480K = 20M 頁表。操作系統(tǒng)在內(nèi)存中劃分出小塊區(qū)域給頁表,并負(fù)責(zé)維護(hù)頁表。
頁表維護(hù)了虛擬地址到真實(shí)地址的映射。每次程序使用內(nèi)存時(shí),需要把虛擬內(nèi)存地址換算成物理內(nèi)存地址,換算過程分為以下 3 個(gè)步驟:
- 通過虛擬地址計(jì)算 Page 編號;
- 查頁表,根據(jù) Page 編號,找到 Frame 編號;
- 將虛擬地址換算成物理地址。
下面我通過一個(gè)例子給你講解上面這個(gè)換算的過程:如果頁大小是 4K,假設(shè)程序要訪問地址:100,000。那么計(jì)算過程如下。
頁編號(Page Number) = 100,000/4096 = 24 余1619。24 是頁編號,1619 是地址偏移量(Offset)。
查詢頁表,得到 24 關(guān)聯(lián)的 Frame 編號(假設(shè)查到 Frame 編號 = 10)。
換算:通常 Frame 和 Page 大小相等,替換 Page Number 為 Frame Number 物理地址 = 4096 * 10 + 1619 = 42579。
MMU
上面的過程發(fā)生在 CPU 中一個(gè)小型的設(shè)備——內(nèi)存管理單元(Memory Management Unit, MMU)中。如下圖所示:
當(dāng) CPU 需要執(zhí)行一條指令時(shí),如果指令中涉及內(nèi)存讀寫操作,CPU 會把虛擬地址給 MMU,MMU 自動完成虛擬地址到真實(shí)地址的計(jì)算;然后,MMU 連接了地址總線,幫助 CPU 操作真實(shí)地址。
這樣的設(shè)計(jì),就不需要在編寫應(yīng)用程序的時(shí)候擔(dān)心虛擬地址到物理地址映射的問題。我們把全部難題都丟給了操作系統(tǒng)——操作系統(tǒng)要確定MMU 可以讀懂自己的頁表格式。所以,操作系統(tǒng)的設(shè)計(jì)者要看 MMU 的說明書完成工作。
難點(diǎn)在于不同 CPU 的 MMU 可能是不同的,因此這里會遇到很多跨平臺的問題。解決跨平臺問題不但有繁重的工作量,更需要高超的編程技巧,Unix 最初期的移植性(跨平臺)是 C 語言作者丹尼斯·里奇實(shí)現(xiàn)的。
MMU 需要查詢頁表(這是內(nèi)存操作),而 CPU 執(zhí)行一條指令通過 MMU 獲取內(nèi)存數(shù)據(jù),難道可以容忍在執(zhí)行一條指令的過程中,發(fā)生多次內(nèi)存讀取(查詢)操作?難道一次普通的讀取操作,還要附加幾次查詢頁表的開銷嗎?當(dāng)然不是,這里還有一些高速緩存的設(shè)計(jì),這部分后面還可以繼續(xù)討論。
頁表?xiàng)l目
上面我們籠統(tǒng)介紹了頁表將 Page 映射到 Frame。那么,頁表中的每一項(xiàng)(頁表?xiàng)l目)長什么樣子呢?下圖是一個(gè)頁表格式的一個(gè)演示。
頁表?xiàng)l目本身的編號可以不存在頁表中,而是通過偏移量計(jì)算。比如地址 100,000 的編號,可以用 100,000 除以頁大小確定。
- Absent(“在”)位,是一個(gè) bit。0 表示頁的數(shù)據(jù)在磁盤中(不再內(nèi)存中),1 表示在內(nèi)存中。如果讀取頁表發(fā)現(xiàn) Absent = 0,那么會觸發(fā)缺頁中斷,去磁盤讀取數(shù)據(jù)。
- Protection(保護(hù))字段可以實(shí)現(xiàn)成 3 個(gè) bit,它決定頁表用于讀、寫、執(zhí)行。比如 000 代表什么都不能做,100 代表只讀等。
- Reference(訪問)位,代表這個(gè)頁被讀寫過,這個(gè)記錄對回收內(nèi)存有幫助。
- Dirty(“臟”)位,代表頁的內(nèi)容被修改過,如果 Dirty =1,那么意味著頁面必須回寫到磁盤上才能置換(Swap)。如果 Dirty = 0,如果需要回收這個(gè)頁,可以考慮直接丟棄它(什么也不做,其他程序可以直接覆蓋)。
- Caching(緩存位),描述頁可不可以被 CPU 緩存。CPU 緩存會造成內(nèi)存不一致問題,在上個(gè)模塊的加餐中我們討論了內(nèi)存一致性問題,具體你可以參考“模塊四”的加餐內(nèi)容。
Frame Number(Frame 編號),這個(gè)是真實(shí)內(nèi)存的位置。用 Frame 編號乘以頁大小,就可以得到 Frame 的基地址。
在 64bit 的系統(tǒng)中,考慮到 Absent、Protection 等字段需要占用一定的位,因此不能將 64bit 都用來描述真實(shí)地址。但是 64bit 可以尋址的空間已經(jīng)遠(yuǎn)遠(yuǎn)超過了 EB 的級別(1EB = 220TB),這已經(jīng)足夠了。在真實(shí)世界,我們還造不出這么大的內(nèi)存呢。
大頁面問題
最后,我們討論一下大頁面的問題。假設(shè)有一個(gè)應(yīng)用,初始化后需要 12M 內(nèi)存,操作系統(tǒng)頁大小是 4K。那么應(yīng)該如何設(shè)計(jì)呢?
為了簡化模型,下圖中,假設(shè)這個(gè)應(yīng)用只有 3 個(gè)區(qū)域(3 個(gè)段)——正文段(程序)、數(shù)據(jù)段(常量、全局變量)、堆棧段。一開始我們 3 個(gè)段都分配了 4M 的空間。隨著程序執(zhí)行,堆棧段的空間會繼續(xù)增加,上不封頂。
上圖中,進(jìn)程內(nèi)部需要一個(gè)頁表存儲進(jìn)程的數(shù)據(jù)。如果進(jìn)程的內(nèi)存上不封頂,那么頁表有多少個(gè)條目合適呢?進(jìn)程分配多少空間合適呢?如果頁表大小為 1024 個(gè)條目,那么可以支持 1024*4K = 4M 空間。按照這個(gè)計(jì)算,如果進(jìn)程需要 1G 空間,則需要 256K 個(gè)條目。我們預(yù)先為進(jìn)程分配這 256K 個(gè)條目嗎?創(chuàng)建一個(gè)進(jìn)程就劃分這么多條目是不是成本太高了?
為了減少條目的創(chuàng)建,可以考慮進(jìn)程內(nèi)部用一個(gè)更大的頁表(比如 4M),操作系統(tǒng)繼續(xù)用 4K 的頁表。這就形成了一個(gè)二級頁表的結(jié)構(gòu),如下圖所示:
這樣 MMU 會先查詢 1 級頁表,再查詢 2 級頁表。在這個(gè)模型下,進(jìn)程如果需要 1G 空間,也只需要 1024 個(gè)條目。比如 1 級頁編號是 2, 那么對應(yīng) 2 級頁表中 [2* 1024, 3*1024-1] 的部分條目。而訪問一個(gè)地址,需要同時(shí)給出一級頁編號和二級頁編號。整個(gè)地址,還可以用 64bit 組裝,如下圖所示:
MMU 根據(jù) 1 級編號找到 1 級頁表?xiàng)l目,1 級頁表?xiàng)l目中記錄了對應(yīng) 2 級頁表的位置。然后 MMU 再查詢 2 級頁表,找到 Frame。最后通過地址偏移量和 Frame 編號計(jì)算最終的物理地址。這種設(shè)計(jì)是一個(gè)遞歸的過程,因此還可增加 3 級、4 級……每增加 1 級,對空間的利用都會提高——當(dāng)然也會帶來一定的開銷。這對于大應(yīng)用非常劃算,比如需要 1T 空間,那么使用 2 級頁表,頁表的空間就節(jié)省得多了。而且,這種多級頁表,頂級頁表在進(jìn)程中可以先只創(chuàng)建需要用到的部分,就這個(gè)例子而言,一開始只需要 3 個(gè)條目,從 256K 個(gè)條目到 3 個(gè),這就大大減少了進(jìn)程創(chuàng)建的成本。