在Linux內(nèi)核中,內(nèi)存管理是整個(gè)系統(tǒng)穩(wěn)定運(yùn)行的基石,而伙伴系統(tǒng)(Buddy System)作為內(nèi)核物理內(nèi)存分配的核心機(jī)制,更是驅(qū)動(dòng)開發(fā)、內(nèi)核模塊開發(fā)的必備知識(shí)點(diǎn)。它通過"2的冪次分配粒度"巧妙解決了外碎片問題,而我們申請(qǐng)內(nèi)核內(nèi)存的所有操作,最終都要通過伙伴系統(tǒng)提供的核心函數(shù)來完成。
今天這篇文章,我們就來全面拆解伙伴系統(tǒng)的內(nèi)存申請(qǐng)函數(shù):從底層核心到上層封裝,從參數(shù)解析到實(shí)戰(zhàn)示例,再到可視化流程,幫你徹底搞懂"內(nèi)核內(nèi)存怎么申請(qǐng)"。
一、前言:為什么要關(guān)注伙伴系統(tǒng)?
內(nèi)核內(nèi)存和用戶態(tài)內(nèi)存完全是兩套管理體系:用戶態(tài)有malloc/free,但內(nèi)核態(tài)不能直接用——內(nèi)核需要更高效、更安全的內(nèi)存分配方式,而伙伴系統(tǒng)就是為此而生。
?解決外碎片:傳統(tǒng)連續(xù)分配會(huì)產(chǎn)生大量"無法利用的小空閑塊",伙伴系統(tǒng)通過固定2^order的分配粒度,讓空閑塊可拆分、可合并,從根源減少外碎片;
?支撐內(nèi)核核心功能:進(jìn)程棧、內(nèi)核模塊、設(shè)備緩沖區(qū)等所有內(nèi)核態(tài)內(nèi)存需求,都依賴伙伴系統(tǒng)分配;
?開發(fā)必備技能:驅(qū)動(dòng)或內(nèi)核模塊中,只要涉及內(nèi)存操作,就必須掌握伙伴系統(tǒng)的申請(qǐng)/釋放函數(shù)。
在講函數(shù)之前,先快速回顧下伙伴系統(tǒng)的核心原理,幫你建立認(rèn)知基礎(chǔ)。
二、伙伴系統(tǒng)核心原理速覽
伙伴系統(tǒng)的設(shè)計(jì)思想非常簡(jiǎn)潔,核心圍繞3個(gè)關(guān)鍵點(diǎn):
1.分配粒度:物理內(nèi)存被劃分為"頁塊",塊大小必須是2^order個(gè)物理頁(order稱為"分配階")。比如order=0對(duì)應(yīng)1頁,order=1對(duì)應(yīng)2頁,order=3對(duì)應(yīng)8頁,最大order由MAX_ORDER定義(默認(rèn)11,即最大2048頁= 8MB);
2.伙伴塊定義:兩個(gè)大小相同、物理地址連續(xù)、且來自同一父塊的頁塊,互為"伙伴"。比如order=1的塊(2頁)拆分后,會(huì)生成兩個(gè)order=0的伙伴塊;
3.分配/釋放邏輯:
?分配:先找對(duì)應(yīng)order的空閑塊,找到直接分配;找不到就拆分更高order的空閑塊,直到得到目標(biāo)大小;
?釋放:釋放的塊會(huì)檢查是否有空閑伙伴,若有則合并為更高order的塊,逐步歸還到空閑鏈表。
理解了這3點(diǎn),再看后續(xù)的函數(shù)就會(huì)豁然開朗——所有申請(qǐng)函數(shù)的本質(zhì),都是向伙伴系統(tǒng)請(qǐng)求"指定order的連續(xù)物理頁塊"。
三、伙伴系統(tǒng)核心申請(qǐng)函數(shù)詳解
伙伴系統(tǒng)提供了一套"底層核心+上層封裝"的函數(shù)體系,不同函數(shù)適用于不同場(chǎng)景。我們從底層到上層逐一拆解:
3.1底層核心:__alloc_pages ()
__alloc_pages()是伙伴系統(tǒng)最底層的內(nèi)存申請(qǐng)函數(shù),所有其他申請(qǐng)函數(shù)最終都會(huì)調(diào)用它??梢哉f,它是"內(nèi)核內(nèi)存分配的入口"。
函數(shù)原型
structpage*__alloc_pages(gfp_tgfp_mask,unsignedintorder);
核心作用
直接向伙伴系統(tǒng)申請(qǐng)2^order個(gè)連續(xù)物理頁,返回對(duì)應(yīng)物理頁的struct page結(jié)構(gòu)體指針(注意:返回的是物理頁描述符,不是虛擬地址)。
參數(shù)解析
|
參數(shù)名
|
作用說明
|
|
gfp_mask
|
分配策略標(biāo)志(核心參數(shù)?。?,告訴內(nèi)核"怎么分配內(nèi)存"(能否睡眠、用哪種內(nèi)存域等)
|
|
order
|
分配階(0≤order
|
關(guān)鍵補(bǔ)充:gfp_mask常用取值
gfp_mask是內(nèi)核內(nèi)存分配的"策略開關(guān)",不同場(chǎng)景必須選對(duì),否則會(huì)導(dǎo)致系統(tǒng)異常:
?GFP_KERNEL:最常用,允許睡眠(可觸發(fā)頁回收),適用于進(jìn)程上下文(比如驅(qū)動(dòng)的probe函數(shù)、內(nèi)核線程);
?GFP_ATOMIC:不允許睡眠、不允許觸發(fā)頁回收,適用于中斷上下文(比如中斷處理函數(shù));
?GFP_DMA:僅從DMA內(nèi)存域分配(適用于需要DMA傳輸?shù)脑O(shè)備緩沖區(qū));
?GFP_HIGHUSER:允許從高端內(nèi)存分配(適用于大內(nèi)存場(chǎng)景)。
返回值
?成功:返回第一個(gè)物理頁的struct page指針;
?失?。悍祷?/span>NULL(表示沒有找到滿足條件的連續(xù)物理頁)。
特點(diǎn)
?底層裸函數(shù),沒有參數(shù)合法性檢查(比如order超過MAX_ORDER也會(huì)嘗試分配);
?不建議直接調(diào)用(風(fēng)險(xiǎn)高),僅內(nèi)核核心代碼使用;
?返回的是page結(jié)構(gòu)體,需要手動(dòng)轉(zhuǎn)換為虛擬地址才能訪問(用page_to_virt())。
3.2常用封裝:alloc_pages ()
alloc_pages()是對(duì)__alloc_pages()的上層封裝,也是驅(qū)動(dòng)開發(fā)中最常用的"page級(jí)分配函數(shù)"。
函數(shù)原型
structpage*alloc_pages(gfp_tgfp_mask,unsignedintorder);
核心作用
與__alloc_pages()功能一致,但增加了參數(shù)合法性檢查,更安全。
與__alloc_pages ()的區(qū)別
|
特性
|
__alloc_pages()
|
alloc_pages()
|
|
參數(shù)檢查
|
無
|
有(比如檢查order范圍)
|
|
適用場(chǎng)景
|
內(nèi)核核心代碼
|
驅(qū)動(dòng)/內(nèi)核模塊開發(fā)
|
|
安全性
|
低
|
高
|
適用場(chǎng)景
需要直接操作struct page結(jié)構(gòu)體的場(chǎng)景:
?設(shè)置頁屬性(比如標(biāo)記為只讀、可緩存);
?映射高端內(nèi)存(高端內(nèi)存無法直接訪問,需要通過page結(jié)構(gòu)體建立映射);
?管理物理頁的引用計(jì)數(shù)。
3.3虛擬地址直達(dá):__get_free_pages ()
如果不需要操作page結(jié)構(gòu)體,只想直接獲取可訪問的虛擬地址,__get_free_pages()是最優(yōu)選擇——它幫我們完成了"申請(qǐng)page +轉(zhuǎn)換虛擬地址"的全過程。
函數(shù)原型
unsignedlong__get_free_pages(gfp_tgfp_mask,unsignedintorder);
核心作用
申請(qǐng)2^order個(gè)連續(xù)物理頁,并返回對(duì)應(yīng)的內(nèi)核虛擬地址(直接可讀寫)。
內(nèi)部邏輯
// 偽代碼:__get_free_pages()的實(shí)現(xiàn)邏輯unsignedlong__get_free_pages(gfp_tgfp_mask,unsignedintorder) {structpage*page =alloc_pages(gfp_mask, order); // 調(diào)用alloc_pages()if(!page)return0; // 失敗返回0(內(nèi)核虛擬地址不會(huì)是0)return(unsignedlong)page_to_virt(page); // 轉(zhuǎn)換為虛擬地址}
參數(shù)與返回值
?參數(shù)和alloc_pages()完全一致;
?返回值:成功返回內(nèi)核虛擬地址(非0),失敗返回0(注意:不是NULL,因?yàn)榉祷刂凳?/span>unsigned long)。
適用場(chǎng)景
大部分驅(qū)動(dòng)開發(fā)場(chǎng)景:比如申請(qǐng)?jiān)O(shè)備緩沖區(qū)、臨時(shí)存儲(chǔ)數(shù)據(jù)等,直接用虛擬地址讀寫即可,無需關(guān)心物理頁細(xì)節(jié)。
3.4清零內(nèi)存:get_zeroed_page ()
如果申請(qǐng)的內(nèi)存需要初始化為0(避免臟數(shù)據(jù)影響),get_zeroed_page()是專用函數(shù)——它是__get_free_pages()的"清零版本"。
函數(shù)原型
unsignedlongget_zeroed_page(gfp_tgfp_mask);
核心作用
申請(qǐng)1頁(order=0)內(nèi)存,并將整個(gè)頁面清零,返回內(nèi)核虛擬地址。
內(nèi)部邏輯
// 偽代碼:get_zeroed_page()的實(shí)現(xiàn)邏輯unsignedlongget_zeroed_page(gfp_tgfp_mask){unsignedlongaddr = __get_free_pages(gfp_mask | __GFP_ZERO,0);// __GFP_ZERO標(biāo)志會(huì)讓內(nèi)核在分配時(shí)自動(dòng)清零returnaddr;}
適用場(chǎng)景
需要"干凈內(nèi)存"的場(chǎng)景:比如存放配置結(jié)構(gòu)體、用戶數(shù)據(jù)拷貝緩沖區(qū)等,避免未初始化的臟數(shù)據(jù)導(dǎo)致邏輯錯(cuò)誤。
3.5簡(jiǎn)化變體:alloc_page ()與__get_free_page ()
為了方便"申請(qǐng)1頁內(nèi)存"的場(chǎng)景,內(nèi)核提供了兩個(gè)簡(jiǎn)化函數(shù)(本質(zhì)是宏定義):
?alloc_page(gfp_mask)=alloc_pages(gfp_mask, 0)(申請(qǐng)1頁,返回page指針);
?__get_free_page(gfp_mask)=__get_free_pages(gfp_mask, 0)(申請(qǐng)1頁,返回虛擬地址)。
四、實(shí)戰(zhàn)示例:函數(shù)怎么用?
光說不練假把式,我們用3個(gè)實(shí)際示例,演示核心函數(shù)的使用(基于Linux內(nèi)核5.4,可直接編譯為內(nèi)核模塊)。
示例1:__get_free_pages ()分配內(nèi)存(最常用場(chǎng)景)
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("__get_free_pages() Example");staticunsignedlongvirt_addr; // 保存分配的虛擬地址// 模塊加載函數(shù)(進(jìn)程上下文,可用GFP_KERNEL)staticint__initfree_pages_init(void){// 申請(qǐng)2頁內(nèi)存,策略GFP_KERNEL(可睡眠)virt_addr = __get_free_pages(GFP_KERNEL, ALLOC_ORDER);if(!virt_addr) { // 檢查分配結(jié)果printk(KERN_ERR"Failed to allocate memory with __get_free_pagesn");return-ENOMEM; // 分配失敗,模塊加載失敗}// 向分配的內(nèi)存寫入數(shù)據(jù)(直接用虛擬地址訪問)sprintf((char*)virt_addr,"Buddy System: Allocate %d pages, size %d bytes",(1<< ALLOC_ORDER), ALLOC_SIZE);// 打印日志(dmesg查看)printk(KERN_INFO"Allocated virtual address: 0x%lxn", virt_addr);printk(KERN_INFO"Data in memory: %sn", (char*)virt_addr);return0;}// 模塊卸載函數(shù)(釋放內(nèi)存)staticvoid__exitfree_pages_exit(void){if(virt_addr) { // 確認(rèn)內(nèi)存已分配free_pages(virt_addr, ALLOC_ORDER); // 對(duì)應(yīng)__get_free_pages()的釋放函數(shù)printk(KERN_INFO"Memory freed successfullyn");}}module_init(free_pages_init);module_exit(free_pages_exit);
編譯運(yùn)行步驟
1.編寫Makefile:
obj-m += buddy_demo1.oall:make -C /lib/modules/$(shelluname -r)/build M=$(PWD)modulesclean:make -C /lib/modules/$(shelluname -r)/build M=$(PWD)clean
1.編譯:make
2.加載模塊:sudo insmod buddy_demo1.ko
3.查看日志:dmesg | grep "Buddy System"
4.卸載模塊:sudo rmmod buddy_demo1
預(yù)期輸出
[] Allocatedvirtualaddress:0xffff88800abc0000[] Datainmemory: Buddy System: Allocate2pages, size8192bytes[] Memory freed successfully
示例2:alloc_pages ()操作page結(jié)構(gòu)體
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("alloc_pages() Example");staticstructpage*page_ptr;staticunsignedlongvirt_addr;staticint__initalloc_pages_init(void){// 申請(qǐng)1頁內(nèi)存,返回page結(jié)構(gòu)體指針page_ptr =alloc_pages(GFP_KERNEL,0);if(!page_ptr) {printk(KERN_ERR"Failed to allocate page with alloc_pagesn");return-ENOMEM;}// 操作page結(jié)構(gòu)體:設(shè)置頁為只讀(通過page屬性)set_bit(PG_ro, &page_ptr->flags);printk(KERN_INFO"Allocated page: frame number = %lun",page_to_pfn(page_ptr));// 轉(zhuǎn)換為虛擬地址并寫入數(shù)據(jù)virt_addr = (unsignedlong)page_to_virt(page_ptr);sprintf((char*)virt_addr,"Page frame %lu is read-only",page_to_pfn(page_ptr));printk(KERN_INFO"Virtual address: 0x%lx, Data: %sn", virt_addr, (char*)virt_addr);return0;}staticvoid__exitalloc_pages_exit(void){if(page_ptr) {__free_pages(page_ptr,0); // 對(duì)應(yīng)alloc_pages()的釋放函數(shù)printk(KERN_INFO"Page freed successfullyn");}}module_init(alloc_pages_init);module_exit(alloc_pages_exit);
示例3:get_zeroed_page ()分配清零內(nèi)存
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("get_zeroed_page() Example");staticunsignedlongzero_addr;staticint__initzero_page_init(void){// 申請(qǐng)1頁清零內(nèi)存zero_addr =get_zeroed_page(GFP_KERNEL);if(!zero_addr) {printk(KERN_ERR"Failed to allocate zeroed pagen");return-ENOMEM;}// 驗(yàn)證清零:直接讀取內(nèi)存,確認(rèn)初始值為0printk(KERN_INFO"Zeroed page address: 0x%lxn", zero_addr);printk(KERN_INFO"Initial value (first byte): %d (should be 0)n",*(unsignedchar*)zero_addr);// 寫入數(shù)據(jù)*(char*)zero_addr ='A';printk(KERN_INFO"After writing 'A', value: %cn", *(char*)zero_addr);return0;}staticvoid__exitzero_page_exit(void){if(zero_addr) {free_pages(zero_addr,0); // get_zeroed_page()用free_pages()釋放printk(KERN_INFO"Zeroed page freedn");}}module_init(zero_page_init);module_exit(zero_page_exit);
五、內(nèi)存申請(qǐng)流程可視化(流程圖)
5.1伙伴系統(tǒng)整體分配流程

5.2 __alloc_pages ()內(nèi)部核心流程

六、關(guān)鍵注意事項(xiàng)(避坑指南)
1.order不能超范圍:必須滿足0 ≤ order < MAX_ORDER(默認(rèn)MAX_ORDER=11),否則分配必失?。?/span>
2.gfp_mask選對(duì)場(chǎng)景:
?中斷上下文、原子操作中,必須用GFP_ATOMIC(不能睡眠);
?進(jìn)程上下文(如probe、內(nèi)核線程),優(yōu)先用GFP_KERNEL(可睡眠,分配成功率更高);
1.必須檢查返回值:分配失敗是常見情況(比如內(nèi)存不足),一定要判斷返回值是否為NULL/0,避免空指針崩潰;
2.釋放函數(shù)要對(duì)應(yīng):
?__get_free_pages()/get_zeroed_page()→free_pages();
?alloc_pages()→__free_pages();
?釋放時(shí)的order必須和申請(qǐng)時(shí)一致,否則會(huì)破壞伙伴系統(tǒng)鏈表;
1.避免內(nèi)存泄漏:內(nèi)核內(nèi)存沒有"自動(dòng)回收",分配的內(nèi)存必須在模塊卸載、函數(shù)退出時(shí)釋放,否則會(huì)導(dǎo)致內(nèi)存泄漏;
2.不要越界訪問:分配的內(nèi)存大小是PAGE_SIZE << order,超出范圍會(huì)觸發(fā)內(nèi)核Oops。
七、總結(jié)
伙伴系統(tǒng)的內(nèi)存申請(qǐng)函數(shù)看似多,但核心邏輯很統(tǒng)一:都是向伙伴系統(tǒng)請(qǐng)求"2^order個(gè)連續(xù)物理頁",區(qū)別僅在于返回形式(page指針/虛擬地址)和附加功能(清零、參數(shù)檢查)。
用一張表總結(jié)函數(shù)選擇邏輯:
|
需求場(chǎng)景
|
推薦函數(shù)
|
返回值類型
|
|
直接用虛擬地址、無需操作page
|
__get_free_pages()
|
內(nèi)核虛擬地址
|
|
申請(qǐng)1頁、需要清零
|
get_zeroed_page()
|
內(nèi)核虛擬地址
|
|
需要操作page結(jié)構(gòu)體(設(shè)置屬性等)
|
alloc_pages()
|
struct page指針
|
|
申請(qǐng)1頁、需要操作page結(jié)構(gòu)體
|
alloc_page()
|
struct page指針
|
掌握這些函數(shù),你就能應(yīng)對(duì)絕大多數(shù)內(nèi)核態(tài)內(nèi)存申請(qǐng)場(chǎng)景。記住核心:選對(duì)函數(shù)、傳對(duì)參數(shù)、檢查返回值、及時(shí)釋放,就能安全、高效地使用內(nèi)核內(nèi)存。
如果覺得這篇文章有用,歡迎點(diǎn)贊、在看、轉(zhuǎn)發(fā)給身邊的開發(fā)小伙伴~
-
Linux
+關(guān)注
關(guān)注
88文章
11763瀏覽量
219088 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4417瀏覽量
67545 -
內(nèi)存分配
+關(guān)注
關(guān)注
0文章
19瀏覽量
8564
發(fā)布評(píng)論請(qǐng)先 登錄
Linux的內(nèi)存管理是什么,Linux的內(nèi)存管理詳解
Linux內(nèi)核中系統(tǒng)調(diào)用詳解
Linux內(nèi)核內(nèi)存規(guī)整總結(jié)
Linux內(nèi)核地址映射模型與Linux內(nèi)核高端內(nèi)存詳解
Linux內(nèi)存系統(tǒng): Linux 內(nèi)存分配算法
高端內(nèi)存的詳解:linux用戶空間與內(nèi)核空間
詳解Linux的物理內(nèi)存
Linux內(nèi)核GPIO操作函數(shù)的詳解分析
伙伴算法如何才能在Linux內(nèi)核中實(shí)現(xiàn)應(yīng)用及其改進(jìn)
Linux內(nèi)存管理之伙伴系統(tǒng)
申請(qǐng)函數(shù)kmalloc、kzalloc、vmalloc區(qū)別說明
Linux內(nèi)核引導(dǎo)內(nèi)存分配器的原理
Linux內(nèi)核如何使用結(jié)構(gòu)體和函數(shù)指針?
RK平臺(tái)Linux IOMMU開發(fā):從原理到實(shí)戰(zhàn)
Linux內(nèi)核伙伴系統(tǒng)內(nèi)存申請(qǐng)函數(shù)詳解:從原理到實(shí)戰(zhàn)
評(píng)論