- 記憶體管理硬體結構
- 早期記憶體的使用方法
- 分段
- 分頁
- 邏輯地址,線性地址(intel架構)
- 虛擬地址
- 實體地址
- 結構圖
- 虛擬地址到實體地址的轉換
- 記憶體管理總覽
- 系統呼叫
- vm_area_struct
- 缺頁中斷
- 夥伴系統
- slab分配器
- 頁面回收
- 反向對映
- KSM
- huge page
- 頁遷移
- 記憶體規整
- OOM
- 記憶體管理的一些資料結構
- 線性對映
- struct page
- zone
- 程序角度看記憶體管理
記憶體管理硬體結構
常見的記憶體分配函式有malloc,mmap等,但大家有沒有想過,這些函式在核心中是怎麼實現的?換句話說,Linux核心的記憶體管理是怎麼實現的?
記憶體管理的目的是管理系統中的記憶體,俗稱記憶體橋,換成專業屬於叫DDR。我們有必要先了解下計算機對記憶體管理的硬體結構。我們先看下關於地址的一些概念。
早期記憶體的使用方法
在計算機早期的發展階段,要執行一個程式,要把計算機程式,全部裝載在記憶體中,程式訪問的記憶體地址就是實際的實體地址。所以,當執行多個程式時,必須保證執行程式的使用的總的記憶體量要小於總的記憶體大小。那這種方式存在什麼問題呢?
一個問題是程序地址空間不合理,任意的程序可以隨意修改其他程序的地址資料;二是記憶體使用效率很低,記憶體緊張時需要把整個程序交換到交換分割槽中,導致程式的使用效率很低。
分段
為了解決這兩個問題,當時的人們提出了分段的機制。它的核心思想是建立一個 虛擬地址空間,將一個程式分成程式碼段,資料段,堆疊段什麼的,每個段各自管理不同的資料。在虛擬地址空間和實體地址空間之間做對映,實現程序的隔離。
分頁
在分段機制中,程式也是全部裝載在記憶體中的,效率也很低。這個時候就提出了分頁機制:分頁這個技術仍然是一種虛擬地址空間到實體地址空間對映的機制。但是,粒度更加的小了。單位不是整個程式,而是某個“頁”,一段虛擬地址空間組成的某一頁對映到一段實體地址空間組成的某一頁。
程式在執行的時候,需要哪個頁面,我再把相關頁面交換進來。經常不用的頁面會交換到swap分割槽。分頁機制也是按需分配,這是作業系統的核心思想。
邏輯地址,線性地址(intel架構)
邏輯地址和線性地址是intel架構的概念,邏輯地址是程式產生的和段相關的那個部分,線性地址是邏輯地址轉換為實體地址的一箇中間層。
在分段的方式中,邏輯地址是段的偏移地址,再加上基地址就是線性地址了。如果是做arm架構的,可以不用關注這部分。
虛擬地址
簡單的說就是可以定址的一片空間。如果這個空間是虛擬的,我們就叫做虛擬地址空間;如果這個空間是真實存在的,我們就叫做實體地址空間。虛擬地址空間是可以任意的大的,因為是虛擬的。而實體地址空間是真實存在的,所以是有限的
實體地址
實體地址是CPU透過外部匯流排直接訪問的外部記憶體地址。如果系統啟動了分頁機制,系統啟動後必須透過查頁表的方式去獲取實體地址。
如果沒有啟動分頁機制,系統啟動後就透過直接變為了實體地址。
結構圖
在啟動MMU後,CPU訪問的是虛擬地址,虛擬地址經過MMU後轉換為實體地址,這種轉換透過查詢儲存在主儲存器的頁表完成。頻繁訪問主儲存器比較耗時,因此引入了TLB的概念。
TLB快取了上一次虛擬地址到實體地址的轉換,TLB不儲存具體的資料,儲存的是頁表的表項。如果能在TLB中找到本次訪問的頁表項,就不需要再訪問主存了。我們把這個過程叫做TLB命中。如果沒有找到頁表項,這個時候只能去查詢頁表,我們叫做TLB Miss。如何查詢頁表的後面我們會詳細介紹。
假設,現在虛擬地址已經轉換為了實體地址。這個時候就會去找一級快取。看一級快取有沒有需要的資料。我們這裡採用的是物理索引(PI),物理標籤(PT)的方式。現在的大部分cache都採用組相聯的方式,訪問cache地址會被分為偏移域,索引域,標記域三部分。如果一級快取沒有相應的資料,就要訪問二級快取了,如果二級快取沒有資料,就要訪問主儲存器了。
還有一種情況,當系統實體記憶體短缺的時候,Linux核心中,有頁面回收的機制,會把不常用的頁面交換到swap分割槽中,這個動作叫做swap。這張圖就從硬體結構的角度解釋了記憶體管理的基本構成。
虛擬地址到實體地址的轉換
虛擬地址的32個bit位可以分為3個域,最高12bit位20~31位稱為L1索引,叫做PGD,頁面目錄。中間的8個bit位叫做L2索引,在Linux核心中叫做PT,頁表。最低的12位叫做頁索引。
在ARM處理器中,TTBRx暫存器存放著頁表基地址,我們這裡的一級頁表有4096個頁表項。每個表項中存放著二級表項的基地址。我們可以透過虛擬地址的L1索引訪問一級頁表,訪問一級頁表相當於陣列訪問。
二級頁表通常是動態分配的,可以透過虛擬地址的中間8bit位L2索引訪問二級頁表,在L2索引中存放著最終實體地址的高20bit位,然後和虛擬地址的低12bit位就組成了最終的實體地址。以上就是虛擬地址轉換為實體地址的過程。
MMU訪問頁表是硬體實現的,但頁表的建立和填充需要Linux核心來填充。通常,一級頁表和二級頁表存放在主儲存器中。
記憶體管理總覽
系統呼叫
Linux核心把使用者空間分為兩部分:使用者空間和核心空間。使用者程序執行在使用者空間,如果需要記憶體的話透過C庫提供的malloc
,mmap
,mlock
,madvice
,mremap
函式。C庫的這些函式最終都會呼叫到核心的sys_xxx
介面分配記憶體空間。如malloc
函式是依賴核心的sys_brk
介面分配記憶體空間的。mmap對應介面為sys_mmap
。
我們以malloc
函式為例,假設現在使用者態的記憶體短缺,就會透過sys_brk
呼叫去堆上分配記憶體。在使用者空間分配的是虛擬記憶體,因此,在堆上分配的也是虛擬記憶體。
vm_area_struct
Linux核心把這些地址稱為程序地址空間。核心使用struct vm_area_struct
來管理這些程序地址空間。VMA
主要管理記憶體的建立,插入,刪除,合併等操作。
由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程序使用多個vm_area_struct
結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct
結構使用連結串列或者樹形結構連結,方便程序快速訪問,如下圖所示:
vm_area_struct
結構中包含區域起始和終止地址以及其他相關資訊,同時也包含一個vm_ops
指標,其內部可引出所有針對這個區域可以使用的系統呼叫函式。這樣,程序對某一虛擬記憶體區域的任何操作需要用要的資訊,都可以從vm_area_struct
中獲得。mmap
函式就是要建立一個新的vm_area_struct
結構,並將其與檔案的物理磁碟地址相連。
缺頁中斷
缺頁中斷是實現了按需分配的思想。站在使用者角度,缺頁中斷後可分配的頁面有匿名頁面和page cache
。匿名頁面指的是沒有關聯任何檔案的頁面,比如程序透過mlock
從堆上分配的記憶體。page cache
是關聯了具體快取的頁面。比如在看影片時的快取就是page cache
。匿名頁面和page cache
的產生需要頁面分配器完成。
夥伴系統
頁面分配器是以頁框為單位的。典型的頁面分配器就是夥伴系統。夥伴系統是一個結合了2的方冪個分配器和空閒緩衝區合併計技術的記憶體分配方案, 其基本思想很簡單。
記憶體被分成含有很多頁面的大塊, 每一塊都是2個頁面大小的方冪。 如果找不到想要的塊, 一個大塊會被分成兩部分, 這兩部分彼此就成為夥伴。 其中一半被用來分配,而另一半則空閒。 這些塊在以後分配的過程中會繼續被二分直至產生一個所需大小的塊。 當一個塊被最終釋放時, 其夥伴將被檢測出來,如果夥伴也空閒則合併兩者。
雖然夥伴演算法實現不復雜,但頁面分配器是核心實現最複雜的系統之一。如果記憶體充足時,你需要多少記憶體,頁面分配器會給你分配多少。但如果記憶體緊張時,頁面分配器會做很多嘗試,比如開啟非同步模式的頁面回收,memory compaction
(記憶體規整)。如果經過嘗試後記憶體仍然不夠,這個時候會拿出重型武器oom kill會殺死一些程序。
slab分配器
剛剛我們講的都是以頁為單位分配的記憶體。但有時候我們需要幾個位元組的記憶體怎麼辦。這個時候就需要slab分配器。slab可以管理特定大小的記憶體,對於固定大小的記憶體就不需要VMA去管理了。頁面分配器是中央財政,slab是地方財政。如果地方需要種棵樹就不要勞煩中央財政了。
頁面回收
頁面回收實現了頁面換出的理念。當系統記憶體短缺的時候,系統需要換出一部分記憶體。這部分記憶體通常是page cache 或者匿名頁面。核心裡面有個swap守護執行緒,當系統記憶體低於某個水位時,會被喚醒去掃描LRU(最近最少使用)連結串列,一般匿名頁面和page cache會新增到連結串列中。實際上,在核心中又將LRU連結串列做了細分,又細分為活躍連結串列,不活躍連結串列,匿名頁面連結串列,page cache連結串列。
核心相對比較喜歡回收page cache
,乾淨的page cache
直接合並就好了。對於髒的page cache
需要寫回磁碟的一個動作。對於匿名頁面是不能直接合並的,匿名頁面一般都是程序的私有資料。一般這些匿名頁面資料需要回收時會swap out 到swap分割槽騰出空間,當這些程序再次需要這些資料時,才會從swap分割槽swap in。頁面回收我們會在後面詳細講解。
如果分配好了頁面,這個時候就要涉及到頁表的管理了。頁表分為核心頁表和程序頁表。核心提供了很多和核心頁表相關的函式,後續我們再分析。
再往下分析就是硬體層,比如MMU,TLB,cache,實體記憶體等,對於這部分我們不做深入分析。
反向對映
當程序分配記憶體併發生寫操作時,會分配虛擬地址併產生缺頁,進而分配實體記憶體並建立虛擬地址到實體地址的對映關係, 這個叫正向對映。
反過來, 透過物理頁面找到對映它的所有虛擬頁面叫反向對映(reverse-mapping, RMAP),它可以從page資料結構中找到對映這個page的虛擬地址空間,也就是我們講過的VMA這個東西,ramp系統是為頁面回收服務的,如果要回收一個匿名頁面或者page cache的時候, 需要把對映這個頁面的使用者PTE斷開對映關係才可以去回收。
KSM
KSM,Kernel Samepage Merging,最早是用來最佳化KVM虛擬機器來發明的一種機制。現在用來合併內容相同的匿名頁面。
huge page
huge page
,通常用來分配2M或者1G大小的頁,目前在伺服器系統中用的比較多。使用huge page
可以減少TLB miss的次數,假如現在需要2M的頁面,一個page是4K,最壞的情況下需要TLB miss
5次,如果使用2M的頁面,只需要TLB miss
1次。每次TLB miss
對系統的損耗很大。
頁遷移
頁遷移,核心中有些頁面是可以遷移的,比如匿名頁面。頁遷移在核心很多模組都被廣泛使用,比如memory compaction
(記憶體規整)。
記憶體規整
memory compaction
,記憶體規整模組是為了緩解記憶體碎片化的,系統執行的時間越長,就越容易產生記憶體碎片,系統此時想分配連續的大塊記憶體就變得越來越難。
大塊連續的記憶體一般是核心所請求的,因為對於使用者空間來講,大塊缺頁記憶體都是透過缺頁中斷一塊一塊來分配的。
記憶體規整的實現原理也不復雜,在一個zoom中有兩個掃描器,分別從頭到尾和從尾到頭掃描,一個去查詢zoom中有那些頁面可以遷移的,另外一個去掃描有那些空閒的頁,兩個掃描器在zoom中相遇的時候,掃描就停止了。這個時候記憶體規整模組就知道zoom中有那些頁面可以遷移到空閒頁面。經過這麼一折騰,就可以騰出一個大的連續的物理空間了。
OOM
在經過記憶體規整,頁面遷移等操作後,如果系統還不能分配出系統需要的頁面,Linux就要使用最後一招了,殺敵一千,自損八百,OOM killer會找一些佔用記憶體比較多的程序殺掉來釋放記憶體。
之所以會發生這種情況,是因為Linux核心在給某個程序分配記憶體時,會比程序申請的記憶體多分配一些。這是為了保證程序在真正使用的時候有足夠的記憶體,因為程序在申請記憶體後並不一定立即使用,當真正使用的時候,可能部分記憶體已經被回收了。
比如 當一個程序申請2G記憶體時,核心可能會分配2.5G的記憶體給它.通常這不會導致什麼問題。然而一旦系統內大量的程序在使用記憶體時,就會出現記憶體供不應求,很快就會導致記憶體耗盡。這時就會觸發這個oom killer,它會選擇性的殺掉某個程序以保證系統能夠正常執行。
記憶體管理的一些資料結構
線性對映
我們以32位系統為例,我們知道程序最大的地址訪問空間是4G,0~3GB是使用者空間,3 ~ 4GB是核心空間。
如果物理空間是大於1GB,核心空間如何訪問大於1GB的空間呢?站在核心的角度,低地址段是線性對映,高地址段是高階對映。
那線性對映和高階對映是如何劃分的呢?不同的體系結構有不同的劃分方法。在ARM32中是線性對映大小為760M。線性對映就是直接把實體地址空間對映到3G ~ 4G的地址空間,這段對映關係就變得比較簡單了,核心訪問時直接使用虛擬地址減去偏移量(page offset)就得到實體地址了。
如果要訪問高階記憶體就麻煩一點,1G的實體記憶體空間有限,不能把所有地址都對映到線性地址空間。如果要訪問高階記憶體就要透過動態對映的方式訪問了。
struct page
struct page
資料結構是用來抽象物理頁面的。這個資料結構很重要,很多核心程式碼都是圍繞這個struct page
展開的。
此外還有個很重要的mem_map[]
陣列,是用來存放每一個struct page
資料結構的。透過陣列,我們可以很方便的透過page找到頁幀號,頁幀號全稱叫page frame number
,pfm。
zone
除了page結構,還有個很重要的資料結構叫zone。前面講到了實體記憶體劃分為兩部分,線性對映和高階記憶體。zone也是根據這個來劃分的。線性對映部分叫zone normal,高階記憶體區域叫zone high。
頁面分配器和頁面回收都是基於zone來管理的。zone 也是一個很重要的管理實體記憶體的資料結構。
程序角度看記憶體管理
看完實體記憶體的管理結構,接下來從程序的角度看下虛擬記憶體是怎麼管理的。
使用者空間有3G的大小,這3GB的大小也做了劃分,0 ~ 1GB 屬於程式碼段,資料段,堆空間。 1G ~ 3G 屬於mmap空間。
每個程序都有一個管理程序的資料結構,作業系統中叫做PCB,程序控制塊,linux核心中就用task_struct
描述程序控制塊,task_struct
內容非常多,後面我們會詳細講解,今天我們只關注mm成員。
mm成員會指向mm_struct
描述程序管理的記憶體資源,我們這裡只關注mmap,pgd。 mmap指向該程序的VMA的連結串列。我們知道程序地址空間使用VMA來管理,VMA是離散的,所以核心使用兩種方式來管理VMA:連結串列和紅黑樹。
pgd指向程序所在的頁表,這裡指的是程序的頁表,程序的一級頁表在fork的時候建立,程序的二級頁表在實際使用的時候動態建立,
以上這張圖就從程序的角度講述了記憶體管理的概貌。