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