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

5.2 __alloc_pages ()內部核心流程

六、關鍵注意事項(避坑指南)
1.order不能超范圍:必須滿足0 ≤ order < MAX_ORDER(默認MAX_ORDER=11),否則分配必失敗;
2.gfp_mask選對場景:
?中斷上下文、原子操作中,必須用GFP_ATOMIC(不能睡眠);
?進程上下文(如probe、內核線程),優先用GFP_KERNEL(可睡眠,分配成功率更高);
1.必須檢查返回值:分配失敗是常見情況(比如內存不足),一定要判斷返回值是否為NULL/0,避免空指針崩潰;
2.釋放函數要對應:
?__get_free_pages()/get_zeroed_page()→free_pages();
?alloc_pages()→__free_pages();
?釋放時的order必須和申請時一致,否則會破壞伙伴系統鏈表;
1.避免內存泄漏:內核內存沒有"自動回收",分配的內存必須在模塊卸載、函數退出時釋放,否則會導致內存泄漏;
2.不要越界訪問:分配的內存大小是PAGE_SIZE << order,超出范圍會觸發內核Oops。
七、總結
伙伴系統的內存申請函數看似多,但核心邏輯很統一:都是向伙伴系統請求"2^order個連續物理頁",區別僅在于返回形式(page指針/虛擬地址)和附加功能(清零、參數檢查)。
用一張表總結函數選擇邏輯:
|
需求場景
|
推薦函數
|
返回值類型
|
|
直接用虛擬地址、無需操作page
|
__get_free_pages()
|
內核虛擬地址
|
|
申請1頁、需要清零
|
get_zeroed_page()
|
內核虛擬地址
|
|
需要操作page結構體(設置屬性等)
|
alloc_pages()
|
struct page指針
|
|
申請1頁、需要操作page結構體
|
alloc_page()
|
struct page指針
|
掌握這些函數,你就能應對絕大多數內核態內存申請場景。記住核心:選對函數、傳對參數、檢查返回值、及時釋放,就能安全、高效地使用內核內存。
如果覺得這篇文章有用,歡迎點贊、在看、轉發給身邊的開發小伙伴~
-
Linux
+關注
關注
88文章
11758瀏覽量
219001 -
函數
+關注
關注
3文章
4417瀏覽量
67499 -
內存分配
+關注
關注
0文章
19瀏覽量
8560
發布評論請先 登錄
Linux內核中系統調用詳解
Linux內核地址映射模型與Linux內核高端內存詳解
高端內存的詳解:linux用戶空間與內核空間
RK平臺Linux IOMMU開發:從原理到實戰
Linux內核伙伴系統內存申請函數詳解:從原理到實戰
評論