一文幫你搞定Linux內(nèi)核分析-內(nèi)存管理詳解圖(秒懂)
一、內(nèi)核空間
1、頁
頁(page)是內(nèi)核的內(nèi)存管理基本單位。(linux/mm_types.h)
flags:頁標(biāo)志包含是不是臟的,是否被鎖定等等,每一位單獨(dú)表示一種狀態(tài),可同時表示出32種不同狀態(tài),定義在<linux/page-flags.h>
_count:計數(shù)值為-1表示未被使用。
virtual:頁在虛擬內(nèi)存中的地址,對于不能永久映射到內(nèi)核空間的內(nèi)存(比如高端內(nèi)存),該值為NULL;需要事必須動態(tài)映射這些內(nèi)存。
盡管處理器的最小可尋址單位通常為字或字節(jié),但內(nèi)存管理單元(MMU,把虛擬地址轉(zhuǎn)換為物理地址的硬件設(shè)備)通常以頁為單位處理。內(nèi)核用struct page結(jié)構(gòu)體表示每個物理頁,struct page結(jié)構(gòu)體占40個字節(jié),假定系統(tǒng)物理頁大小為4KB,對于4GB物理內(nèi)存,1M個頁面,故所有的頁面page結(jié)構(gòu)體共占有內(nèi)存大小為40MB,相對系統(tǒng)4G,這個代價并不高。
2、區(qū)(zone)
內(nèi)核把頁劃分在不同的區(qū)(zone)
總共3個區(qū),具體如下:

執(zhí)行DMA操作的內(nèi)存必須從ZONE_DMA區(qū)分配
一般內(nèi)存,既可從ZONE_DMA,也可從ZONE_NORMAL分配,但不能同時從兩個區(qū)分配;
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)?
?

3、頁分配與釋放
下面列舉所有的頁為單位進(jìn)行連續(xù)物理內(nèi)存分配,也稱為低級頁分配器:

get_zeroed_page:對于用戶空間,這個方法能保障系統(tǒng)敏感數(shù)據(jù)不會泄露
page_address: 把給定的頁轉(zhuǎn)換成邏輯地址

4、字節(jié)分配與釋放
kmalloc,vmalloc分配都是以字節(jié)為單位
(1) kmalloc
該函數(shù)返回的是一個指向內(nèi)存塊的指針,其內(nèi)存塊大小至少為size,所分配的內(nèi)存在物理內(nèi)存中連續(xù)且保持原有的數(shù)據(jù)(不清零)
其中部分flags取值說明:
GFP_USER: 用于用戶空間的分配內(nèi)存,可能休眠;
GFP_KERNEL:用于內(nèi)核空間的內(nèi)存分配,可能休眠;
GFP_ATOMIC:用于原子性的內(nèi)存分配,不會休眠;典型原子性場景有中斷處理程序,軟中斷,tasklet等
kmalloc內(nèi)存分配最終總是調(diào)用__get_free_pages 來進(jìn)行實(shí)際的分配,故前綴都是GFP_開頭。 kmalloc分最多只能分配32個page大小的內(nèi)存,每個page=4k,也就是128K大小,其中16個字節(jié)用來記錄頁描述結(jié)構(gòu)。
kmalloc分配的是常駐內(nèi)存,不會被交換到文件中。最小分配單位是32或64字節(jié)。
kzalloc
kzalloc()等價于先用 kmalloc() 申請空間, 再用memset()來初始化,所有申請的元素都被初始化為0。
vmalloc
該函數(shù)返回的是一個指向內(nèi)存塊的指針,其內(nèi)存塊大小至少為size,所分配的內(nèi)存是邏輯上連續(xù)的。kmalloc不同,該函數(shù)乜有flags,默認(rèn)是可以休眠的。
小結(jié):

編輯切換為居中
5、slab層 slab分配器的作用:
對于頻繁地分配和釋放的數(shù)據(jù)結(jié)構(gòu),會緩存它;
頻繁分配和回收比如導(dǎo)致內(nèi)存碎片,為了避免,空閑鏈表的緩存會連續(xù)的存放,已釋放的數(shù)據(jù)結(jié)構(gòu)又會放回空閑鏈表,不會導(dǎo)致碎片;
讓部分緩存專屬單個處理器,分配和釋放操作可以不加SMP鎖;
slab層把不同的對象劃分為高速緩存組,每個高速緩存組都存放不同類型的對象,每個對象類型對應(yīng)一個高速緩存。kmalloc接口監(jiān)理在slab層只是,使用一組通用高速緩存。 每個高速緩存都是用kmem_cache結(jié)構(gòu)來表示
kmem_cache_crreate:創(chuàng)建高速緩存
kmem_cache_destroy: 撤銷高速緩存
kmem_cache_alloc: 從高速緩存中返回一個指向?qū)ο蟮闹羔?/p>
kmem_cache_free:釋放一個對象
實(shí)例分析: 內(nèi)核初始化期間,/kernel/fork.c的fork_init()中會創(chuàng)建一個名叫task_struct的高速緩存; 每當(dāng)進(jìn)程調(diào)用fork()時,會通過dup_task_struct()創(chuàng)建一個新的進(jìn)程描述符,并調(diào)用do_fork(),完成從高速緩存中獲取對象。
6、棧的靜態(tài)分配
當(dāng)設(shè)置單頁內(nèi)核棧,那么每個進(jìn)程的內(nèi)核棧只有一頁大小,這取決于編譯時配置選項(xiàng)。 好處:
可以減少每個進(jìn)程內(nèi)存的消耗;
隨著機(jī)器運(yùn)行時間的增加,尋找兩個未分配的、連續(xù)的頁越來越困難,物理內(nèi)存碎片化不斷加重,那么給每個新進(jìn)程分配虛擬內(nèi)存的壓力也增大;
每個進(jìn)程的調(diào)用鏈在自己的內(nèi)核棧中,當(dāng)單頁棧選項(xiàng)被激活時,中斷處理程序可獲得自己的棧;
任意函數(shù)必須盡量節(jié)省棧資源, 方法就是所有函數(shù)讓局部變量所占空間之和不要超過幾百字節(jié)。
7、高端內(nèi)存的映射
高端內(nèi)存中的頁不能永久地映射到內(nèi)核地址空間。
1.kmap:把給定page結(jié)構(gòu)映射到內(nèi)核地址空間;
當(dāng)page位于低端內(nèi)存,函數(shù)返回該頁的虛擬地址
當(dāng)page位于高端內(nèi)存,建立一個永久映射,再返回地址
2.kunmap: 永久映射的數(shù)量有限,應(yīng)通過kunmap及時解除映射
3.kmap_atomic: 臨時映射
4.kunmap_atomic: 解除臨時映射
8、每個CPU數(shù)據(jù)
alloc_percpu: 給系統(tǒng)的每個處理器分配一個指定類型對象的實(shí)例,以單字節(jié)對齊;
free_percpu: 釋放每個處理器的對象實(shí)例;
get_cpu_var: 返回一個執(zhí)行當(dāng)前處理器數(shù)據(jù)的特殊實(shí)例,同時會禁止內(nèi)核搶占
put_cpu_var: 會重新激活內(nèi)核搶占
使用每個CPU數(shù)據(jù)好處:
減少了數(shù)據(jù)鎖定,每個CPU訪問自己CPU數(shù)據(jù)
大大減少緩存失效,失效往往發(fā)生在一個處理器操作某個數(shù)據(jù),而其他處理器緩存了該數(shù)據(jù),那么必須清理或刷新緩存。持續(xù)不斷的緩存失效稱為緩存抖動。
9、小結(jié)
分配函數(shù)選擇:
連續(xù)的物理頁,使用低級頁分配器 或kmalloc();
高端內(nèi)存分配,使用alloc_pages(),返回page結(jié)構(gòu)指針; 想獲取地址指針,應(yīng)使用kmap(),把高端內(nèi)存映射到內(nèi)核的邏輯地址空間;
僅僅需要虛擬地址連續(xù)頁,使用vmalloc(),性能有所損失;
頻繁創(chuàng)建和撤銷大量數(shù)據(jù)結(jié)構(gòu),考慮建立slab高速緩存。
二、用戶空間
用戶空間中進(jìn)程的內(nèi)存,往往稱為進(jìn)程地址空間。Linux采用虛擬內(nèi)存技術(shù)。
1、地址空間
每個進(jìn)程都有一個32位或64位的地址空間,取決于體系結(jié)構(gòu)。 一個進(jìn)程的地址空間與另一個進(jìn)程的地址空間即使有相同的內(nèi)存地址,也彼此互不相干,對于這種共享地址空間的進(jìn)程稱之為線程。一個進(jìn)程可尋址4GB的虛擬內(nèi)存(32位地址空間中),但不是所有虛擬地址都有權(quán)訪問。對于進(jìn)程可訪問的地址空間稱為內(nèi)存區(qū)域。每個內(nèi)存區(qū)域都具有對相關(guān)進(jìn)程的可讀、可寫、可執(zhí)行屬性等相關(guān)權(quán)限設(shè)置。
內(nèi)存區(qū)域可包含的對象:
代碼段(text section): 可執(zhí)行文件代碼
數(shù)據(jù)段(data section): 可執(zhí)行文件的已初始化全局變量(靜態(tài)分配的變量和全局變量)。
bss段:程序中未初始化的全局變量,零頁映射(頁面的信息全部為0值)。
進(jìn)程用戶空間棧的零頁映射(進(jìn)程的內(nèi)核棧獨(dú)立存在并由內(nèi)核維護(hù))
每一個諸如C庫或動態(tài)連接程序等共享庫的代碼段、數(shù)據(jù)段和bss也會被載入進(jìn)程的地址空間
任何內(nèi)存映射文件
任何共享內(nèi)存段
任何匿名的內(nèi)存映射(比如由malloc()分配的內(nèi)存)
這些內(nèi)存區(qū)域不能相互覆蓋,每一個進(jìn)程都有不同的內(nèi)存片段。
2、內(nèi)存描述符
內(nèi)存描述符由mm_struct結(jié)構(gòu)體表示,(linux/sched.h)

mm_users:代表正在使用該地址的進(jìn)程數(shù)目,當(dāng)該值為0時mm_count也變?yōu)?;
mm_count: 代表mm_struct的主引用計數(shù),當(dāng)該值為0說明沒有任何指向該mm_struct結(jié)構(gòu)體的引用,結(jié)構(gòu)體會被撤銷。
mmap和mm_rb:描述的對象都是相同的
mmap以鏈表形式存放, 利于高效地遍歷所有元素
mm_rb以紅黑樹形式存放,適合搜索指定元素
mmlist:所有的mm_struct結(jié)構(gòu)體都通過mmlist連接在一個雙向鏈表中,該鏈表的首元素是init_mm內(nèi)存描述符,它代表init進(jìn)程的地址空間。
在進(jìn)程的進(jìn)程描述符(<linux/sched.h>中定義的task_struct結(jié)構(gòu)體)中,mm域記錄該進(jìn)程使用的內(nèi)存描述符。故current->mm代表當(dāng)前進(jìn)程的內(nèi)存描述符。
fork()函數(shù) 利用copy_mm函數(shù)復(fù)制父進(jìn)程的內(nèi)存描述符,子進(jìn)程中的mm_struct結(jié)構(gòu)體通過allcote_mm()從高速緩存中分配得到。通常,每個進(jìn)程都有唯一的mm_struct結(jié)構(gòu)體,即唯一的進(jìn)程地址空間。
當(dāng)子進(jìn)程與父進(jìn)程是共享地址空間,可調(diào)用clone(),那么不再調(diào)用allcote_mm(),而是僅僅是將mm域指向父進(jìn)程的mm,即 tsk->mm = current->mm。
相反地,撤銷內(nèi)存是exit_mm()函數(shù),該函數(shù)會進(jìn)行常規(guī)的撤銷工作,更新一些統(tǒng)計量。
3、內(nèi)核線程
沒有進(jìn)程地址空間,即內(nèi)核線程對應(yīng)的進(jìn)程描述符中mm=NULL
內(nèi)核線程直接使用前一個進(jìn)程的內(nèi)存描述符,僅僅使用地址空間中和內(nèi)核內(nèi)存相關(guān)的信息
4、虛擬內(nèi)存區(qū)域(VMA)
虛擬內(nèi)存區(qū)域由vm_area_struct結(jié)構(gòu)體描述, 指定地址空間內(nèi)連續(xù)區(qū)間的一個獨(dú)立內(nèi)存范圍。 每個VMA代表不同類型的內(nèi)存區(qū)域。linux/mm_types.h
每個內(nèi)存描述符對應(yīng)于進(jìn)程地址空間的唯一區(qū)間,vm_end - vm_start便是內(nèi)存區(qū)間的長度。
5、VMA操作
6、查看進(jìn)程內(nèi)存空間
每行數(shù)據(jù)格式: 開始-結(jié)束 訪問權(quán)限 偏移 主設(shè)備號:次設(shè)備號 i節(jié)點(diǎn) 文件
設(shè)備表示為00:00, 索引節(jié)點(diǎn)標(biāo)示頁為0,這個區(qū)域就是零頁(所有數(shù)據(jù)全為零)
數(shù)據(jù)段和bss具有可讀、可寫但不可執(zhí)行權(quán)限;而堆??勺x、可寫、甚至可執(zhí)行
也可通過工具pmap
7、內(nèi)存區(qū)域操作
find_vma 查看mm_struct所屬于的VMA,搜索第一個vm_end大于addr的內(nèi)存區(qū)域
檢查mmap_cache,查看緩存VMA是否包含所需地址,如果沒有找到,進(jìn)入2
通過紅黑樹搜索;
find_vma_prev 查看mm_struct所屬于的VMA,搜索第一個vm_end小于addr的內(nèi)存區(qū)域
struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev)mmap
內(nèi)核使用do_mmap()創(chuàng)建一個新的線性地址區(qū)間,如果創(chuàng)建的地址區(qū)間和一個已存在的相鄰地址區(qū)間有相同的訪問權(quán)限,則將兩個區(qū)間合并為一個。
mmap()系統(tǒng)調(diào)用獲取內(nèi)核函數(shù)do_mmap()的功能。
do_mummap()從特定的進(jìn)程地址空間中刪除指定地址區(qū)間
mummap()與 mmap功能相反。
頁表
應(yīng)用程序操作的對象時映射到物理內(nèi)存之上的虛擬內(nèi)存,而處理器直接操作的是物理內(nèi)存。故應(yīng)用程序訪問一個虛擬地址時,需要將虛擬地址轉(zhuǎn)換為物理地址,然后處理器才能解析地址訪問請求,這個轉(zhuǎn)換工作通過查詢頁表完成。
Linux使用三級頁表完成地址轉(zhuǎn)換。

頂級頁表:頁全局目錄(PGD),指向二級頁目錄;
二級頁表:中間頁目錄(PMD),指向PTE中的表項(xiàng);
最后一級:頁表(PTE),指向物理頁面。
多數(shù)體系結(jié)構(gòu),搜索頁表工作由硬件完成。每個進(jìn)程都有自己的頁表(線程會共享頁表)。為了加快搜索,實(shí)現(xiàn)了翻譯后緩沖器(TLB),作為將虛擬地址映射到物理地址的硬件緩存。還有寫時拷貝方式共享頁表,當(dāng)fork()時,父子進(jìn)程共享頁表,只有當(dāng)子進(jìn)程或父進(jìn)程試圖修改特定頁表項(xiàng)時,內(nèi)核才創(chuàng)建該頁表項(xiàng)的新拷貝,之后父子進(jìn)程不再共享該頁表項(xiàng)??梢?,利用共享頁表可以消除fork()操作中頁表拷貝所帶來的消耗。
進(jìn)程與內(nèi)存
所有進(jìn)程都必須占用一定數(shù)量的內(nèi)存,這些內(nèi)存用來存放從磁盤載入的程序代碼,或存放來自用戶輸入的數(shù)據(jù)等。內(nèi)存可以提前靜態(tài)分配和統(tǒng)一回收,也可以按需動態(tài)分配和回收。
對于普通進(jìn)程對應(yīng)的內(nèi)存空間包含5種不同的數(shù)據(jù)區(qū):
代碼段
數(shù)據(jù)段
BSS段
堆:動態(tài)分配的內(nèi)存段,大小不固定,可動態(tài)擴(kuò)張(malloc等函數(shù)分配內(nèi)存),或動態(tài)縮減(free等函數(shù)釋放);
棧:存放臨時創(chuàng)建的局部變量;

進(jìn)程內(nèi)存空間
Linux采用虛擬內(nèi)存管理技術(shù),每個進(jìn)程都有各自獨(dú)立的進(jìn)程地址空間(即4G的線性虛擬空間),無法直接訪問物理內(nèi)存。這樣起到保護(hù)操作系統(tǒng),并且讓用戶程序可使用比實(shí)際物理內(nèi)存更大的地址空間。
4G進(jìn)程地址空間被劃分兩部分,內(nèi)核空間和用戶空間。用戶空間從0到3G,內(nèi)核空間從3G到4G;
用戶進(jìn)程通常情況只能訪問用戶空間的虛擬地址,不能訪問內(nèi)核空間虛擬地址。只有用戶進(jìn)程進(jìn)行系統(tǒng)調(diào)用(代表用戶進(jìn)程在內(nèi)核態(tài)執(zhí)行)等情況可訪問到內(nèi)核空間;
用戶空間對應(yīng)進(jìn)程,所以當(dāng)進(jìn)程切換,用戶空間也會跟著變化;
內(nèi)核空間是由內(nèi)核負(fù)責(zé)映射,不會跟著進(jìn)程變化;內(nèi)核空間地址有自己對應(yīng)的頁表,用戶進(jìn)程各自有不同額頁表。

內(nèi)存分配
進(jìn)程分配內(nèi)存,陷入內(nèi)核態(tài)分別由brk和mmap完成,但這兩種分配還沒有分配真正的物理內(nèi)存,真正分配在后面會講。
brk: 數(shù)據(jù)段的最高地址指針_edata往高地址推
當(dāng)malloc需要分配的內(nèi)存<M_MMAP_THRESHOLD(默認(rèn)128k)時,采用brk;
brk分配的內(nèi)存需高地址內(nèi)存全部釋放之后才會釋放。(由于是通過推動指針方式)
當(dāng)最高地址空間的空閑內(nèi)存大于M_TRIM_THRESHOLD時(默認(rèn)128k),執(zhí)行內(nèi)存緊縮操作;
do_mmap:在堆棧中間的文件映射區(qū)域找空閑的虛擬內(nèi)存
當(dāng)malloc需要分配的內(nèi)存>M_MMAP_THRESHOLD(默認(rèn)128k)時,采用do_map();
mmap分配的內(nèi)存可以單獨(dú)釋放
8、物理內(nèi)存
物理內(nèi)存只有進(jìn)程真正去訪問虛擬地址,發(fā)生缺頁中斷時,才分配實(shí)際的物理頁面,建立物理內(nèi)存和虛擬內(nèi)存的映射關(guān)系。
應(yīng)用程序操作的是虛擬內(nèi)存;而處理器直接操作的卻是物理內(nèi)存。當(dāng)應(yīng)用程序訪問虛擬地址,必須將虛擬地址轉(zhuǎn)化為物理地址,處理器才能解析地址訪問請求。
物理內(nèi)存是通過分頁機(jī)制實(shí)現(xiàn)的
物理頁在系統(tǒng)中由也結(jié)構(gòu)struct page描述,所有的page都存儲在數(shù)組mem_map[]中,可通過該數(shù)組找到系統(tǒng)中的每一頁。
9、虛擬內(nèi)存 轉(zhuǎn)化為 真實(shí)物理內(nèi)存:
虛擬進(jìn)程空間:通過查詢進(jìn)程頁表,獲取實(shí)際物理內(nèi)存地址;
虛擬內(nèi)核空間:通過查詢內(nèi)核頁表,獲取實(shí)際物理內(nèi)存地址;
物理內(nèi)存映射區(qū):物理內(nèi)存映射區(qū)與實(shí)際物理去偏移量僅PAGE_OFFSET,通過通過virt_to_phys()轉(zhuǎn)化;
虛擬內(nèi)存與真實(shí)物理內(nèi)存映射關(guān)系:

其中物理地址空間中除了896M(ZONE_DMA + ZONE_NORMAL)的區(qū)域是絕對的物理連續(xù),其他內(nèi)存都不是物理內(nèi)存連續(xù)。在虛擬內(nèi)核地址空間中的安全保護(hù)區(qū)域的指針都是非法的,用于保證指針非法越界類的操作,vm_struct是連續(xù)的虛擬內(nèi)核空間,對應(yīng)的物理頁面可以不連續(xù),地址范圍(3G + 896M + 8M) ~ 4G;另外在虛擬用戶空間中 vm_area_struct同樣也是一塊連續(xù)的虛擬進(jìn)程空間,地址空間范圍0~3G。
碎片問題
外部碎片:未被分配的內(nèi)存,由于太多零碎的不連續(xù)小內(nèi)存,無法滿足當(dāng)前較大內(nèi)存的申請要求;
原因:頻繁的分配與回收物理頁導(dǎo)致大量的小塊內(nèi)存夾雜在已分配頁面中間;
解決方案:伙伴算法有所改善
內(nèi)部碎片:已經(jīng)分配的內(nèi)存,卻不能被利用的內(nèi)存空間;
緣由:所有內(nèi)存分配必須起始可被4、8或16(體系結(jié)構(gòu)決定)整除的地址或者M(jìn)MU分頁機(jī)制限制;
解決方案:slab分配器有所改善
實(shí)例:請求一個11Byte的內(nèi)存塊,系統(tǒng)可能會分配12Byte、16Byte等稍大一些的字節(jié),這些多余空間就產(chǎn)生碎片
整體內(nèi)存圖總結(jié)如下:

