news 2026/3/2 21:31:45

Linux内核伙伴系统(Buddy System)原理详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核伙伴系统(Buddy System)原理详解

Linux内核伙伴系统(Buddy System)原理详解

目录

  1. 概述
  2. 基本概念
  3. 数据结构
  4. 伙伴关系
  5. 分配算法
  6. 释放与合并算法
  7. 完整示例
  8. 性能分析
  9. 优缺点总结
  10. 实际应用

概述

伙伴系统(Buddy System)是Linux内核中用于管理物理页框的核心算法,它通过将内存按2的幂次组织,实现了高效的大块连续内存分配和回收机制。

核心思想

伙伴系统的核心思想是:

  • 按2的幂次组织内存:将内存划分为20、21、22…2n大小的块
  • 伙伴关系:相邻的两个相同大小的空闲块互为"伙伴"
  • 自动合并:释放内存时,如果伙伴块也是空闲的,自动合并成更大的块
  • 按需分割:分配时如果没有合适大小的块,从更大的块中分割

基本概念

Order(阶)

order是伙伴系统中的核心概念,表示2的幂次:

order页框数量内存大小(4KB页)二进制表示
014KB2^0 = 1
128KB2^1 = 2
2416KB2^2 = 4
3832KB2^3 = 8
41664KB2^4 = 16
532128KB2^5 = 32
664256KB2^6 = 64
7128512KB2^7 = 128
82561MB2^8 = 256
95122MB2^9 = 512
1010244MB2^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;};

重要说明:

  1. 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)都是唯一的,因为数组索引是唯一的
  2. 同一个order下可以有多个空闲块

    • 虽然每个 order 值唯一,但同一个 order 下可以有多个空闲块
    • 这些空闲块通过free_list链表连接起来
    • 例如:free_area[1].free_list[0]可能链接着多个 order=1 的空闲块(每个都是2个页框)
  3. 数据结构层次关系

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] → ... (链表,可以有多个块) └── ...
  1. 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字段的作用:

  1. 链表连接

    • lrustruct list_head类型,用于实现侵入式链表
    • 重要:free_area 链的是"块"(block),不是单个页框
    • 每个块用一个struct page表示(指向块的第一个页框)
    • 当块在伙伴系统的空闲链表中时,通过块的第一个页框的lru字段连接成双向链表
    • 每个free_area[order].free_list[MIGRATE_TYPE]链表中,空闲块通过lru字段连接
  2. 块与页框的关系

    • 块(block):由多个连续的页框组成,大小取决于 order
      • order=0 的块:1个页框
      • order=1 的块:2个页框
      • order=2 的块:4个页框
    • struct page:链表中存储的是块的第一个页框的struct page
    • 例如:order=2 的块包含页框0-3,但链表中只存储页框0的struct page
  3. 从链表节点获取块

    • 链表操作时,我们只有lru字段的地址(链表节点)
    • 通过list_entry(ptr, struct page, lru)宏,可以从lru的地址计算出struct page的起始地址
    • 这个struct page代表块的第一个页框,通过它可以访问整个块
    • 这是Linux内核中典型的"侵入式链表"用法
  4. 数据结构关系图示

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) ...
  1. 使用示例
// 从链表中取出第一个空闲块(通过块的第一个页框的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的地址删除节点

伙伴关系

伙伴的定义

两个内存块互为"伙伴"需要满足以下条件:

  1. 大小相同:两个块必须是相同的order
  2. 物理相邻:两个块在物理内存中必须相邻
  3. 对齐要求:起始地址必须对齐到块大小的边界

伙伴判断算法

给定一个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(已分配)

释放与合并算法

算法流程

释放内存时,伙伴系统会尝试合并伙伴块:

释放order=k的块
将块加入order=k的空闲链表
检查伙伴块是否空闲?
合并两个块
形成order=k+1的块
从order=k链表移除两个块
将合并后的块加入order=k+1链表
可以继续合并?
检查order=k+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

分析:

  1. 块B(页框2-3)和块D(页框6-7)都是order=1的空闲块,但它们不是伙伴

    • 块B的伙伴是页框0-1(已分配),无法合并
    • 块D的伙伴是页框4-5(已分配),无法合并
    • 块B和块D虽然都是order=1,但不相邻(中间隔着页框4-5),不满足伙伴关系
  2. 所以块E和块F会合并成order=2的块(页框8-11)!

  3. 正确的状态应该是

order=1: [块B: 页框2-3] [块D: 页框6-7] [块H: 页框14-15] order=2: [块EF: 页框8-11] ← 块E和块F已合并

总结:

  • free_area中存储的是合并后的块:能合并的都已经合并了
  • 同一order下多个块的原因
    1. 这些块之间不是伙伴关系(不相邻或不对齐)
    2. 它们的伙伴块已经被分配,所以无法合并
    3. 例如:页框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的幂次大小,存在内部碎片

性能优化

  1. 迁移类型分类:按MIGRATE_TYPES分类,减少碎片
  2. 水线标记:通过pages_min/pages_low/pages_high控制内存回收
  3. 每CPU缓存:使用per-CPU页框缓存,减少锁竞争

优缺点总结

优点

  1. 分配速度快:O(1)到O(log n)的时间复杂度
  2. 保证物理连续性:分配的页框物理地址连续,满足DMA需求
  3. 自动合并机制:减少外部碎片
  4. 实现简单:算法清晰,易于理解和维护
  5. 可预测性:分配时间可预测,适合实时系统

缺点

  1. 内部碎片严重:只能分配2的幂次大小,浪费内存
  2. 不适合小对象:最小分配单位是4KB(一页)
  3. 内存浪费:实际需求与分配大小不匹配时造成浪费
  4. 最大分配限制:通常最大4MB(MAX_ORDER=11)
  5. 合并开销:释放时需要检查并合并伙伴块

内部碎片示例

请求分配:5个页框(20KB) 实际分配:8个页框(32KB,order=3) 浪费:3个页框(12KB,37.5%的浪费率)

实际应用

内核中的使用

  1. DMA缓冲区分配
// DMA需要物理连续的内存dma_addr_tdma_addr=dma_map_single(dev,buf,size,DMA_TO_DEVICE);
  1. 大页内存(Huge Page)
// 分配大页内存,提高TLB效率alloc_huge_page(vma,addr,0);
  1. 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);}

调试工具

  1. 查看伙伴系统状态
# 查看/proc/buddyinfocat/proc/buddyinfo# 输出示例:Node0, zone Normal11112222220# 表示order=0有1个空闲块,order=1有1个空闲块,等等
  1. 查看页框信息
# 查看/proc/pagetypeinfocat/proc/pagetypeinfo

总结

伙伴系统是Linux内核内存管理的基石,它通过巧妙的2的幂次组织和伙伴关系,实现了高效的大块连续内存管理。虽然存在内部碎片的问题,但通过与其他分配器(如SLAB/SLUB)的配合,形成了完整的内存管理体系。

理解伙伴系统的原理对于深入理解Linux内核内存管理至关重要,也是进行内核内存优化和调试的基础。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/20 5:14:39

B站视频下载终极指南:批量下载高清画质一键搞定

还在为无法离线观看B站精彩内容而烦恼吗&#xff1f;想要轻松实现B站视频下载&#xff0c;享受高清画质的观影体验&#xff1f;今天为大家推荐一款功能强大的开源工具——哔哩下载姬&#xff0c;让你彻底告别在线播放的种种限制&#xff01; 【免费下载链接】downkyi 哔哩下载姬…

作者头像 李华
网站建设 2026/3/2 0:34:34

2026年大模型(LLM)学习终极指南:从零基础到精通,一篇涵盖全部核心技术与实战!

简介 大语言模型技术主要包括预训练、适配微调、提示学习和知识增强等。预训练阶段通过优化任务设计、热启动机制和分层渐进训练等策略提升效率&#xff1b;适配微调包括指令微调和参数高效微调(如Prefix-Tuning、LoRA等)&#xff1b;提示学习涵盖少样本、零样本和上下文学习&…

作者头像 李华
网站建设 2026/3/2 7:15:03

星海SABS系列与SABF系列整流桥相同点与不同点分析

在电子元器件领域&#xff0c;整流桥作为电源转换的核心部件&#xff0c;其性能和稳定性对整个电路的运行至关重要。那么&#xff0c;星海SABS与SABF系列整流桥&#xff0c;谁更契合你的电源需求&#xff1f;本文我们将深入分析这两个系列的相同点与不同点。相同点超薄封装设计…

作者头像 李华
网站建设 2026/2/27 22:15:37

应对 API 调用频率限制的自动化优化方案

一、引言&#xff1a;调用频率限制&#xff08;Rate Limit&#xff09;的挑战 挑战&#xff1a; 企业微信作为大型平台&#xff0c;对所有外部 API 调用都实施了严格的调用频率限制&#xff08;Rate Limit&#xff09;&#xff0c;以保护其系统资源和网络稳定性。不同的 API 接…

作者头像 李华
网站建设 2026/2/25 2:38:06

Wan2.2-T2V-A14B如何实现天气系统动态变化模拟

Wan2.2-T2V-A14B 如何实现天气系统动态变化模拟 在影视预演、气象科普和智慧城市的实际需求推动下&#xff0c;人们对“用一句话生成一段逼真自然现象视频”的期待正从幻想变为现实。想象这样一个场景&#xff1a;气象台值班员输入一句“未来两小时&#xff0c;杭州城区将经历一…

作者头像 李华
网站建设 2026/2/26 17:25:34

日期题模版(made by yyf)

日期题通常包括&#xff1a;判断是否为闰年&#xff0c;计算某年某月有多少天&#xff0c;日期自增&#xff0c;遍历日期等&#xff0c;这里给出总结判断是否为闰年首先什么是闰年&#xff0c;闰年具有哪些特征&#xff1f;如果是整百年&#xff08;如2000&#xff0c;1700&…

作者头像 李华