在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指针
|
掌握这些函数,你就能应对绝大多数内核态内存申请场景。记住核心:选对函数、传对参数、检查返回值、及时释放,就能安全、高效地使用内核内存。
如果觉得这篇文章有用,欢迎点赞、在看、转发给身边的开发小伙伴~