Linux内核伙伴系统内存申请函数详解:从原理到实战 电子说
在 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 的连续物理页块"。
伙伴系统提供了一套 "底层核心 + 上层封装" 的函数体系,不同函数适用于不同场景。我们从底层到上层逐一拆解:
__alloc_pages()是伙伴系统最底层的内存申请函数,所有其他申请函数最终都会调用它。可以说,它是 "内核内存分配的入口"。
struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order);
直接向伙伴系统申请2^order 个连续物理页,返回对应物理页的struct page结构体指针(注意:返回的是物理页描述符,不是虚拟地址)。
|
参数名
|
作用说明
|
|
gfp_mask
|
分配策略标志(核心参数!),告诉内核 "怎么分配内存"(能否睡眠、用哪种内存域等)
|
|
order
|
分配阶(0≤order
|
gfp_mask 是内核内存分配的 "策略开关",不同场景必须选对,否则会导致系统异常:
•GFP_KERNEL:最常用,允许睡眠(可触发页回收),适用于进程上下文(比如驱动的 probe 函数、内核线程);
•GFP_ATOMIC:不允许睡眠、不允许触发页回收,适用于中断上下文(比如中断处理函数);
•GFP_DMA:仅从 DMA 内存域分配(适用于需要 DMA 传输的设备缓冲区);
•GFP_HIGHUSER:允许从高端内存分配(适用于大内存场景)。
•成功:返回第一个物理页的struct page指针;
•失败:返回NULL(表示没有找到满足条件的连续物理页)。
•底层裸函数,没有参数合法性检查(比如 order 超过 MAX_ORDER 也会尝试分配);
•不建议直接调用(风险高),仅内核核心代码使用;
•返回的是page结构体,需要手动转换为虚拟地址才能访问(用page_to_virt())。
alloc_pages()是对__alloc_pages()的上层封装,也是驱动开发中最常用的 "page 级分配函数"。
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
与__alloc_pages()功能一致,但增加了参数合法性检查,更安全。
|
特性
|
__alloc_pages()
|
alloc_pages()
|
|
参数检查
|
无
|
有(比如检查 order 范围)
|
|
适用场景
|
内核核心代码
|
驱动 / 内核模块开发
|
|
安全性
|
低
|
高
|
需要直接操作struct page结构体的场景:
•设置页属性(比如标记为只读、可缓存);
•映射高端内存(高端内存无法直接访问,需要通过 page 结构体建立映射);
•管理物理页的引用计数。
如果不需要操作page结构体,只想直接获取可访问的虚拟地址,__get_free_pages()是最优选择—— 它帮我们完成了 "申请 page + 转换虚拟地址" 的全过程。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
申请 2^order 个连续物理页,并返回对应的内核虚拟地址(直接可读写)。
// 伪代码:__get_free_pages()的实现逻辑unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) {struct page *page = alloc_pages(gfp_mask, order); // 调用alloc_pages()if (!page)return 0; // 失败返回0(内核虚拟地址不会是0)return (unsigned long)page_to_virt(page); // 转换为虚拟地址}
•参数和alloc_pages()完全一致;
•返回值:成功返回内核虚拟地址(非 0),失败返回 0(注意:不是 NULL,因为返回值是 unsigned long)。
大部分驱动开发场景:比如申请设备缓冲区、临时存储数据等,直接用虚拟地址读写即可,无需关心物理页细节。
如果申请的内存需要初始化为 0(避免脏数据影响),get_zeroed_page()是专用函数—— 它是__get_free_pages()的 "清零版本"。
unsigned long get_zeroed_page(gfp_t gfp_mask);
申请 1 页(order=0)内存,并将整个页面清零,返回内核虚拟地址。
// 伪代码:get_zeroed_page()的实现逻辑unsigned long get_zeroed_page(gfp_t gfp_mask) {unsigned long addr = __get_free_pages(gfp_mask | __GFP_ZERO, 0);// __GFP_ZERO标志会让内核在分配时自动清零return addr;}
需要 "干净内存" 的场景:比如存放配置结构体、用户数据拷贝缓冲区等,避免未初始化的脏数据导致逻辑错误。
为了方便 "申请 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,可直接编译为内核模块)。
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("__get_free_pages() Example");static unsigned long virt_addr; // 保存分配的虚拟地址// 模块加载函数(进程上下文,可用GFP_KERNEL)static int __init free_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);return 0;}// 模块卸载函数(释放内存)static void __exit free_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/$(shell uname -r)/build M=$(PWD) modulesclean:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
1.编译:make
2.加载模块:sudo insmod buddy_demo1.ko
3.查看日志:dmesg | grep "Buddy System"
4.卸载模块:sudo rmmod buddy_demo1
[] Allocated virtual address: 0xffff88800abc0000[] Data in memory: Buddy System: Allocate 2 pages, size 8192 bytes[] Memory freed successfully
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("alloc_pages() Example");static struct page *page_ptr;static unsigned long virt_addr;static int __init alloc_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 = (unsigned long)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);return 0;}static void __exit alloc_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);
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("get_zeroed_page() Example");static unsigned long zero_addr;static int __init zero_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",*(unsigned char *)zero_addr);// 写入数据*(char *)zero_addr = 'A';printk(KERN_INFO "After writing 'A', value: %cn", *(char *)zero_addr);return 0;}static void __exit zero_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);


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