buddy system :分离适配的一个特例,每个大小类都是2的幂。
- 基本思路:假设一个堆大小2^m个字,为每一个块大小为2^k维护一个分离空闲链表。请求块大小向上舍入到最接近的2的幂。
- 最开始只有一个大小为2^m个字的空闲块。
- 优点:快速合并和快速搜索
- 缺点:要求块大小为2的幂导致显著的内存碎片。
-检查一个数x是否是2的幂。在计算机科学中,2的幂的数在二进制表示中只有一个1,其余位都是0。例如,2的幂有1(1)、2(10)、4(100)、8(1000)等。
现在,看看宏的逻辑:!((x) & ((x)-1))。
- define MAX(a,b)((a)>(b)?(a):(b))在宏定义中为什么每个变量都要加上括号呢?
- 在C/C++宏定义中,由于宏展开是简单的文本替换,如果不加括号,可能会导致运算符优先级问题,从而产生意想不到的结果。
 以MAX(a,b)为例,如果定义为 define MAX(a,b) a>b?a:b,那么当在表达式中使用宏时,可能会因为运算符优先级而错误计算。
 例如:考虑 MAX(1,2) + 3,展开后为 1>2?1:2+3,这相当于 1>2?1:(2+3),因为加法运算符的优先级高于条件运算符。所以结果是5,而不是我们期望的(1>2?1:2)+3=3。
 同样,如果参数是表达式,比如 MAX(i++, j++),展开后为 i++>j++?i++:j++,这会导致变量自增两次,而不是一次。
因此,为了安全,每个参数和整个表达式都应该用括号括起来。
正确的定义: define MAX(a,b) ((a) > (b) ? (a) : (b))
这样,MAX(1,2)+3 展开为 ((1) > (2) ? (1) : (2)) + 3,条件运算符的优先级较低,所以先计算条件表达式,然后再加3,结果正确。
但是,即使这样,仍然不能避免参数多次求值的问题(例如,当参数是自增表达式时)。因此,在C++中,通常建议使用内联函数来代替宏,以避免此类问题。
总结:加括号是为了确保宏展开后的表达式运算顺序与预期一致,防止因运算符优先级导致的错误。
- 
. 数学证明
对于任意正整数 n,设其最高位1在第 k 位,那么:
- 
执行 n |= n >> 1后,第k-1位变成1
- 
执行 n |= n >> 2后,第k-2到k-3位变成1
- 
以此类推,经过 log₂(字长)次操作后,第0到第k位全部变成1
最后 n + 1 就得到了 2^(k+1),即大于原数的最小2的幂。
命令./start_qemu4.sh
- 哭,docker 配置
- docker start -ai qemu4
- 
entry.s做了什么 - 保存硬件传入的hartid和dtb物理地址到全局符号,共c代码阅读。
- 启用sv39分页,将预制的三级页表的物理页号写入satp,并使用Sfence,vma1刷新
- 切换到高地址虚拟空间,设置内核栈指针sp
- 跳转到C入口
- 数据段提供:内核引导栈空间 预制三级页表内存 用于保存boot_hartid/boot_dtb的全局变量
 
- 
硬件进入引导入口 → entry.S:kern_entry 执行低级初始化。 
- 
准备虚拟内存环境: 
- 
使用预制的 1GiB 大页映射:将高半地址 0xffffffffc0000000 段映射到物理 0x80000000(OpenSBI 后的 DRAM 起点),方便“高半内核”直接访问内核与物理内存。 
- 
写 satp 为 Sv39 模式 + 预制页表物理页号,并刷新 TLB。 
- 
切栈并跳转到 kern_init()(C 世界): 
- 
C 端清 BSS、解析 DTB(获取实际物理内存布局)、初始化控制台、初始化物理内存管理(选择策略并建自由链/伙伴树)、自检。 
- 有关页表:
- 
每级 512 项(4KiB/8B)。 
- 
叶子判断:V=1 且 (R|W|X)!=0。 
 
- 
page_init函数
为什么首地址向上,尾地址向下?不理解
Thoughtfor 3s
[MODE: RESEARCH]
因为要保证交给物理页分配器的区间正好由“完整页框”组成,既不覆盖已占用的碎片,也不越界到无效区域。设区间是半开区间 [begin, end):
- 
首地址向上对齐 ROUNDUP(begin, PGSIZE) 
- 
若 begin 落在某页的中间,页前半部分可能已被“内核镜像/元数据”占用,不能把这页剩余部分当作空闲整页使用,所以必须跳到“下一页的起点”。 
- 
尾地址向下对齐 ROUNDDOWN(end, PGSIZE) 
- 
若 end 落在某页的中间,这一页后半部分不足一整页,不能算作一个可分配页框,必须截断到“这一页的起点”,排除尾部碎片。 
高地址 →
┌───────────────────────────────────────────┐
│ 内核镜像 (.text .rodata .data .bss ... ) │
│                 直到符号 end              │
└─────────────── end ───────────────────────┘
                │
                │  向上页对齐 ROUNDUP(end, PGSIZE)
                ▼
          pages (VA)  ← 页描述符数组的起始虚拟地址
                │
                │  数量 = (npage - nbase)
                │  总字节 = sizeof(struct Page) * (npage - nbase)
                ▼
pages + sizeof(Page)*(npage-nbase) (VA)  ← 元数据数组末尾的“下一个地址”
                │
                └─ 这还在 VA,需要用 PADDR 转为物理地址
                
                
低地址 →
┌───────────────────────────────────────────┐
│    物理内存中被内核镜像占用的区域         │  (映射到上图“内核镜像”)
└───────────────────────────────────────────┘
┌───────────────────────────────────────────┐
│   物理中存放 struct Page[] 数组的区域     │  (映射到上图 pages..pages+size)
└───────────────────────────────────────────┘
↑
│
freemem = PADDR(pages + sizeof(Page)*(npage-nbase))  ← 元数据之后的第一个可用物理地址
│
└─ 接下来会对齐到页边界:mem_begin = ROUNDUP(freemem, PGSIZE)
 Buddy System 的核心机制
1. 数据结构基础
Buddy System 使用完全二叉树而不是链表:
- 
树节点:每个节点记录”该子树中最大可用块的大小” 
- 
数组存储:用数组 buddy_longest[2*buddy_size-1] 存储二叉树 
- 
索引关系:节点 i 的左孩子是 2i+1,右孩子是 2i+2 
2. 内存布局图
 
Apply to lab2.md
 
物理内存布局:
 
┌─────────────────────────────────────────────────────────────┐
 
│                   原始内存区域 base..base+n                  │
 
├─────────────────────────────────────────────────────────────┤
 
│  元数据页区域    │        可分配数据区域                      │
 
│ meta_base       │        buddy_base_page                     │
 
│ (存储longest树) │        (实际可分配内存)                     │
 
│                 │                                            │
 
│ buddy_longest[] │  [块0] [块1] [块2] [块3] [块4] [块5] [块6] │
 
│ (2*buddy_size-1)│                                            │
 
└─────────────────────────────────────────────────────────────┘3. 伙伴树结构图
 
Apply to lab2.md
 
完全二叉树结构 (buddy_size = 4 为例):
 
                    根节点(0)
 
                   longest[0] = 4
 
                  /              \
 
                 /                \
 
           左子树(1)              右子树(2)
 
         longest[1] = 2        longest[2] = 2
 
         /          \           /          \
 
        /            \         /            \
 
   叶节点(3)      叶节点(4)  叶节点(5)    叶节点(6)
 
longest[3]=1   longest[4]=1 longest[5]=1 longest[6]=1
 
对应的内存块:
 
┌─────┬─────┬─────┬─────┐
 
│块0  │块1  │块2  │块3  │
 
│1页  │1页  │1页  │1页  │
 
└─────┴─────┴─────┴─────┘4. 分配过程图解
请求分配 3 页(实际分配 4 页):
 
Apply to lab2.md
 
步骤1: 向上取整
 
请求: 3页 → 实际分配: 4页 (2^2)
 
步骤2: 树中查找路径
 
根节点(0): longest[0]=4 >= 4 ✓
 
├─ 左子树(1): longest[1]=2 < 4 ✗
 
└─ 右子树(2): longest[2]=2 < 4 ✗
 
   但右子树是唯一选择,继续向下
 
步骤3: 到达叶节点
 
最终到达叶节点(6),对应块3
 
标记: longest[6] = 0 (占用)
 
步骤4: 更新祖先
 
向上回溯更新所有祖先节点的longest值5. 释放过程图解
释放块3(4页):
 
Apply to lab2.md
 
步骤1: 定位节点
 
通过偏移计算找到叶节点(6)
 
步骤2: 标记空闲
 
longest[6] = 1 (空闲)
 
步骤3: 向上合并
 
检查父节点(2)的左右子树:
 
- 左子树(5): longest[5] = 1
 
- 右子树(6): longest[6] = 1
 
- 相等且都为空闲 → 合并!
 
- longest[2] = 2 (合并为2页块)
 
继续向上检查根节点(0)6. 模块架构图
 
 
Buddy System 模块架构:
 
┌─────────────────────────────────────────────────────────────┐
 
│                    Buddy System 模块                        │
 
├─────────────────────────────────────────────────────────────┤
 
│  全局状态管理                                                │
 
│  ┌─────────────────────────────────────────────────────────┐ │
 
│  │ buddy_base_page: 可分配区域基址                         │ │
 
│  │ buddy_longest:   伙伴树数组指针                         │ │
 
│  │ buddy_size:      管理总页数(2的幂)                      │ │
 
│  │ nr_free:         当前空闲页数                            │ │
 
│  └─────────────────────────────────────────────────────────┘ │
 
├─────────────────────────────────────────────────────────────┤
 
│  工具函数层                                                  │
 
│  ┌─────────────────────────────────────────────────────────┐ │
 
│  │ IS_POWER_OF_2(): 判断2的幂                              │ │
 
│  │ roundup_pow2():  向上取整到2的幂                        │ │
 
│  │ floor_pow2():    向下取整到2的幂                        │ │
 
│  │ page_kva():      页到虚拟地址转换                       │ │
 
│  └─────────────────────────────────────────────────────────┘ │
 
├─────────────────────────────────────────────────────────────┤
 
│  核心算法层                                                  │
 
│  ┌─────────────────────────────────────────────────────────┐ │
 
│  │ buddy_init():          初始化全局状态                   │ │
 
│  │ buddy_init_memmap():   建立伙伴树结构                   │ │
 
│  │ buddy_alloc_pages():   树中查找+分配                   │ │
 
│  │ buddy_free_pages():    树中定位+合并                   │ │
 
│  │ buddy_nr_free_pages(): 返回空闲页数                    │ │
 
│  └─────────────────────────────────────────────────────────┘ │
 
├─────────────────────────────────────────────────────────────┤
 
│  接口适配层                                                  │
 
│  ┌─────────────────────────────────────────────────────────┐ │
 
│  │ buddy_pmm_manager: 符合pmm_manager接口的结构体         │ │
 
│  │ .name = "buddy_pmm_manager"                            │ │
 
│  │ .init = buddy_init                                     │ │
 
│  │ .init_memmap = buddy_init_memmap                       │ │
 
│  │ .alloc_pages = buddy_alloc_pages                       │ │
 
│  │ .free_pages = buddy_free_pages                         │ │
 
│  │ .nr_free_pages = buddy_nr_free_pages                   │ │
 
│  │ .check = buddy_check                                   │ │
 
│  └─────────────────────────────────────────────────────────┘ │
 
└─────────────────────────────────────────────────────────────┘7. 与 First/Best Fit 的根本区别
| 特性 | First/Best Fit | Buddy System | 
|---|---|---|
| 数据结构 | 双向链表 | 完全二叉树(数组) | 
| 分配策略 | 线性搜索 | 树形搜索 | 
| 块大小 | 任意大小 | 必须是2的幂 | 
| 时间复杂度 | O(n) | O(log n) | 
| 合并机制 | 相邻块合并 | 伙伴块合并 | 
| 碎片类型 | 外部碎片 | 内部碎片 | 
8. 关键设计思想
- 
2的幂约束:所有分配都向上取整到2的幂,简化管理 
- 
伙伴关系:只有”同层同大小且地址相邻”的块才能合并 
- 
树形管理:用二叉树快速定位和更新,避免线性搜索 
- 
元数据分离:伙伴树存储在独立页中,避免被分配 
总结:Buddy System 完全摒弃了链表管理,改用二叉树+数组的高效数据结构,通过2的幂约束和伙伴合并机制,实现了 O(log n) 的快速分配和自动合并,是现代操作系统内存管理的重要算法。
Footnotes
- 
- sfence: 代表 “Store Fence”,即存储屏障。它确保在屏障之前的所有存储(写)操作,在屏障之后的任何操作看来,都已经完成。
- vma: 代表 “Virtual Memory Address”。这指明了这条屏障指令的特定用途——与虚拟内存地址转换有关。 所以,- sfence.vma合起来的意思是:确保所有在它之前发出的、与页表(Page Table)相关的存储操作,对后续的指令地址转换是可见的。