Linux内核伙伴系统内存申请函数详解:从原理到实战

Linux内核中,内存管理是整个系统稳定运行的基石,而伙伴系统(Buddy System作为内核物理内存分配的核心机制,更是驱动开发、内核模块开发的必备知识点。它通过"2的幂次分配粒度"巧妙解决了外碎片问题,而我们申请内核内存的所有操作,最终都要通过伙伴系统提供的核心函数来完成。

今天这篇文章,我们就来全面拆解伙伴系统的内存申请函数:从底层核心到上层封装,从参数解析到实战示例,再到可视化流程,帮你彻底搞懂"内核内存怎么申请"

一、前言:为什么要关注伙伴系统?

内核内存和用户态内存完全是两套管理体系:用户态有malloc/free,但内核态不能直接用——内核需要更高效、更安全的内存分配方式,而伙伴系统就是为此而生。

解决外碎片:传统连续分配会产生大量"无法利用的小空闲块",伙伴系统通过固定2^order的分配粒度,让空闲块可拆分、可合并,从根源减少外碎片;

支撑内核核心功能:进程栈、内核模块、设备缓冲区等所有内核态内存需求,都依赖伙伴系统分配;

开发必备技能:驱动或内核模块中,只要涉及内存操作,就必须掌握伙伴系统的申请/释放函数。

在讲函数之前,先快速回顾下伙伴系统的核心原理,帮你建立认知基础。

二、伙伴系统核心原理速览

伙伴系统的设计思想非常简洁,核心围绕3个关键点:

1.分配粒度:物理内存被划分为"页块",块大小必须是2^order个物理页(order称为"分配阶")。比如order=0对应1页,order=1对应2页,order=3对应8页,最大orderMAX_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 ),表示申请2^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 ()分配内存(最常用场景)

#include
      #include
      #include
      #include
      MODULE_LICENSE("GPL");MODULE_DESCRIPTION("__get_free_pages() Example");#defineALLOC_ORDER 1 // 申请2^1=2页内存#defineALLOC_SIZE (PAGE_SIZE << ALLOC_ORDER)  // 总大小=2*4096=8192字节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

预期输出

[12345.678901] Allocatedvirtualaddress:0xffff88800abc0000[12345.678905] Datainmemory: Buddy System: Allocate2pages, size8192bytes[12345.678907] Memory freed successfully

示例2alloc_pages ()操作page结构体

#include
      #include
      #include
      #include
      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);

示例3get_zeroed_page ()分配清零内存

#include
      #include
      #include
      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; } // 验证清零:直接读取内存,确认初始值为0 printk(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伙伴系统整体分配流程

wKgZO2kXJXuAa_PwAASMpMJBbYc948.png

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指针

掌握这些函数,你就能应对绝大多数内核态内存申请场景。记住核心:选对函数、传对参数、检查返回值、及时释放,就能安全、高效地使用内核内存。

如果觉得这篇文章有用,欢迎点赞、在看、转发给身边的开发小伙伴~

Gravatar

About 奥洁自由人

作者文章