Linux的記憶體管理可謂是學好Linux的必經之路,也是Linux的關鍵知識點,有人說打通了記憶體管理的知識,也就打通了Linux的任督二脈,這一點不誇張。有人問網上有很多Linux記憶體管理的內容,為什麼還要看你這一篇,這正是我寫此文的原因,網上碎片化的相關知識點大都是東拼西湊,先不說正確性與否,就連基本的邏輯都沒有搞清楚,我可以負責任的說Linux記憶體管理只需要看此文一篇就可以讓你入Linux核心的大門,省去你東找西找的時間,讓你形成記憶體管理知識的閉環。
文章比較長,做好準備,深呼吸,讓我們一起開啟Linux核心的大門!
Linux記憶體管理之CPU訪問記憶體的過程
我喜歡用圖的方式來說明問題,簡單直接:
藍色部分是cpu,灰色部分是記憶體,白色部分就是cpu訪問記憶體的過程,也是地址轉換的過程。在解釋地址轉換的本質前我們先理解下幾個概念:
TLB:MMU工作的過程就是查詢頁表的過程。如果把頁表放在記憶體中查詢的時候開銷太大,因此為了提高查詢效率,專門用一小片訪問更快的區域存放地址轉換條目。(當頁表內容有變化的時候,需要清除TLB,以防止地址映射出錯。)
Caches:cpu和記憶體之間的快取機制,用於提高訪問速率,armv8架構的話上圖的caches其實是L2 Cache,這裡就不做進一步解釋了。
虛擬地址轉換為物理地址的本質
我們知道核心中的定址空間大小是由CONFIG_ARM64_VA_BITS控制的,這裡以48位為例,ARMv8中,Kernel Space的頁表基地址存放在TTBR1_EL1暫存器中,User Space頁表基地址存放在TTBR0_EL0暫存器中,其中核心地址空間的高位為全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF),使用者地址空間的高位為全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)
有了宏觀概念,下面我們以核心態定址過程為例看下是如何把虛擬地址轉換為物理地址的。
我們知道linux採用了分頁機制,通常採用四級頁表,頁全域性目錄(PGD),頁上級目錄(PUD),頁中間目錄(PMD),頁表(PTE)。如下:
MMU根據虛擬地址的最高位判斷用哪個頁表基地址做為訪問的起點。最高位是0時,使用TTBR0_EL0作為起點,表示訪問使用者空間地址;最高位時1時,使用TTBR1_EL1作為起點,表示訪問核心空間地址。MMU從相應的頁表基地址暫存器TTBR0_EL0或者TTBR1_EL1,獲取PGD頁全域性目錄基地址。
找到PGD後,從虛擬地址中找到PGD index,透過PGD index找到頁上級目錄PUD基地址。
找到PUD後,從虛擬地址中找到PUD index,透過PUD index找到頁中間目錄PMD基地址。
找到PMD後,從虛擬地址中找到PDM index,透過PMD index找到頁表項PTE基地址。
找到PTE後,從虛擬地址中找到PTE index,透過PTE index找到頁表項PTE。
從頁表項PTE中取出物理頁幀號PFN,然後加上頁內偏移VA[11,0],就組成了最終的物理地址PA。
整個過程是比較機械的,每次轉換先獲取物理頁基地址,再從線性地址中獲取索引,合成物理地址後再訪問記憶體。不管是頁表還是要訪問的資料都是以頁為單位存放在主存中的,因此每次訪問記憶體時都要先獲得基址,再透過索引(或偏移)在頁內訪問資料,因此可以將線性地址看作是若干個索引的集合。
【Linux核心記憶體管理專題訓練營】火熱開營!!
最新Linux核心技術詳解
獨家Linux核心記憶體管理乾貨分享
兩天持續技術輸出:
——————————
第一天:
1。物理記憶體對映及空間劃分
2。ARM32/64頁表的對映過程
3。分配物理頁面及Slab分配器
4。實戰:VMA查詢/插入/合併
——————————
第二天:
5。實戰:mallocap系統呼叫實現
6。缺頁中斷處理/反向對映
7。回收頁面/匿名頁面生命週期
8。KSM實現/Dirty COW記憶體漏洞
原價“198”,現“0。02”特惠!
限時特價入營地址
立即搶購加入吧
https://
m。ke。qq。com/course/3485
817?flowToken=1036017
(二維碼自動識別)
Linux記憶體初始化
有了armv8架構訪問記憶體的理解,我們來看下linux在記憶體這塊的初始化就更容易理解了。
建立啟動頁表:
在彙編程式碼階段的head。S檔案中,負責建立對映關係的函式是create_page_tables。create_page_tables函式負責identity mapping和kernel image mapping。
identity map:是指把idmap_text區域的物理地址對映到相等的虛擬地址上,這種對映完成後,其虛擬地址等於物理地址。idmap_text區域都是一些開啟MMU相關的程式碼。
kernel image map:將kernel執行需要的地址(kernel txt、rodata、data、bss等等)進行對映。
arch/arm64/kernel/head。S:
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
/*
* The following calls CPU setup code, see arch/arm64/mm/proc。S for
* details。
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set。
*/
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)
__create_page_tables主要執行的就是identity map和kernel image map:
__create_page_tables:
……
create_pgd_entry x0, x3, x5, x6
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
create_block_map x0, x7, x3, x5, x6
/*
* Map the kernel image (starting with PHYS_OFFSET)。
*/
adrp x0, swapper_pg_dir
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
create_pgd_entry x0, x5, x3, x6
adrp x6, _end // runtime __pa(_end)
adrp x3, _text // runtime __pa(_text)
sub x6, x6, x3 // _end - _text
add x6, x6, x5 // runtime __va(_end)
create_block_map x0, x7, x3, x5, x6
……
其中呼叫create_pgd_entry進行PGD及所有中間level(PUD, PMD)頁表的建立,呼叫create_block_map進行PTE頁表的對映。關於四級頁表的關係如下圖所示,這裡就不進一步解釋了。
彙編結束後的記憶體對映關係如下圖所示:
等記憶體初始化後就可以進入真正的記憶體管理了,初始化我總結了一下,大體分為四步:
物理記憶體進系統前
用memblock模組來對記憶體進行管理
頁表對映
zone初始化
Linux是如何組織物理記憶體的?
node 目前計算機系統有兩種體系結構:
非一致性記憶體訪問 NUMA(Non-Uniform Memory Access)意思是記憶體被劃分為各個node,訪問一個node花費的時間取決於CPU離這個node的距離。每一個cpu內部有一個本地的node,訪問本地node時間比訪問其他node的速度快
一致性記憶體訪問 UMA(Uniform Memory Access)也可以稱為SMP(Symmetric Multi-Process)對稱多處理器。意思是所有的處理器訪問記憶體花費的時間是一樣的。也可以理解整個記憶體只有一個node。
zone
ZONE的意思是把整個物理記憶體劃分為幾個區域,每個區域有特殊的含義
page
代表一個物理頁,在核心中一個物理頁用一個struct page表示。
page frame
為了描述一個物理page,核心使用struct page結構來表示一個物理頁。假設一個page的大小是4K的,核心會將整個物理記憶體分割成一個一個4K大小的物理頁,而4K大小物理頁的區域我們稱為page frame
page frame num(pfn)
pfn是對每個page frame的編號。故物理地址和pfn的關係是:
物理地址>>PAGE_SHIFT = pfn
pfn和page的關係
核心中支援了好幾個記憶體模型:CONFIG_FLATMEM(平坦記憶體模型)CONFIG_DISCONTIGMEM(不連續記憶體模型)CONFIG_SPARSEMEM_VMEMMAP(稀疏的記憶體模型)目前ARM64使用的稀疏的型別模式。
系統啟動的時候,核心會將整個struct page對映到核心虛擬地址空間vmemmap的區域,所以我們可以簡單的認為struct page的基地址是vmemmap,則:
vmemmap+pfn的地址就是此struct page對應的地址。
Linux分割槽頁框分配器
頁框分配在核心裡的機制我們叫做分割槽頁框分配器(zoned page frame allocator),在linux系統中,分割槽頁框分配器管理著所有物理記憶體,無論你是核心還是程序,都需要請求分割槽頁框分配器,這時才會分配給你應該獲得的物理記憶體頁框。當你所擁有的頁框不再使用時,你必須釋放這些頁框,讓這些頁框回到管理區頁框分配器當中。
有時候目標管理區不一定有足夠的頁框去滿足分配,這時候系統會從另外兩個管理區中獲取要求的頁框,但這是按照一定規則去執行的,如下:
如果要求從DMA區中獲取,就只能從ZONE_DMA區中獲取。
如果沒有規定從哪個區獲取,就按照順序從 ZONE_NORMAL -> ZONE_DMA 獲取。
如果規定從HIGHMEM區獲取,就按照順序從 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 獲取。
核心中根據不同的分配需求有6個函式介面來請求頁框,最終都會呼叫到__alloc_pages_nodemask。
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);//fastpath分配頁面:從pcp(per_cpu_pages)和夥伴系統中正常的分配記憶體空間
……
page = __alloc_pages_slowpath(alloc_mask, order, &ac);//slowpath分配頁面:如果上面沒有分配到空間,呼叫下面函式慢速分配,允許等待和回收
……
}
在頁面分配時,有兩種路徑可以選擇,如果在快速路徑中分配成功了,則直接返回分配的頁面;快速路徑分配失敗則選擇慢速路徑來進行分配。總結如下:
正常分配(或叫快速分配):
如果分配的是單個頁面,考慮從per CPU快取中分配空間,如果快取中沒有頁面,從夥伴系統中提取頁面做補充。
分配多個頁面時,從指定型別中分配,如果指定型別中沒有足夠的頁面,從備用型別連結串列中分配。最後會試探保留型別連結串列。
慢速(允許等待和頁面回收)分配:
當上面兩種分配方案都不能滿足要求時,考慮頁面回收、殺死程序等操作後在試。
Linux頁框分配器之夥伴演算法
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask)
{
if (!zone_watermark_fast(zone, order, mark, ac_classzone_idx(ac), alloc_flags))
{
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN:
continue;
case NODE_RECLAIM_FULL:
continue;
default:
if (zone_watermark_ok(zone, order, mark, ac_classzone_idx(ac), alloc_flags))
goto try_this_zone;
continue;
}
}
try_this_zone: //本zone正常水位
page = rmqueue(ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype);
}
return NULL;
}
首先遍歷當前zone,按照HIGHMEM->NORMAL的方向進行遍歷,判斷當前zone是否能夠進行記憶體分配的條件是首先判斷free memory是否滿足low water mark水位值,如果不滿足則進行一次快速的記憶體回收操作,然後再次檢測是否滿足low water mark,如果還是不能滿足,相同步驟遍歷下一個zone,滿足的話進入正常的分配情況,即rmqueue函式,這也是夥伴系統的核心。
Buddy 分配演算法
在看函式前,我們先看下演算法,因為我一直認為有了“道”的理解才好進一步理解“術”。
假設這是一段連續的頁框,陰影部分表示已經被使用的頁框,現在需要申請一個連續的5個頁框。這個時候,在這段記憶體上不能找到連續的5個空閒的頁框,就會去另一段記憶體上去尋找5個連續的頁框,這樣子,久而久之就形成了頁框的浪費。為了避免出現這種情況,Linux核心中引入了夥伴系統演算法(Buddy system)。把所有的空閒頁框分組為11個塊連結串列,每個塊連結串列分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連續頁框,對應4MB大小的連續記憶體。每個頁框塊的第一個頁框的物理地址是該塊大小的整數倍,如圖:
假設要申請一個256個頁框的塊,先從256個頁框的連結串列中查詢空閒塊,如果沒有,就去512個頁框的連結串列中找,找到了則將頁框塊分為2個256個頁框的塊,一個分配給應用,另外一個移到256個頁框的連結串列中。如果512個頁框的連結串列中仍沒有空閒塊,繼續向1024個頁框的連結串列查詢,如果仍然沒有,則返回錯誤。頁框塊在釋放時,會主動將兩個連續的頁框塊合併為一個較大的頁框塊。
從上面可以知道Buddy演算法一直在對頁框做拆開合併拆開合併的動作。Buddy演算法牛逼就牛逼在運用了世界上任何正整數都可以由2^n的和組成。這也是Buddy演算法管理空閒頁表的本質。空閒記憶體的資訊我們可以透過以下命令獲取:
也可以透過echo m > /proc/sysrq-trigger來觀察buddy狀態,與/proc/buddyinfo的資訊是一致的:
Buddy 分配函式
static inline
struct page *rmqueue(struct zone *preferred_zone,
struct zone *zone, unsigned int order,
gfp_t gfp_flags, unsigned int alloc_flags,
int migratetype)
{
if (likely(order == 0)) { //如果order=0則從pcp中分配
page = rmqueue_pcplist(preferred_zone, zone, order, gfp_flags, migratetype);
}
do {
page = NULL;
if (alloc_flags & ALLOC_HARDER) {//如果分配標誌中設定了ALLOC_HARDER,則從free_list[MIGRATE_HIGHATOMIC]的連結串列中進行頁面分配
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
}
if (!page) //前兩個條件都不滿足,則在正常的free_list[MIGRATE_*]中進行分配
page = __rmqueue(zone, order, migratetype);
} while (page && check_new_pages(page, order));
……
}
Linux分割槽頁框分配器之水位
我們講頁框分配器的時候講到了快速分配和慢速分配,其中夥伴演算法是在快速分配裡做的,忘記的小夥伴我們再看下:
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask)
{
if (!zone_watermark_fast(zone, order, mark, ac_classzone_idx(ac), alloc_flags))
{
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN:
continue;
case NODE_RECLAIM_FULL:
continue;
default:
if (zone_watermark_ok(zone, order, mark, ac_classzone_idx(ac), alloc_flags))
goto try_this_zone;
continue;
}
}
try_this_zone: //本zone正常水位
page = rmqueue(ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype);
}
return NULL;
}
可以看到在進行夥伴演算法分配前有個關於水位的判斷,今天我們就看下水位的概念。
簡單的說在使用分割槽頁面分配器中會將可以用的free pages與zone裡的水位(watermark)進行比較。
水位初始化
nr_free_buffer_pages 是獲取ZONE_DMA和ZONE_NORMAL區中高於high水位的總頁數nr_free_buffer_pages = managed_pages - high_pages
min_free_kbytes 是總的min大小,min_free_kbytes = 4 * sqrt(lowmem_kbytes)
setup_per_zone_wmarks 根據總的min值,再加上各個zone在總記憶體中的佔比,然後透過do_div就計算出他們各自的min值,進而計算出各個zone的水位大小。min,low,high的關係如下:low = min *125%;
high = min * 150%
min:low:high = 4:5:6
setup_per_zone_lowmem_reserve 當從Normal失敗後,會嘗試從DMA申請分配,透過lowmem_reserve[DMA],限制來自Normal的分配請求。其值可以透過/proc/sys/vm/lowmem_reserve_ratio來修改。
從這張圖可以看出:
如果空閒頁數目min值,則該zone非常缺頁,頁面回收壓力很大,應用程式寫記憶體操作就會被阻塞,直接在應用程式的程序上下文中進行回收,即direct reclaim。
如果空閒頁數目小於low值,kswapd執行緒將被喚醒,並開始釋放回收頁面。
如果空閒頁面的值大於high值,則該zone的狀態很完美, kswapd執行緒將重新休眠。
Linux頁框分配器之記憶體碎片化整理
什麼是記憶體碎片化
Linux物理記憶體碎片化包括兩種:內部碎片化和外部碎片化。
內部碎片化:
指分配給使用者的記憶體空間中未被使用的部分。例如程序需要使用3K bytes物理記憶體,於是向系統申請了大小等於3Kbytes的記憶體,但是由於Linux核心夥伴系統演算法最小顆粒是4K bytes,所以分配的是4Kbytes記憶體,那麼其中1K bytes未被使用的記憶體就是記憶體內碎片。
外部碎片化:
指系統中無法利用的小記憶體塊。例如系統剩餘記憶體為16K bytes,但是這16K bytes記憶體是由4個4K bytes的頁面組成,即16K記憶體物理頁幀號#1不連續。在系統剩餘16K bytes記憶體的情況下,系統卻無法成功分配大於4K的連續物理記憶體,該情況就是記憶體外碎片導致。
碎片化整理演算法
Linux記憶體對碎片化的整理演算法主要應用了核心的頁面遷移機制,是一種將可移動頁面進行遷移後騰出連續物理記憶體的方法。
假設存在一個非常小的記憶體域如下:
藍色表示空閒的頁面,白色表示已經被分配的頁面,可以看到如上記憶體域的空閒頁面(藍色)非常零散,無法分配大於兩頁的連續物理記憶體。
下面演示一下記憶體規整的簡化工作原理,核心會執行兩個獨立的掃描動作:第一個掃描從記憶體域的底部開始,一邊掃描一邊將已分配的可移動(MOVABLE)頁面記錄到一個列表中:
另外第二掃描是從記憶體域的頂部開始,掃描可以作為頁面遷移目標的空閒頁面位置,然後也記錄到一個列表裡面:
等兩個掃描在域中間相遇,意味著掃描結束,然後將左邊掃描得到的已分配的頁面遷移到右邊空閒的頁面中,左邊就形成了一段連續的物理記憶體,完成頁面規整。
碎片化整理的三種方式
static struct page *
__alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
unsigned int alloc_flags, const struct alloc_context *ac,
enum compact_priority prio, enum compact_result *compact_result)
{
struct page *page;
unsigned int noreclaim_flag;
if (!order)
return NULL;
noreclaim_flag = memalloc_noreclaim_save();
*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
prio);
memalloc_noreclaim_restore(noreclaim_flag);
if (*compact_result <= COMPACT_INACTIVE)
return NULL;
count_vm_event(COMPACTSTALL);
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page) {
struct zone *zone = page_zone(page);
zone->compact_blockskip_flush = false;
compaction_defer_reset(zone, order, true);
count_vm_event(COMPACTSUCCESS);
return page;
}
count_vm_event(COMPACTFAIL);
cond_resched();
return NULL;
}
在linux核心裡一共有3種方式可以碎片化整理,我們總結如下:
Linux slab分配器
在Linux中,夥伴系統是以頁為單位分配記憶體。但是現實中很多時候卻以位元組為單位,不然申請10Bytes記憶體還要給1頁的話就太浪費了。slab分配器就是為小記憶體分配而生的。slab分配器分配記憶體以Byte為單位。但是slab分配器並沒有脫離夥伴系統,而是基於夥伴系統分配的大記憶體進一步細分成小記憶體分配。
他們之間的關係可以用一張圖來描述:
流程分析
kmem_cache_alloc 主要四步:
先從 kmem_cache_cpu->freelist中分配,如果freelist為null
接著去 kmem_cache_cpu->partital連結串列中分配,如果此連結串列為null
接著去 kmem_cache_node->partital連結串列分配,如果此連結串列為null
重新分配一個slab。
Linux 記憶體管理之vmalloc
根據前面的系列文章,我們知道了buddy system是基於頁框分配器,kmalloc是基於slab分配器,而且這些分配的地址都是物理記憶體連續的。但是隨著碎片化的積累,連續物理記憶體的分配就會變得困難,對於那些非DMA訪問,不一定非要連續物理記憶體的話完全可以像malloc那樣,將不連續的物理記憶體頁框對映到連續的虛擬地址空間中,這就是vmap的來源)(提供把離散的page對映到連續的虛擬地址空間),vmalloc的分配就是基於這個機制來實現的。
vmalloc最小分配一個page,並且分配到的頁面不保證是連續的,因為vmalloc內部呼叫alloc_page多次分配單個頁面。
vmalloc的區域就是在上圖中VMALLOC_START - VMALLOC_END之間,可透過/proc/vmallocinfo檢視。
vmalloc流程
主要分以下三步:
從VMALLOC_START到VMALLOC_END查詢空閒的虛擬地址空間(hole)
根據分配的size,呼叫alloc_page依次分配單個頁面。
把分配的單個頁面,對映到第一步中找到的連續的虛擬地址。把分配的單個頁面,對映到第一步中找到的連續的虛擬地址。
Linux程序的記憶體管理之缺頁異常
當程序訪問這些還沒建立對映關係的虛擬地址時,處理器會自動觸發缺頁異常。
ARM64把異常分為同步異常和非同步異常,通常非同步異常指的是中斷(可看《上帝視角看中斷》),同步異常指的是異常。關於ARM異常處理的文章可參考《ARMv8異常處理簡介》。
當處理器有異常發生時,處理器會先跳轉到ARM64的異常向量表中:
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error_invalid // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error_invalid // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
以el1下的異常為例,當跳轉到el1_sync函式時,讀取ESR的值以判斷異常型別。根據型別跳轉到不同的處理函數里,如果是data abort的話跳轉到el1_da函數里,instruction abort的話跳轉到el1_ia函數里:
el1_sync:
kernel_entry 1
mrs x1, esr_el1 // read the syndrome register
lsr x24, x1, #ESR_ELx_EC_SHIFT // exception class
cmp x24, #ESR_ELx_EC_DABT_CUR // data abort in EL1
b。eq el1_da
cmp x24, #ESR_ELx_EC_IABT_CUR // instruction abort in EL1
b。eq el1_ia
cmp x24, #ESR_ELx_EC_SYS64 // configurable trap
b。eq el1_undef
cmp x24, #ESR_ELx_EC_SP_ALIGN // stack alignment exception
b。eq el1_sp_pc
cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
b。eq el1_sp_pc
cmp x24, #ESR_ELx_EC_UNKNOWN // unknown exception in EL1
b。eq el1_undef
cmp x24, #ESR_ELx_EC_BREAKPT_CUR // debug exception in EL1
b。ge el1_dbg
b el1_inv
流程圖如下:
do_page_fault
static int __do_page_fault(struct mm_struct *mm, unsigned long addr,
unsigned int mm_flags, unsigned long vm_flags,
struct task_struct *tsk)
{
struct vm_area_struct *vma;
int fault;
vma = find_vma(mm, addr);
fault = VM_FAULT_BADMAP; //沒有找到vma區域,說明addr還沒有在程序的地址空間中
if (unlikely(!vma))
goto out;
if (unlikely(vma->vm_start > addr))
goto check_stack;
/*
* Ok, we have a good vm_area for this memory access, so we can handle
* it。
*/
good_area://一個好的vma
/*
* Check that the permissions on the VMA allow for the fault which
* occurred。
*/
if (!(vma->vm_flags & vm_flags)) {//許可權檢查
fault = VM_FAULT_BADACCESS;
goto out;
}
//重新建立物理頁面到VMA的對映關係
return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags);
check_stack:
if (vma->vm_flags & VM_GROWSDOWN && !expand_stack(vma, addr))
goto good_area;
out:
return fault;
}
從__do_page_fault函式能看出來,當觸發異常的虛擬地址屬於某個vma,並且擁有觸發頁錯誤異常的許可權時,會呼叫到handle_mm_fault函式來建立vma和物理地址的對映,而handle_mm_fault函式的主要邏輯是透過__handle_mm_fault來實現的。
__handle_mm_fault
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
……
//查詢頁全域性目錄,獲取地址對應的表項
pgd = pgd_offset(mm, address);
//查詢頁四級目錄表項,沒有則建立
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
//查詢頁上級目錄表項,沒有則建立
vmf。pud = pud_alloc(mm, p4d, address);
……
//查詢頁中級目錄表項,沒有則建立
vmf。pmd = pmd_alloc(mm, vmf。pud, address);
……
//處理pte頁表
return handle_pte_fault(&vmf);
}
do_anonymous_page
匿名頁缺頁異常,對於匿名對映,對映完成之後,只是獲得了一塊虛擬記憶體,並沒有分配物理記憶體,當第一次訪問的時候:
如果是讀訪問,會將虛擬頁對映到0頁,以減少不必要的記憶體分配
如果是寫訪問,用alloc_zeroed_user_highpage_movable分配新的物理頁,並用0填充,然後對映到虛擬頁上去
如果是先讀後寫訪問,則會發生兩次缺頁異常:第一次是匿名頁缺頁異常的讀的處理(虛擬頁到0頁的對映),第二次是寫時複製缺頁異常處理。
從上面的總結我們知道,第一次訪問匿名頁時有三種情況,其中第一種和第三種情況都會涉及到0頁。
do_fault
do_swap_page
上面已經講過,pte對應的內容不為0(頁表項存在),但是pte所對應的page不在記憶體中時,表示此時pte的內容所對應的頁面在swap空間中,缺頁異常時會透過do_swap_page()函式來分配頁面。
do_swap_page發生在swap in的時候,即查詢磁碟上的slot,並將資料讀回。
換入的過程如下:
查詢swap cache中是否存在所查詢的頁面,如果存在,則根據swap cache引用的記憶體頁,重新對映並更新頁表;如果不存在,則分配新的記憶體頁,並新增到swap cache的引用中,更新記憶體頁內容完成後,更新頁表。
換入操作結束後,對應swap area的頁引用減1,當減少到0時,代表沒有任何程序引用了該頁,可以進行回收。
int do_swap_page(struct vm_fault *vmf)
{
……
//根據pte找到swap entry, swap entry和pte有一個對應關係
entry = pte_to_swp_entry(vmf->orig_pte);
……
if (!page)
//根據entry從swap快取中查詢頁, 在swapcache裡面尋找entry對應的page
//Lookup a swap entry in the swap cache
page = lookup_swap_cache(entry, vma_readahead ? vma : NULL,
vmf->address);
//沒有找到頁
if (!page) {
if (vma_readahead)
page = do_swap_page_readahead(entry,
GFP_HIGHUSER_MOVABLE, vmf, &swap_ra);
else
//如果swapcache裡面找不到就在swap area裡面找,分配新的記憶體頁並從swap area中讀入
page = swapin_readahead(entry,
GFP_HIGHUSER_MOVABLE, vma, vmf->address);
……
//獲取一個pte的entry,重新建立對映
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);
……
//anonpage數加1,匿名頁從swap空間交換出來,所以加1
//swap page個數減1,由page和VMA屬性建立一個新的pte
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);
pte = mk_pte(page, vma->vm_page_prot);
……
flush_icache_page(vma, page);
if (pte_swp_soft_dirty(vmf->orig_pte))
pte = pte_mksoft_dirty(pte);
//將新生成的PTE entry新增到硬體頁表中
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
vmf->orig_pte = pte;
//根據page是否為swapcache
if (page == swapcache) {
//如果是,將swap快取頁用作anon頁,新增反向對映rmap中
do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
mem_cgroup_commit_charge(page, memcg, true, false);
//並新增到active連結串列中
activate_page(page);
//如果不是
} else { /* ksm created a completely new copy */
//使用新頁面並複製swap快取頁,新增反向對映rmap中
page_add_new_anon_rmap(page, vma, vmf->address, false);
mem_cgroup_commit_charge(page, memcg, false, false);
//並新增到lru連結串列中
lru_cache_add_active_or_unevictable(page, vma);
}
//釋放swap entry
swap_free(entry);
……
if (vmf->flags & FAULT_FLAG_WRITE) {
//有寫請求則寫時複製
ret |= do_wp_page(vmf);
if (ret & VM_FAULT_ERROR)
ret &= VM_FAULT_ERROR;
goto out;
}
……
return ret;
}
do_wp_page
走到這裡說明頁面在記憶體中,只是PTE只有讀許可權,而又要寫記憶體的時候就會觸發do_wp_page。
do_wp_page函式用於處理寫時複製(copy on write),其流程比較簡單,主要是分配新的物理頁,複製原來頁的內容到新頁,然後修改頁表項內容指向新頁並修改為可寫(vma具備可寫屬性)。
static int do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
//從頁表項中得到頁幀號,再得到頁描述符,發生異常時地址所在的page結構
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (!vmf->page) {
//沒有page結構是使用頁幀號的特殊對映
/*
* VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
* VM_PFNMAP VMA。
*
* We should not cow pages in a shared writeable mapping。
* Just mark the pages writable and/or call ops->pfn_mkwrite。
*/
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
//處理共享可寫對映
return wp_pfn_shared(vmf);
pte_unmap_unlock(vmf->pte, vmf->ptl);
//處理私有可寫對映
return wp_page_copy(vmf);
}
/*
* Take out anonymous pages first, anonymous shared vmas are
* not dirty accountable。
*/
if (PageAnon(vmf->page) && !PageKsm(vmf->page)) {
int total_map_swapcount;
if (!trylock_page(vmf->page)) {
//新增原來頁的引用計數,方式被釋放
get_page(vmf->page);
//釋放頁表鎖
pte_unmap_unlock(vmf->pte, vmf->ptl);
lock_page(vmf->page);
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!pte_same(*vmf->pte, vmf->orig_pte)) {
unlock_page(vmf->page);
pte_unmap_unlock(vmf->pte, vmf->ptl);
put_page(vmf->page);
return 0;
}
put_page(vmf->page);
}
//單身匿名頁面的處理
if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
if (total_map_swapcount == 1) {
/*
* The page is all ours。 Move it to
* our anon_vma so the rmap code will
* not search our parent or siblings。
* Protected against the rmap code by
* the page lock。
*/
page_move_anon_rmap(vmf->page, vma);
}
unlock_page(vmf->page);
wp_page_reuse(vmf);
return VM_FAULT_WRITE;
}
unlock_page(vmf->page);
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
//共享可寫,不需要複製物理頁,設定頁表許可權即可
return wp_page_shared(vmf);
}
/*
* Ok, we need to copy。 Oh, well。。
*/
get_page(vmf->page);
pte_unmap_unlock(vmf->pte, vmf->ptl);
//私有可寫,複製物理頁,將虛擬頁對映到物理頁
return wp_page_copy(vmf);
}
Linux 記憶體管理之CMA
CMA是reserved的一塊記憶體,用於分配連續的大塊記憶體。當裝置驅動不用時,記憶體管理系統將該區域用於分配和管理可移動型別頁面;當裝置驅動使用時,此時已經分配的頁面需要進行遷移,又用於連續記憶體分配;其用法與DMA子系統結合在一起充當DMA的後端,具體可參考《沒有IOMMU的DMA操作》。
CMA區域 cma_areas 的建立
CMA區域的建立有兩種方法,一種是透過dts的reserved memory,另外一種是透過command line引數和核心配置引數。
dts方式:
reserved-memory {
/* global autoconfigured region for contiguous allocations */
linux,cma {
compatible = “shared-dma-pool”;
reusable;
size = <0 0x28000000>;
alloc-ranges = <0 0xa0000000 0 0x40000000>;
linux,cma-default;
};
};
device tree中可以包含reserved-memory node,系統啟動的時候會開啟rmem_cma_setup
RESERVEDMEM_OF_DECLARE(cma, “shared-dma-pool”, rmem_cma_setup);
command line方式:cma=nn[MG]@[start[MG][-end[MG]]]
static int __init early_cma(char *p)
{
pr_debug(“%s(%s)\n”, __func__, p);
size_cmdline = memparse(p, &p);
if (*p != ‘@’) {
/*
if base and limit are not assigned,
set limit to high memory bondary to use low memory。
*/
limit_cmdline = __pa(high_memory);
return 0;
}
base_cmdline = memparse(p + 1, &p);
if (*p != ‘-’) {
limit_cmdline = base_cmdline + size_cmdline;
return 0;
}
limit_cmdline = memparse(p + 1, &p);
return 0;
}
early_param(“cma”, early_cma);
系統在啟動的過程中會把cmdline裡的nn, start, end傳給函式dma_contiguous_reserve,流程如下:
setup_arch——->arm64_memblock_init——->dma_contiguous_reserve->dma_contiguous_reserve_area->cma_declare_contiguous
將CMA區域新增到Buddy System
為了避免這塊reserved的記憶體在不用時候的浪費,記憶體管理模組會將CMA區域新增到Buddy System中,用於可移動頁面的分配和管理。CMA區域是透過cma_init_reserved_areas介面來新增到Buddy System中的。
static int __init cma_init_reserved_areas(void)
{
int i;
for (i = 0; i < cma_area_count; i++) {
int ret = cma_activate_area(&cma_areas[i]);
if (ret)
return ret;
}
return 0;
}
core_initcall(cma_init_reserved_areas);
其實現比較簡單,主要分為兩步:
把該頁面設定為MIGRATE_CMA標誌
透過__free_pages將頁面新增到buddy system中
CMA分配
《沒有IOMMU的DMA操作》裡講過,CMA是透過cma_alloc分配的。cma_alloc->alloc_contig_range(。。。, MIGRATE_CMA,。。。),向剛才釋放給buddy system的MIGRATE_CMA型別頁面,重新“收集”過來。
用CMA的時候有一點需要注意:
也就是上圖中黃色部分的判斷。CMA記憶體在分配過程是一個比較“重”的操作,可能涉及頁面遷移、頁面回收等操作,因此不適合用於atomic context。比如之前遇到過一個問題,當記憶體不足的情況下,向隨身碟寫資料的同時操作介面會出現卡頓的現象,這是因為CMA在遷移的過程中需要等待當前頁面中的資料回寫到隨身碟之後,才會進一步的規整為連續記憶體供gpu/display使用,從而出現卡頓的現象。
總結
至此,從CPU開始訪問記憶體,到物理頁的劃分,再到核心頁框分配器的實現,以及slab分配器的實現,最後到CMA等連續記憶體的使用,把Linux記憶體管理的知識串了起來,算是形成了整個閉環。相信如果掌握了本篇內容,肯定打開了Linux核心的大門,有了這個基石,接下來的核心學習會越來越輕鬆。
文章轉載至萬字整理,肝翻Linux記憶體管理所有知識點