Linux内核伙伴系统(Buddy System)原理详解
目录
- 概述
- 基本概念
- 数据结构
- 伙伴关系
- 分配算法
- 释放与合并算法
- 完整示例
- 性能分析
- 优缺点总结
- 实际应用
概述
伙伴系统(Buddy System)是Linux内核中用于管理物理页框的核心算法,它通过将内存按2的幂次组织,实现了高效的大块连续内存分配和回收机制。
核心思想
伙伴系统的核心思想是:
- 按2的幂次组织内存:将内存划分为20、21、22…2n大小的块
- 伙伴关系:相邻的两个相同大小的空闲块互为"伙伴"
- 自动合并:释放内存时,如果伙伴块也是空闲的,自动合并成更大的块
- 按需分割:分配时如果没有合适大小的块,从更大的块中分割
基本概念
Order(阶)
order是伙伴系统中的核心概念,表示2的幂次:
| order | 页框数量 | 内存大小(4KB页) | 二进制表示 |
|---|---|---|---|
| 0 | 1 | 4KB | 2^0 = 1 |
| 1 | 2 | 8KB | 2^1 = 2 |
| 2 | 4 | 16KB | 2^2 = 4 |
| 3 | 8 | 32KB | 2^3 = 8 |
| 4 | 16 | 64KB | 2^4 = 16 |
| 5 | 32 | 128KB | 2^5 = 32 |
| 6 | 64 | 256KB | 2^6 = 64 |
| 7 | 128 | 512KB | 2^7 = 128 |
| 8 | 256 | 1MB | 2^8 = 256 |
| 9 | 512 | 2MB | 2^9 = 512 |
| 10 | 1024 | 4MB | 2^10 = 1024 |
MAX_ORDER:通常为11,意味着最大可以分配2^10 = 1024个页框(4MB)
页框(Page Frame)
Linux内核将物理内存划分为固定大小的页框,ARM64架构通常使用4KB页大小。每个页框都有一个对应的struct page描述符。
数据结构
Zone结构
每个内存区域(zone)都维护一个伙伴系统的空闲链表数组:
structzone{// ... 其他字段// 伙伴系统的核心:MAX_ORDER个空闲区域链表structfree_areafree_area[MAX_ORDER];// ... 其他字段};free_area结构
每个order对应一个free_area结构:
structfree_area{// 按迁移类型分类的空闲链表// MIGRATE_TYPES包括:MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE等structlist_headfree_list[MIGRATE_TYPES];// 该order的空闲块数量unsignedlongnr_free;};重要说明:
order值作为数组索引是唯一的:
free_area[0]对应 order=0(1个页框)free_area[1]对应 order=1(2个页框)free_area[2]对应 order=2(4个页框)- …以此类推到
free_area[MAX_ORDER-1] - 每个 order 值(0 到 MAX_ORDER-1)都是唯一的,因为数组索引是唯一的
同一个order下可以有多个空闲块:
- 虽然每个 order 值唯一,但同一个 order 下可以有多个空闲块
- 这些空闲块通过
free_list链表连接起来 - 例如:
free_area[1].free_list[0]可能链接着多个 order=1 的空闲块(每个都是2个页框)
数据结构层次关系:
zone->free_area[MAX_ORDER] (数组,索引就是order值) ├── free_area[0] (order=0,唯一) │ └── free_list[0] → [块1] → [块2] → [块3] → ... (链表,可以有多个块) ├── free_area[1] (order=1,唯一) │ └── free_list[0] → [块1] → [块2] → ... (链表,可以有多个块) └── ...- free_area中存储的是合并后的块:
- ✅自动合并机制:释放内存时,如果伙伴块也是空闲的,会自动合并成更大的块
- ✅存储的是最大可合并块:free_area 中存储的是"当前可以合并的最大块"
- ⚠️合并是有条件的:只有当两个块满足伙伴关系(大小相同、物理相邻、对齐)且都是空闲时才会合并
- ⚠️为什么同一order下还有多个块:
- 如果某个 order 下有多个空闲块,说明这些块之间不是伙伴关系
- 或者它们的伙伴块已经被分配了,所以无法合并
- 例如:页框0-1和页框10-11都是order=1的空闲块,但它们不是伙伴(不相邻),所以不会合并
页描述符
每个物理页框都有一个struct page描述符:
structpage{// 页标志位unsignedlongflags;// 引用计数atomic_t_count;// 如果页在伙伴系统中,order值存储在这里unsignedintorder;// LRU链表节点(Least Recently Used)// 在伙伴系统中,用于将空闲页框连接成链表// 当页框在空闲链表中时,通过lru字段连接structlist_headlru;// ... 其他字段};lru字段的作用:
链表连接:
lru是struct list_head类型,用于实现侵入式链表- 重要:free_area 链的是"块"(block),不是单个页框
- 每个块用一个
struct page表示(指向块的第一个页框) - 当块在伙伴系统的空闲链表中时,通过块的第一个页框的
lru字段连接成双向链表 - 每个
free_area[order].free_list[MIGRATE_TYPE]链表中,空闲块通过lru字段连接
块与页框的关系:
- 块(block):由多个连续的页框组成,大小取决于 order
- order=0 的块:1个页框
- order=1 的块:2个页框
- order=2 的块:4个页框
- …
- struct page:链表中存储的是块的第一个页框的
struct page - 例如:order=2 的块包含页框0-3,但链表中只存储页框0的
struct page
- 块(block):由多个连续的页框组成,大小取决于 order
从链表节点获取块:
- 链表操作时,我们只有
lru字段的地址(链表节点) - 通过
list_entry(ptr, struct page, lru)宏,可以从lru的地址计算出struct page的起始地址 - 这个
struct page代表块的第一个页框,通过它可以访问整个块 - 这是Linux内核中典型的"侵入式链表"用法
- 链表操作时,我们只有
数据结构关系图示:
free_area[order=2].free_list[MIGRATE_TYPE] (链表头) ↓ ┌─────────────────────────────────┐ struct page (块1的第一个页框) 代表块1: 页框0-3 (4个页框) ... struct list_head lru; ←────────── 链表节点1 ... └─────────────────────────────────┘ ↓ (lru.next) ┌─────────────────────────────────┐ struct page (块2的第一个页框) 代表块2: 页框8-11 (4个页框) ... struct list_head lru; ←────────── 链表节点2 ... └─────────────────────────────────┘ ↓ (lru.next) ...- 使用示例:
// 从链表中取出第一个空闲块(通过块的第一个页框的struct page)structlist_head*node=area->free_list[MIGRATE_TYPE].next;// 获取lru节点地址structpage*page=list_entry(node,structpage,lru);// 计算出page地址// page 指向块的第一个页框,通过 page->order 可以知道块的大小// 从链表中删除块list_del(&page->lru);// 通过page->lru的地址删除节点伙伴关系
伙伴的定义
两个内存块互为"伙伴"需要满足以下条件:
- 大小相同:两个块必须是相同的order
- 物理相邻:两个块在物理内存中必须相邻
- 对齐要求:起始地址必须对齐到块大小的边界
伙伴判断算法
给定一个order为k的块,其起始页框号为page_num,其伙伴块的页框号计算如下:
// 伙伴块的页框号计算staticinlineunsignedlong__find_buddy_index(unsignedlongpage_idx,unsignedintorder){returnpage_idx^(1<<order);}原理说明:
- 对于order=k的块,大小为2^k个页框
- 伙伴块的位置:如果块起始于
page_idx,其伙伴块起始于page_idx ^ (1 << k) ^是异或运算,1 << k是2^k
示例:
// 假设order=2(4个页框),起始页框号为4page_idx=4order=2buddy_idx=4^(1<<2)=4^4=0// 如果起始页框号为0page_idx=0buddy_idx=0^4=4伙伴关系图示
内存布局示例(order=2,每个块4个页框): 页框号: 0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15 ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ 块A: 块A (4页) 块B (4页) 块C (4页) 块D (4页) └───────────┘ └───────────┘ └───────────┘ └───────────┘ 起始:0 起始:4 起始:8 起始:12 伙伴关系: - 块A(0-3) 和 块B(4-7) 是伙伴(相邻且大小相同,可合并成order=3的块,包含页框0-7) - 块C(8-11) 和 块D(12-15) 是伙伴(相邻且大小相同,可合并成order=3的块,包含页框8-15) - 块B(4-7) 和 块C(8-11) 不是伙伴(虽然相邻,但合并后起始地址4不对齐到8的倍数)分配算法
算法流程
伙伴系统的分配算法遵循以下步骤:
graph TD A[请求分配order=k的块] --> B{检查order=k的空闲链表} B -->|有空闲块| C[从链表取出一个块] C --> D[标记为已分配] D --> E[返回块地址] B -->|无空闲块| F[检查order=k+1] F --> G{有空闲块?} G -->|是| H[分割块] H --> I[将块分成两个order=k的块] I --> J[一个分配,一个加入order=k链表] J --> E G -->|否| K[继续向上查找order=k+2...] K --> L{找到空闲块?} L -->|是| M[递归分割直到order=k] M --> E L -->|否| N[分配失败]分配算法伪代码
structpage*alloc_pages(gfp_tgfp_mask,unsignedintorder){structzone*zone;structfree_area*area;structpage*page;unsignedintcurrent_order;// 1. 选择合适的zonezone=get_zone(gfp_mask);// 2. 从请求的order开始查找for(current_order=order;current_order<MAX_ORDER;current_order++){area=&zone->free_area[current_order];// 3. 检查是否有空闲块if(!list_empty(&area->free_list[MIGRATE_TYPE])){// 4. 从链表中取出一个块// list_entry宏:从链表节点(lru字段)的地址,计算出struct page的起始地址// area->free_list[MIGRATE_TYPE].next 指向第一个空闲块的第一个页框的lru字段// 通过list_entry可以获取包含该lru字段的struct page指针(代表块的第一个页框)page=list_entry(area->free_list[MIGRATE_TYPE].next,structpage,lru);// 从空闲链表中删除该块(通过块的第一个页框的lru字段删除链表节点)list_del(&page->lru);// 更新该order的空闲块计数area->nr_free--;// 5. 如果取出的块比需要的大,需要分割if(current_order>order){// 分割块expand(zone,page,order,current_order,area);}// 6. 设置页标志set_page_private(page,order);returnpage;}}// 7. 所有order都没有空闲块,分配失败returnNULL;}分割算法(expand)
当从更大的块中分配时,需要将块分割:
staticinlinevoidexpand(structzone*zone,structpage*page,intlow,inthigh,structfree_area*area){unsignedlongsize=1<<high;// 原始块大小// 循环分割,直到达到需要的orderwhile(high>low){area--;// 移到下一个更小的orderhigh--;// order减1size>>=1;// 大小减半// 创建伙伴块(高地址的一半)structpage*buddy=page+size;// 将伙伴块加入空闲链表(通过buddy->lru字段添加到链表)list_add(&buddy->lru,&area->free_list[MIGRATE_TYPE]);area->nr_free++;// 设置伙伴块的orderset_page_private(buddy,high);}}分配示例
场景:请求分配order=1的块(2个页框),但order=1没有空闲块
初始状态: order=0: 无空闲块 order=1: 无空闲块 order=2: [块A: 页框0-3] ← 有空闲块 步骤1:从order=2取出块A(4个页框) 步骤2:分割块A: - 低地址一半(页框0-1)→ 分配出去(order=1) - 高地址一半(页框2-3)→ 加入order=1的空闲链表 结果: order=0: 无空闲块 order=1: [块B: 页框2-3] ← 新加入的空闲块 order=2: 无空闲块 分配:页框0-1(已分配)释放与合并算法
算法流程
释放内存时,伙伴系统会尝试合并伙伴块:
释放算法伪代码
void__free_pages(structpage*page,unsignedintorder){structzone*zone=page_zone(page);unsignedlongpage_idx=page_to_pfn(page);structfree_area*area;unsignedintcurrent_order=order;// 1. 循环尝试合并,直到无法合并为止while(current_order<MAX_ORDER-1){// 2. 计算伙伴块的页框号unsignedlongbuddy_idx=__find_buddy_index(page_idx,current_order);structpage*buddy=page+(buddy_idx-page_idx);// 3. 检查伙伴块是否存在、是否空闲、是否在同一zoneif(!page_is_buddy(page,buddy,current_order))break;// 无法合并,退出循环// 4. 伙伴块可以合并// 从空闲链表中移除伙伴块(通过lru字段删除链表节点)list_del(&buddy->lru);area=&zone->free_area[current_order];area->nr_free--;// 5. 清除伙伴块的order标记clear_page_private(buddy);// 6. 确定合并后块的起始地址(取较小的页框号)if(buddy_idx<page_idx)page_idx=buddy_idx;// 7. 准备合并到下一个更大的ordercurrent_order++;page=page+(page_idx-page_to_pfn(page));}// 8. 将最终合并后的块加入空闲链表// 通过page->lru字段将块添加到对应order的空闲链表中// page指向块的第一个页框,通过lru字段连接成链表area=&zone->free_area[current_order];list_add(&page->lru,&area->free_list[MIGRATE_TYPE]);area->nr_free++;set_page_private(page,current_order);}伙伴块检查函数
staticinlineintpage_is_buddy(structpage*page,structpage*buddy,unsignedintorder){// 1. 检查伙伴块是否在同一zoneif(page_zone(page)!=page_zone(buddy))return0;// 2. 检查伙伴块是否在伙伴系统中(通过检查PG_buddy标志)if(!PageBuddy(buddy))return0;// 3. 检查伙伴块的order是否匹配if(page_order(buddy)!=order)return0;return1;// 是有效的伙伴块}释放示例
场景:释放页框2-3(order=1),其伙伴块页框0-1也是空闲的(order=1)
释放前状态: order=0: 无空闲块 order=1: [块A: 页框0-1] ← 空闲 order=2: 无空闲块 步骤1:释放页框2-3(order=1) 步骤2:检查伙伴块页框0-1: - 页框0-1是空闲的(order=1) - 可以合并! 步骤3:合并两个order=1的块: - 移除块A(页框0-1)和块B(页框2-3) - 合并成order=2的块(页框0-3) - 加入order=2的空闲链表 结果: order=0: 无空闲块 order=1: 无空闲块 order=2: [块C: 页框0-3] ← 合并后的块完整示例
示例:内存分配和释放的完整过程
假设系统有16个页框(页框号0-15),初始状态所有页框都在order=3的空闲链表中(8个页框的块)。
初始状态: order=3: [块: 页框0-7] [块: 页框8-15] 内存布局: 页框: 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ┌─────────────────────┐ ┌─────────────────────┐ 块A (order=3) 块B (order=3) └─────────────────────┘ └─────────────────────┘操作1:分配order=1(2个页框)
步骤1:order=1无空闲块,向上查找 步骤2:order=2无空闲块,继续向上 步骤3:order=3有空闲块,取出块A 步骤4:分割块A: - order=2: 页框0-3(继续分割) - order=2: 页框4-7(加入空闲链表) 步骤5:继续分割页框0-3: - order=1: 页框0-1(分配出去) - order=1: 页框2-3(加入空闲链表) 结果: order=1: [页框2-3] order=2: [页框4-7] order=3: [页框8-15] 已分配: 页框0-1操作2:分配order=2(4个页框)
步骤1:order=2有空闲块(页框4-7),直接分配 结果: order=1: [页框2-3] order=2: 无空闲块 order=3: [页框8-15] 已分配: 页框0-1, 页框4-7操作3:释放页框0-1(order=1)
步骤1:检查伙伴块页框2-3: - 页框2-3是空闲的(order=1) - 可以合并! 步骤2:合并成order=2的块(页框0-3) 结果: order=1: 无空闲块 order=2: [页框0-3] order=3: [页框8-15] 已分配: 页框4-7操作4:释放页框4-7(order=2)
步骤1:检查伙伴块页框0-3: - 页框0-3是空闲的(order=2) - 可以合并! 步骤2:合并成order=3的块(页框0-7) 结果: order=1: 无空闲块 order=2: 无空闲块 order=3: [页框0-7] [页框8-15] 已分配: 无为什么同一个order下会有多个块?
关键理解:free_area中存储的是"已经合并过的块",但合并是有条件的!
示例:为什么order=1下会有多个块?
假设内存状态如下:
内存布局(16个页框): 页框: 0 1 | 2 3 | 4 5 | 6 7 | 8 9 | 10 11 | 12 13 | 14 15 ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────────────┐ ┌────┐ ┌────┐ 已分 空闲 已分 空闲 空闲 空闲 已分 空闲 └────┘ └────┘ └────┘ └────┘ └────────────┘ └────┘ └────┘ 块A 块B 块C 块D 块E 块F 块G 块H分析:
块B(页框2-3)和块D(页框6-7)都是order=1的空闲块,但它们不是伙伴:
- 块B的伙伴是页框0-1(已分配),无法合并
- 块D的伙伴是页框4-5(已分配),无法合并
- 块B和块D虽然都是order=1,但不相邻(中间隔着页框4-5),不满足伙伴关系
所以块E和块F会合并成order=2的块(页框8-11)!
正确的状态应该是:
order=1: [块B: 页框2-3] [块D: 页框6-7] [块H: 页框14-15] order=2: [块EF: 页框8-11] ← 块E和块F已合并总结:
- ✅free_area中存储的是合并后的块:能合并的都已经合并了
- ✅同一order下多个块的原因:
- 这些块之间不是伙伴关系(不相邻或不对齐)
- 它们的伙伴块已经被分配,所以无法合并
- 例如:页框2-3和页框6-7都是order=1,但它们的伙伴(页框0-1和页框4-5)都被分配了,所以无法合并
性能分析
时间复杂度
分配操作:O(log n),其中n是最大order值
- 最坏情况:需要从MAX_ORDER向下分割到请求的order
- 平均情况:如果对应order有空闲块,O(1)
释放操作:O(log n)
- 最坏情况:需要合并到MAX_ORDER
- 平均情况:通常只需要合并几次
空间复杂度
- 元数据开销:每个空闲块只需要链表节点,开销很小
- 内存利用率:由于只能分配2的幂次大小,存在内部碎片
性能优化
- 迁移类型分类:按MIGRATE_TYPES分类,减少碎片
- 水线标记:通过pages_min/pages_low/pages_high控制内存回收
- 每CPU缓存:使用per-CPU页框缓存,减少锁竞争
优缺点总结
优点
- ✅分配速度快:O(1)到O(log n)的时间复杂度
- ✅保证物理连续性:分配的页框物理地址连续,满足DMA需求
- ✅自动合并机制:减少外部碎片
- ✅实现简单:算法清晰,易于理解和维护
- ✅可预测性:分配时间可预测,适合实时系统
缺点
- ❌内部碎片严重:只能分配2的幂次大小,浪费内存
- ❌不适合小对象:最小分配单位是4KB(一页)
- ❌内存浪费:实际需求与分配大小不匹配时造成浪费
- ❌最大分配限制:通常最大4MB(MAX_ORDER=11)
- ❌合并开销:释放时需要检查并合并伙伴块
内部碎片示例
请求分配:5个页框(20KB) 实际分配:8个页框(32KB,order=3) 浪费:3个页框(12KB,37.5%的浪费率)实际应用
内核中的使用
- DMA缓冲区分配
// DMA需要物理连续的内存dma_addr_tdma_addr=dma_map_single(dev,buf,size,DMA_TO_DEVICE);- 大页内存(Huge Page)
// 分配大页内存,提高TLB效率alloc_huge_page(vma,addr,0);- SLAB分配器的底层支持
// SLAB从伙伴系统获取页框,然后细分为小对象structpage*page=alloc_pages(GFP_KERNEL,0);分配器选择建议
// 根据需求选择合适的分配方式if(size<4KB){// 使用SLAB/SLUB或kmallockmalloc(size,GFP_KERNEL);}elseif(size<128KB&&need_physically_contiguous){// 使用kmalloc(底层使用伙伴系统)kmalloc(size,GFP_KERNEL);}elseif(size>=4KB&&need_physically_contiguous){// 直接使用伙伴系统unsignedintorder=get_order(size);alloc_pages(GFP_KERNEL,order);}else{// 不需要物理连续,使用vmallocvmalloc(size);}调试工具
- 查看伙伴系统状态
# 查看/proc/buddyinfocat/proc/buddyinfo# 输出示例:Node0, zone Normal11112222220# 表示order=0有1个空闲块,order=1有1个空闲块,等等- 查看页框信息
# 查看/proc/pagetypeinfocat/proc/pagetypeinfo总结
伙伴系统是Linux内核内存管理的基石,它通过巧妙的2的幂次组织和伙伴关系,实现了高效的大块连续内存管理。虽然存在内部碎片的问题,但通过与其他分配器(如SLAB/SLUB)的配合,形成了完整的内存管理体系。
理解伙伴系统的原理对于深入理解Linux内核内存管理至关重要,也是进行内核内存优化和调试的基础。