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

电子说

1.4w人已加入

描述

 

 

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

 

 

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

 

 

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

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

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

 

 

1.分配粒度:物理内存被划分为 "页块",块大小必须是 2^order 个物理页(order 称为 "分配阶")。比如 order=0 对应 页,order=1 对应 页,order=3 对应 页,最大 order MAX_ORDER定义(默认 11,即最大 2048 页 = 8MB);

 

 

2.伙伴块定义:两个大小相同、物理地址连续、且来自同一父块的页块,互为 "伙伴"。比如 order=1 的块(页)拆分后,会生成两个 order=0 的伙伴块;

 

 

3.分配 / 释放逻辑

 

 

分配:先找对应 order 的空闲块,找到直接分配;找不到就拆分更高 order 的空闲块,直到得到目标大小;

 

 

释放:释放的块会检查是否有空闲伙伴,若有则合并为更高 order 的块,逐步归还到空闲链表。

 

 

理解了这 3 点,再看后续的函数就会豁然开朗 —— 所有申请函数的本质,都是向伙伴系统请求 "指定 order 的连续物理页块"

 

 

三、伙伴系统核心申请函数详解

 

伙伴系统提供了一套 "底层核心 上层封装的函数体系,不同函数适用于不同场景。我们从底层到上层逐一拆解:

 

 

3.1 底层核心:__alloc_pages ()

 

__alloc_pages()是伙伴系统最底层的内存申请函数,所有其他申请函数最终都会调用它。可以说,它是 "内核内存分配的入口"

 

 

函数原型

 

  •  
struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order);

核心作用

 

直接向伙伴系统申请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 级分配函数"

 

 

函数原型

 

  •  
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

核心作用

 

__alloc_pages()功能一致,但增加了参数合法性检查,更安全。

 

 

__alloc_pages () 的区别

 

特性

 

 

__alloc_pages()

 

 

alloc_pages()

 

 

参数检查

 

 

 

 

有(比如检查 order 范围)

 

 

适用场景

 

 

内核核心代码

 

 

驱动 / 内核模块开发

 

 

安全性

 

 

 

 

 

 

适用场景

 

需要直接操作struct page结构体的场景:

 

 

设置页属性(比如标记为只读、可缓存);

 

 

映射高端内存(高端内存无法直接访问,需要通过 page 结构体建立映射);

 

 

管理物理页的引用计数。

 

 

3.3 虚拟地址直达:__get_free_pages ()

 

如果不需要操作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)。

 

 

适用场景

 

大部分驱动开发场景:比如申请设备缓冲区、临时存储数据等,直接用虚拟地址读写即可,无需关心物理页细节。

 

 

3.4 清零内存:get_zeroed_page ()

 

如果申请的内存需要初始化为 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;}

适用场景

 

需要 "干净内存的场景:比如存放配置结构体、用户数据拷贝缓冲区等,避免未初始化的脏数据导致逻辑错误。

 

 

3.5 简化变体:alloc_page () __get_free_page ()

 

为了方便 "申请 页内存的场景,内核提供了两个简化函数(本质是宏定义):

 

 

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");#define ALLOC_ORDER 1  // 申请2^1=2页内存#define ALLOC_SIZE  (PAGE_SIZE << ALLOC_ORDER)  // 总大小=2*4096=8192字节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

 

 

预期输出

 

  •  
  •  
  •  
[12345.678901] Allocated virtual address: 0xffff88800abc0000[12345.678905] Data in memory: Buddy System: Allocate 2 pages, size 8192 bytes[12345.678907] Memory freed successfully

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

 

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

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

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#include #include #include 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;    }    // 验证清零:直接读取内存,确认初始值为0    printk(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);

五、内存申请流程可视化(流程图)

 

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

 

 

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

 

 

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

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分