操作系统学习16 物理内存探测与分配器(PMM)
操作系统学习16 物理内存探测与分配器(PMM)
- 一、本章说明
- 1. 前置知识
- 2. 本章目标
- 3. 目录结构
- 4. 与 lesson15 的差异
- 二、为什么不能直接“使用”所有内存?
- 三、BIOS INT 15h(E820)探测物理内存
- 1. ARDS 是什么?
- 如何判定「无效 / 空」条目(检测为 0)
- 2. ARDS 结构(20 字节)
- 3. 迭代查询流程
- 4. 本课实现(`boot/loader.s`)
- 四、物理页分配器(PMM)
- 1. 位图约定
- 2. 数据结构(`kernel/pmm.h`)
- 3. 初始化四步(`pmm_init`)
- 4. 分配与释放
- 5. kernel_end
- kernel_end.s 本身
- 它靠链接顺序“钉”在末尾:
- c 侧使用
- 对齐说明
- 五、内核演示(`kernel/kernel.c`)
- 六、本章小结
一、本章说明
1. 前置知识
本章接续 lesson15(8253 PIT + 抢占式多任务)。前面已经能进保护模式、处理中断并做任务切换,但物理内存地址仍依赖“假定机器有多少 RAM”的硬编码——换一台虚拟机配置就可能踩到空洞或设备预留区。
2. 本章目标
完成本章后,我们将能够:
- 在实模式下用 BIOS
INT 15h, AX=E820h探测真实物理内存布局(ARDS) - 把杂乱的内存段清洗为4KB 物理页帧(Page Frame)
- 用**位图(Bitmap)**实现物理内存分配器(PMM):
pmm_init/pmm_alloc_page/pmm_free_page - 在内核中打印 ARDS、总内存与分配/释放结果,在 Bochs 中验证
3. 目录结构
lesson16/ ├── Makefile ├── boot/ │ ├── loader.s # 读盘 + E820 探测 + 进保护模式 │ └── pm.inc # 段描述符宏 ├── kernel/ │ ├── main.c # 打印 ARDS / PMM 状态,演示 alloc/free │ ├── pmm.c / pmm.h # 位图物理页分配器 │ ├── kernel_end.s # 导出 kernel_end(位图紧随内核放置) │ ├── utilities.s # 显存输出等(沿用前几章) │ └── utilities.h └── README.md4. 与 lesson15 的差异
| 项目 | lesson15 | lesson16 |
|---|---|---|
| 引导 | 读盘后直接进 PM | 进 PM 前增加E820 探测 |
| 内存管理 | 无(栈/任务栈静态数组) | Bitmap PMM |
| 多任务 | task.c/task.s+ IRQ0 | 本章演示不再依赖调度 |
| 内核入口 | 创建 A/B 任务并切换 | 初始化 PMM 并打印分配结果 |
二、为什么不能直接“使用”所有内存?
系统开机时,物理 RAM 并不是连续且完全可用的。部分区域被固件、显存或主板设备占用:
0x00000000 +-----------------------------------+ | IVT / BIOS Data Area | <- 需谨慎使用 0x000A0000 +-----------------------------------+ | Video RAM (VGA 640KB~1MB) | <- 不可作普通 RAM 0x00100000 +-----------------------------------+ | Extended Memory(内核通常在此) | <- 可用 Usable | ... | +-----------------------------------+ | ACPI / Reserved / Device MMIO | <- 硬件预留 +-----------------------------------+盲目向0xA0000写数据会花屏;写入 Reserved 区可能直接崩溃。因此:先探测、后分配。
三、BIOS INT 15h(E820)探测物理内存
1. ARDS 是什么?
ARDS全称Address Range Descriptor Structure(地址范围描述符结构)。
可以把它理解成 BIOS 给出的一张「内存地契」:一条 ARDS = 物理地址空间里的一段连续区域,说明这段从哪开始、有多长、能不能当普通 RAM 用。
E820 不会一次把整张内存地图塞给你,而是迭代返回多条 ARDS;把它们拼起来,就是整机的物理内存布局。例如典型机器可能返回:
| 条目 | 大致范围 | Type | 含义 |
|---|---|---|---|
| #0 | 0x00000000起一小段 | 1 | 可用低端 RAM |
| #1 | 0x000A0000附近 | 2 | 显存 / 设备预留,不可当堆用 |
| #2 | 0x00100000起大段 | 1 | 可用扩展内存(内核常在此) |
| … | … | 3/4 等 | ACPI 等特殊用途 |
内核只把Type = 1(Usable)的页交给分配器;其它类型一律视为不可分配。
如何判定「无效 / 空」条目(检测为 0)
BIOS 不会用「全 0 结构」作为官方结束符——正常结束看的是EBX == 0(或 CF=1)。
但缓冲区里仍可能出现全 0 槽位(未写入的残留、被跳过的洞)。判定方式:
/* type==0 或 length==0 → 无效,直接跳过 */staticinlineintards_is_valid(constards_t*e){returne->type!=0&&e->length!=0;}| 字段 | 为 0 时含义 |
|---|---|
type == 0 | 不是合法 E820 类型(合法值从 1 起),视为空槽 |
length == 0 | 零长度区间,无意义,跳过 |
base与length皆 0 | 典型的未初始化填充,与上面两条一并过滤 |
截图里
[6][7]的type=0就是这类脏槽。真正的条目数应以 loader 写入的ARDS count为准;若 count 被破坏,再按ards_is_valid过滤并钳制上限。
2. ARDS 结构(20 字节)
每条 ARDS 固定20 字节(本课按经典 E820 请求;部分 BIOS 还可返回 24 字节扩展属性,此处不展开):
| 偏移 | 大小 | 名称 | 含义 |
|---|---|---|---|
0x00 | 8 | BaseAddr | 该段物理起始地址(64 位) |
0x08 | 8 | Length | 该段长度(字节,64 位) |
0x10 | 4 | Type | 1=可用 RAM;2=预留;3=ACPI 可回收;4=ACPI NVS;0=无效 |
本课约定内存地图布局(均在内核0xC200之前):
| 地址 | 内容 |
|---|---|
0x8000 | uint16有效 ARDS 条数 |
0x8010 | ARDS 数组(每项 20 字节) |
3. 迭代查询流程
每次调用返回一条 ARDS,并用EBX作为 continuation;EBX == 0表示结束。
4. 本课实现(boot/loader.s)
在读盘成功、enter_pm之前调用detect_memory:
ARDS_COUNT EQU 0x8000 ARDS_BUFFER EQU 0x8010 detect_memory: xor ebx, ebx xor ax, ax mov ds, ax mov es, ax mov di, ARDS_BUFFER mov word [ARDS_COUNT], 0 .e820_loop: mov eax, 0xE820 mov ecx, 20 mov edx, 0x534D4150 ; 'SMAP' int 0x15 jc .e820_done cmp eax, 0x534D4150 jne .e820_error ; type==0 或 length==0 → 跳过(不前进 DI,下次覆盖写) cmp dword [es:di + 16], 0 je .e820_skip cmp dword [es:di + 8], 0 jne .e820_accept cmp dword [es:di + 12], 0 je .e820_skip .e820_accept: add di, 20 inc word [ARDS_COUNT] .e820_skip: test ebx, ebx jnz .e820_loop .e820_done: ret对应 C 侧判定(与 20 字节条目一一对应):
typedefstruct{uint64_tbase_addr;uint64_tlength;uint32_ttype;}__attribute__((packed))ards_t;/* 检测是否为 0 / 无效条目 */staticinlineintards_is_valid(constards_t*e){returne->type!=0&&e->length!=0;}四、物理页分配器(PMM)
x86 常见以4KB为一页。若物理上界为 128MB:
总页数 = 128 × 1024 × 1024 4096 = 32768 \text{总页数} = \frac{128 \times 1024 \times 1024}{4096} = 32768总页数=4096128×1024×1024=32768
1. 位图约定
- 1 bit ↔ 1 个 4KB 页
0= 空闲,1= 已占用 / 不可用
8 bit 管理 (8 \times 4\text{KB} = 32\text{KB});管理 128MB 仅需约4KB位图。
2. 数据结构(kernel/pmm.h)
typedefstruct{uint8_t*bitmap;uint32_tbitmap_size;uint32_ttotal_pages;uint32_tfree_pages;uint32_tmax_mem_size;}pmm_manager_t;kernel_end由链接末尾的kernel_end.s导出,位图放在页对齐后的内核末尾,避免踩内核代码/数据。
3. 初始化四步(pmm_init)
- 扫描
type == 1的 ARDS,得到最大物理上界 →total_pages/bitmap_size - 位图默认全部置
0xFF(全部占用,安全第一) - 再把可用段对应页清 0,并累计
free_pages - 把低端内存 + 内核 + 位图自身重新置 1
半页处理:可用段起始页向上对齐、结束页向下对齐,避免把非完整页标成空闲。
4. 分配与释放
void*pmm_alloc_page(void);/* 找第一个空闲 bit,置 1,返回页物理地址 */voidpmm_free_page(void*p);/* 页对齐检查后清 0,free_pages++ */分配是线性扫描位图(教学实现,清晰优先;后续可改为 buddy / 空闲链表等)。
5. kernel_end
kernel_end.s 本身
链接时把一个空标签放到内核映像最后,标签的地址就是“内核结束地址”。
对于kernel_end.s本身:
- global kernel_end:把符号导出,给 C 用
- kernel_end::只是一个标签,后面没有任何指令/数据
- 所以它不占有效内容,只是一个地址标记
它靠链接顺序“钉”在末尾:
kernel.bin: main.o utilities.o pmm.o kernel_end.o$(LD)$(LDFLAGS)-Ttext0xC200...$(TARGET)main.o$(TARGET)utilities.o$(TARGET)pmm.o$(TARGET)kernel_end.o链接器按命令行顺序排布:[0xC200]main.o → utilities.o → pmm.o → kernel_end.o ↑ kernel_end 落在这里-Ttext 0xC200 指定内核从 0xC200 开始;kernel_end.o 放在最后,所以 kernel_end 的值 ≈ 内核映像末尾地址。
c 侧使用
externuint8_tkernel_end[];/* 由 kernel_end.s 提供,链在内核映像末尾 */pmm_init(ards,ards_count,(uint32_t)kernel_end);- 声明成数组,取的是地址,不是内容
- (uint32_t)kernel_end 就是内核结束地址
uint32_tbitmap_addr=align_up(kernel_end_addr,PAGE_SIZE);把位图放在 kernel_end 向上对齐到页边界之后,这样位图紧挨内核,又不会踩到内核代码/数据。
对齐说明
向上对齐是为了在页边界上安全地放下位图,且不踩内核。
假设kernel_end = 0xC345,页大小0x1000:
| 方向 | 结果 | 问题 |
|---|---|---|
| 向下对齐 | 0xC000 | 落进内核,会覆盖代码/数据 |
| 不齐 | 0xC345 | 位图和内核尾巴挤在同一页 |
| 向上对齐 | 0xD000 | 越过内核,从整页开始 |
PMM 按整页管理(分配、回收、预留都是PAGE_SIZE)。位图放在页对齐地址后:
- 内核最后那半页只属于内核,不会和位图混在一起
- 后面按页预留时更干净:
uint32_t reserved_end = align_up( (uint32_t)pmm.bitmap + pmm.bitmap_size, PAGE_SIZE); uint32_t reserved_pages = reserved_end / PAGE_SIZE;一句话:向下会覆盖内核;不齐会让一页里既有内核又有位图;向上才是“紧随内核之后、又按页对齐”的安全位置。
五、内核演示(kernel/kernel.c)
voidmain(void){ards_t*ards=(ards_t*)ARDS_BUFFER_ADDR;/* 0x8010 */uint16_tards_count=*(uint16_t*)ARDS_COUNT_ADDR;/* 0x8000 */clear_screen();/* 打印若干条 ARDS */pmm_init(ards,ards_count,(uint32_t)kernel_end);/* 打印 Total Memory / Total Pages / Free Pages */void*p1=pmm_alloc_page();void*p2=pmm_alloc_page();pmm_free_page(p1);/* ... */}Bochs 中典型输出(内存大小随配置变化):
Lesson16: Physical Memory Manager (PMM) ======================================== ARDS count: ... [0] base=0x00000000 len=... type=1 ... === PMM Init === PMM state: mem=31MB pages=8176 used=113 free=8063_ bitmap@0x0000F000 size=1022_ regions: [USED] 0x00000000-0x00010000 [#] [FREE] 0x00010000-0x0009F000 [.] [USED] 0x0009F000-0x00100000 [#] [FREE] 0x00100000-0x01FF0000 [...................] Allocated Page 1: 0x00010000_ Allocated Page 2: 0x00011000_ Freed Page 1. Free Pages: 8062_构建与运行:
makemakerun六、本章小结
| 步骤 | 实现内容 | 关键点 |
|---|---|---|
| 1. 探测 | loader.s中 E820 迭代 | 实模式、SMAP、ARDS @0x8000 |
| 2. 页框 | 4KB 粒度 | 上界 →total_pages |
| 3. 位图 | 先全占再按 ARDS 释放 | 保护内核与位图自身 |
| 4. API | alloc/free | bit 的 set / clear / test |
至此内核掌握了物理页的分配权。以后可以建立页表,把“物理页”映射为进程可用的虚拟地址空间,从而实现隔离与更灵活的内存布局。
