操作系统学习16 物理内存探测与分配器(PMM)
操作系统学习16 物理内存探测与分配器PMM一、本章说明1. 前置知识2. 本章目标3. 目录结构4. 与 lesson15 的差异二、为什么不能直接“使用”所有内存三、BIOS INT 15hE820探测物理内存1. ARDS 是什么如何判定「无效 / 空」条目检测为 02. ARDS 结构20 字节3. 迭代查询流程4. 本课实现boot/loader.s四、物理页分配器PMM1. 位图约定2. 数据结构kernel/pmm.h3. 初始化四步pmm_init4. 分配与释放5. kernel_endkernel_end.s 本身它靠链接顺序“钉”在末尾c 侧使用对齐说明五、内核演示kernel/kernel.c六、本章小结一、本章说明1. 前置知识本章接续 lesson158253 PIT 抢占式多任务。前面已经能进保护模式、处理中断并做任务切换但物理内存地址仍依赖“假定机器有多少 RAM”的硬编码——换一台虚拟机配置就可能踩到空洞或设备预留区。2. 本章目标完成本章后我们将能够在实模式下用 BIOSINT 15h, AXE820h探测真实物理内存布局ARDS把杂乱的内存段清洗为4KB 物理页帧Page Frame用**位图Bitmap**实现物理内存分配器PMMpmm_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 的差异项目lesson15lesson16引导读盘后直接进 PM进 PM 前增加E820 探测内存管理无栈/任务栈静态数组Bitmap PMM多任务task.c/task.s IRQ0本章演示不再依赖调度内核入口创建 A/B 任务并切换初始化 PMM 并打印分配结果实模式读盘INT 15h E820计数0x8000 / ARDS0x8010进入保护模式pmm_initbitmap 管理 4KB 页alloc / free二、为什么不能直接“使用”所有内存系统开机时物理 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 15hE820探测物理内存1. ARDS 是什么ARDS全称Address Range Descriptor Structure地址范围描述符结构。可以把它理解成 BIOS 给出的一张「内存地契」一条 ARDS 物理地址空间里的一段连续区域说明这段从哪开始、有多长、能不能当普通 RAM 用。E820 不会一次把整张内存地图塞给你而是迭代返回多条 ARDS把它们拼起来就是整机的物理内存布局。例如典型机器可能返回条目大致范围Type含义#00x00000000起一小段1可用低端 RAM#10x000A0000附近2显存 / 设备预留不可当堆用#20x00100000起大段1可用扩展内存内核常在此……3/4 等ACPI 等特殊用途内核只把Type 1Usable的页交给分配器其它类型一律视为不可分配。如何判定「无效 / 空」条目检测为 0BIOS 不会用「全 0 结构」作为官方结束符——正常结束看的是EBX 0或 CF1。但缓冲区里仍可能出现全 0 槽位未写入的残留、被跳过的洞。判定方式/* type0 或 length0 → 无效直接跳过 */staticinlineintards_is_valid(constards_t*e){returne-type!0e-length!0;}字段为 0 时含义type 0不是合法 E820 类型合法值从 1 起视为空槽length 0零长度区间无意义跳过base与length皆 0典型的未初始化填充与上面两条一并过滤截图里[6][7]的type0就是这类脏槽。真正的条目数应以 loader 写入的ARDS count为准若 count 被破坏再按ards_is_valid过滤并钳制上限。2. ARDS 结构20 字节每条 ARDS 固定20 字节本课按经典 E820 请求部分 BIOS 还可返回 24 字节扩展属性此处不展开偏移大小名称含义0x008BaseAddr该段物理起始地址64 位0x088Length该段长度字节64 位0x104Type1可用 RAM2预留3ACPI 可回收4ACPI NVS0无效本课约定内存地图布局均在内核0xC200之前地址内容0x8000uint16有效 ARDS 条数0x8010ARDS 数组每项 20 字节3. 迭代查询流程每次调用返回一条 ARDS并用EBX作为 continuationEBX 0表示结束。CF1 或 EAX ! SMAPCF0 且 EAX SMAP是否设置 EAX0xE820, ECX20, EDXSMAP, EBX0INT 15h检查返回值失败 / 结束保存 ARDSDI20计数1EBX 0 ?结束继续循环4. 本课实现boot/loader.s在读盘成功、enter_pm之前调用detect_memoryARDS_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 ; type0 或 length0 → 跳过不前进 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!0e-length!0;}四、物理页分配器PMMx86 常见以4KB为一页。若物理上界为 128MB总页数 128 × 1024 × 1024 4096 32768 \text{总页数} \frac{128 \times 1024 \times 1024}{4096} 32768总页数4096128×1024×1024327681. 位图约定1 bit ↔ 1 个 4KB 页0 空闲1 已占用 / 不可用8 bit 管理 (8 \times 4\text{KB} 32\text{KB})管理 128MB 仅需约4KB位图。2. 数据结构kernel/pmm.htypedefstruct{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);/* 页对齐检查后清 0free_pages */分配是线性扫描位图教学实现清晰优先后续可改为 buddy / 空闲链表等。5. kernel_endkernel_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_addralign_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.cvoidmain(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*p1pmm_alloc_page();void*p2pmm_alloc_page();pmm_free_page(p1);/* ... */}Bochs 中典型输出内存大小随配置变化Lesson16: Physical Memory Manager (PMM) ARDS count: ... [0] base0x00000000 len... type1 ... PMM Init PMM state: mem31MB pages8176 used113 free8063_ bitmap0x0000F000 size1022_ 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 0x80002. 页框4KB 粒度上界 →total_pages3. 位图先全占再按 ARDS 释放保护内核与位图自身4. APIalloc/freebit 的 set / clear / test至此内核掌握了物理页的分配权。以后可以建立页表把“物理页”映射为进程可用的虚拟地址空间从而实现隔离与更灵活的内存布局。