Linux內(nèi)存管理那些事兒
自馮諾伊曼已來,計算機都是采用程序存儲架構(gòu)。
什么是程序存儲架構(gòu)?簡單來說就是把用來運行的程序也當(dāng)做數(shù)據(jù)一樣存儲在計算機中。現(xiàn)在聽起來這么簡單的一個思想,***提出卻是了不起的貢獻(xiàn):里面包含了哥德爾精致的集合悖論和圖靈美妙的圖靈機設(shè)想。最早的計算機器僅內(nèi)含固定用途的程序。現(xiàn)代的某些計算機依然維持這樣的設(shè)計方式,通常是為了簡化或教育目的。例如一個計算器僅有固定的數(shù)學(xué)計算程序,它不能拿來當(dāng)作文字處理軟件,更不能拿來玩游戲。若想要改變此機器的程序,你必須更改線路、更改結(jié)構(gòu)甚至重新設(shè)計此機器。當(dāng)然最早的計算機并沒有設(shè)計的那么可編程化。當(dāng)時所謂的“重寫程序”很可能指的是紙筆設(shè)計程序步驟,接著制訂工程細(xì)節(jié),再施工將機器的電路配線或結(jié)構(gòu)改變。而程序存儲型電腦的概念改變了這一切。借由創(chuàng)造一組指令集結(jié)構(gòu),并將所謂的運算轉(zhuǎn)化成一串程序指令的運行細(xì)節(jié),讓此機器更有彈性。借著將指令當(dāng)成一種特別類型的靜態(tài)數(shù)據(jù),一臺存儲程序型電腦可輕易改變其程序,并在程控下改變其運算內(nèi)容。
講這一段,就是要明確,在計算機中,準(zhǔn)確的說是CPU中,程序和數(shù)據(jù)都是存儲在計算機的內(nèi)存或者硬盤中的,要使用時,都是要經(jīng)過尋址來找出來加載到CPU中的。內(nèi)存只是更快的硬盤,以下將會使用內(nèi)存紙袋存儲,忽略內(nèi)存到硬盤的換入換出。
內(nèi)存地址本質(zhì)上對應(yīng)物理硬件上的地址引腳。使用內(nèi)存地址訪問物理存儲天經(jīng)地義,內(nèi)存地址和物理地址一一對應(yīng),也是天經(jīng)地義。內(nèi)存地址就是物理地址,這就是“實模式”。
那是什么時候內(nèi)存地址有了“邏輯地址”,“線性地址”,“虛擬地址”,“物理地址”等,這些稱呼?
這一切都要從80286說起,Intel微處理器從這個版本開始引入了“保護(hù)模式”,也是就是內(nèi)存地址的分段表示。顧名思義,這是為了內(nèi)存保護(hù)的目的(還有就是分離用戶空間和內(nèi)核空間)。這樣內(nèi)存地址不再是物理地址了,而僅僅是一個偏移量了,原來的內(nèi)存地址變成了邏輯地址,而這個偏移量則是“線性地址”或“虛擬地址”。要獲得物理就需要在自己所在的段中去找(下面提到的段描述中有一個基地址)!為此,Intel引入了段描述符以及保護(hù)目的的鑒權(quán)屬性,結(jié)合“程序存儲”的概念,至少存在兩種段描述符:代碼段描述符和數(shù)據(jù)段描述符。如何找到內(nèi)存地址所在的段描述符呢?Intel又引入段選擇子,以及鑒權(quán)屬性。這樣使用段選擇子就是可以找到段描述符,再使用原來的內(nèi)存地址作為偏移量來找到真正的物理地址了。但是從實模式到保護(hù)模式,只有一個地址序號,哪里去找段選擇子和段描述符呢?Intel引入了一些段寄存器來存儲段選擇子,當(dāng)然至少是代碼段寄存器和數(shù)據(jù)段寄存器。而內(nèi)核在啟動時會建立全局段描述符表,這樣一切都解決了。為了加快段描述符表的訪問(否則又是瓶頸),Intel又引入了不可編程的段描述符寄存器,隨著段寄出器加載而加載。這就是鼎鼎大名的影子寄存器。
現(xiàn)在看來,這次失敗的設(shè)計還是挺成功的。
Linux就直接繞過了分段機制。分段機制引入段選擇子和段描述符把地址空間轉(zhuǎn)換成偏移量,Linux通過為所有程序設(shè)置相同的段選擇子和段描述符,把偏移量又轉(zhuǎn)換成地址空間,一切又回到了原點。
看看分頁是多么簡單和優(yōu)雅。以32位地址空間為例,分為三段(10,10,12),分別是作為頁目錄,頁表和頁框的索引,即可訪問32G的物理內(nèi)存。只需要存儲頁目錄一個寄存器(cr3)即可。
在整個尋址的過程中,TLB緩存是至關(guān)主要的。TLB緩存內(nèi)存線性地址到物理地址的直接映射,你說重要不重要?
TLB緩存還間接了影響了地址空間架構(gòu)。我們知道Linux環(huán)境下32位系統(tǒng)進(jìn)程地址空間是4G,這4G地址空間用戶態(tài)占3G,內(nèi)核態(tài)占1G。并且用戶態(tài)各個進(jìn)程的地址空間是獨立的,也就是每個進(jìn)程都可以訪問0到3G的進(jìn)程地址空間。而內(nèi)核態(tài)的進(jìn)程地址空間是所有進(jìn)程共享的,即所有進(jìn)程共用1個的地址空間。(更準(zhǔn)確的說法是把內(nèi)核地址空間映射到所有進(jìn)程的地址空間)。內(nèi)核為什么這么設(shè)計呢?為什么要劃分用戶空間和內(nèi)核空間呢?答案就在TLB 緩存:因為在內(nèi)核空間進(jìn)入/退出時,刷新整個TLB的代價太高了。
- In the i386 arch, for example, we choose to map the kernel into every process's VM space so that we don't have to pay the full TLB invalidation costs for kernel entry/exit. This means the available virtual memory space (4GiB on i386) has to be divided between user and kernel space.
和TLB緩存相關(guān)還有一個重要的概念是hugepage。hugepage支持建立在大多數(shù)現(xiàn)代處理器架構(gòu)提供的多頁大小支持之上。例如,x86 CPU通常支持4K和2M(1G,如果架構(gòu)支持)頁面大小,ia64 架構(gòu)支持多頁大小4K,8K,64K,256K,1M,4M,16M,256M以及ppc64支持4K和16M。隨著越來越大的物理存儲器(幾GB)更容易獲得,TLB的優(yōu)化更為關(guān)鍵。
TLB 駐留在CPU的1級cache里,是芯片訪問最快的緩存,一般只能容納100多條頁表項,如果采用hugepage,則可以極大減少 TLB cache miss 導(dǎo)致的開銷:TLB***,立即就獲取到物理地址,如果不***,需要查 rc3->進(jìn)程頁目錄表pgd->進(jìn)程頁中間表pmd->進(jìn)程頁框->物理內(nèi)存,如果這中間pmd或者頁框被虛擬內(nèi)存系統(tǒng)替換到交互區(qū),則還需要交互區(qū)load回內(nèi)存。。總之,TLB cache miss是性能大殺手,而采用hugepage可以有效降低TLB cache miss。
一旦大量的頁面被預(yù)分配給內(nèi)核作為hugepage的頁面池,這些頁面將在內(nèi)核中保留,不能用于其他目的。內(nèi)核使用名字為“hugetlbfs” 的文件系統(tǒng)管理這些頁面池。當(dāng)支持多個hugepage大小時,/proc/sys/vm/nr_hugepages指示預(yù)先分配的大量頁面的默認(rèn)大小的當(dāng)前數(shù)量。因此,可以使用以下命令來動態(tài)分配/取消分配默認(rèn)大小的持續(xù)hugepage:
echo 20 > /proc/sys/vm/nr_hugepages
該命令將嘗試將hugepage頁面池中的默認(rèn)大小的hugepage的數(shù)量調(diào)整為20 ,根據(jù)需要分配或釋放hugepage。
/proc/meminfo文件提供有關(guān)內(nèi)核hugepage池中持久hugetlb頁面總數(shù)的信息。它還顯示有關(guān)免費,預(yù)留和剩余hugepage數(shù)量以及默認(rèn)頁面大小的信息。“cat /proc/meminfo”的輸出將包括以下行:
- .....
- HugePages_Total: vvv
- HugePages_Free: www
- HugePages_Rsvd: xxx
- HugePages_Surp: yyy
- Hugepagesize: zzz kB
其中: HugePages_Total是hugepage頁面池的大小。 HugePages_Free是池中尚未分配的hugepage數(shù)。 HugePages_Rsvd是“保留” 的縮寫,是從池中分配的承諾的hugepage的數(shù)量,但尚未分配。保留hugepage保證應(yīng)用程序能夠在故障時間從hugepage頁面池中分配一個hugepage。 HugePages_Surp是“剩余”的縮寫,是 /proc/sys/vm/nr_hugepages中的值之上的hugepage數(shù)。剩余hugepage的***數(shù)量由/proc/sys/vm/nr_overcommit_hugepages控制。
由于內(nèi)核1G地址空間的限制,對于高端內(nèi)存(物理地址空間大于虛擬地址空間的情況),內(nèi)核無法同時映射所有物理內(nèi)存,這意味著當(dāng)使用這些內(nèi)存時,內(nèi)核使用臨時映射。
說到高端內(nèi)存,不禁想起了物理地址擴(kuò)展。處理器所支持的RAM容量受鏈接到地址總線上的地址管腳數(shù)限制。早起Intel處理器從80386到Pentium使用32位物理地址。從理論上講,這樣的系統(tǒng)上可以安裝高達(dá)4GB的RAM,而實際上,由于用戶進(jìn)程線性地址空間的需要,內(nèi)核不能直接對1GB以上的RAM進(jìn)行尋址。然而,大型服務(wù)器需要大于4GB的RAM來同時運行上千的進(jìn)程,實際上我們現(xiàn)在的很多計算機的RAM都可能超過這個量級。Intel通過在它的處理器上把管腳數(shù)從32增加到36已經(jīng)滿足了這些需求。從Pentium Pro開始,Intel所有的處理器現(xiàn)在的尋址能力達(dá)2^36=64GB.不過,只有引入一種新的分頁機制把32位線性地址轉(zhuǎn)換為36位物理地址才能使用所增加的物理地址。顯然,PAE并沒有擴(kuò)大進(jìn)程的線性地址空間,因為它只能處理物理地址,此外,只有內(nèi)核能夠修改進(jìn)程的頁表,所以用戶態(tài)下運行的進(jìn)程不能使用大于4GB的物理地址空間。另一方面,PAE允許內(nèi)核使用高達(dá)64GB的RAM,從而顯著增加了系統(tǒng)中的進(jìn)程數(shù)量。
【本文是51CTO專欄作者石頭的原創(chuàng)文章,轉(zhuǎn)載請通過作者微信公眾號補天遺石(butianys)獲取授權(quán)】