新闻详情

新闻详情

首页 / 资讯中心 / 详情

《从零手写操作系统 (05):物理内存管理器——让内核学会“分配”》

发布时间:2026/9/26 15:30:05来源:尧图网络
《从零手写操作系统 (05):物理内存管理器——让内核学会“分配”》
前言从“硬编码地址”到“动态分配”在前几章中我们所有的数据结构都依赖硬编码的绝对地址。GDT放在0x800栈顶固定在0x90000串口驱动直接操作0x3F8。这种方式在代码量小时尚可维持但随着内核功能膨胀你会陷入无尽的地址冲突噩梦新加一个缓冲区会不会覆盖页表栈向下增长会不会踩到BSS段本章我们将彻底终结这种原始状态。通过探测硬件报告的内存布局构建一个物理页帧分配器Physical Page Frame Allocator让内核首次拥有“申请内存”和“释放内存”的能力。这是迈向虚拟内存、进程管理和文件系统的地基。本章里程碑✅ 通过E820 BIOS调用获取真实物理内存布局✅ 设计并实现基于Bitmap的物理页帧分配器✅ 支持4KB对齐的页帧分配与释放✅ 串口打印内存统计信息验证分配器正确性核心概念E820内存映射与页帧抽象为什么不能假设内存从0开始连续x86 PC的内存空间从来不是连续的。BIOS ROM、显存、PCI设备MMIO区域会像岛屿一样散布在地址空间中。直接使用未探测的地址等于定时炸弹。E820是UEFI/BIOS提供的标准内存探测接口它返回一系列(base, length, type)三元组精确描述每一段内存的属性。Type值含义内核可用1Usable RAM✅ 可分配2Reserved❌ 不可用3ACPI Reclaimable⚠️ 解析ACPI后可回收4ACPI NVS❌ 不可用5Bad Memory / Undefined❌ 不可用为什么以4KB为分配单位x86分页机制的最小粒度是4KB页。物理分配器必须与页大小对齐否则无法映射到页表。我们将每个4KB块称为一个页帧Page Frame用唯一索引编号管理。分配器只需追踪哪些页帧空闲、哪些已占用。实战代码E820内存探测memory.c⚠️关键前提E820必须在实模式下调用。你需要在Stage1或Stage2的实模式阶段完成探测将结果存入一个全局数组再传递给32位C代码。以下展示32位侧的数据结构和解析逻辑。// memory.h #ifndef MEMORY_H #define MEMORY_H #include stdint.h #define E820_USABLE 1 typedef struct { uint64_t base; uint64_t length; uint32_t type; } __attribute__((packed)) e820_entry_t; // 由实模式代码填充链接器符号指向该数组 extern e820_entry_t e820_map[]; extern uint32_t e820_count; void memory_init(void); #endif// memory.c - 物理内存管理器 #include memory.h #include serial.h #define PAGE_SIZE 4096 // Bitmap: 1已分配, 0空闲 static uint8_t *pmm_bitmap; static uint32_t total_pages; static uint32_t used_pages; // 将字节数向上对齐到页边界 static inline uint32_t align_up(uint64_t val, uint32_t align) { return (val align - 1) ~(align - 1); } void memory_init(void) { uint64_t max_addr 0; // 第一遍找到最大可用地址确定Bitmap大小 for (uint32_t i 0; i e820_count; i) { if (e820_map[i].type E820_USABLE) { uint64_t end e820_map[i].base e820_map[i].length; if (end max_addr) max_addr end; } } total_pages (uint32_t)(max_addr / PAGE_SIZE); uint32_t bitmap_bytes (total_pages 7) / 8; // 第二遍在第一个足够大的Usable区域末尾放置Bitmap // 【注意】实际实现需确保Bitmap不与内核镜像重叠 // 这里简化演示假设0x100000处安全 pmm_bitmap (uint8_t *)0x100000; for (uint32_t i 0; i bitmap_bytes; i) pmm_bitmap[i] 0xFF; // 先全部标记为已占用 // 第三遍将Usable区域对应的bit清零标记为空闲 for (uint32_t i 0; i e820_count; i) { if (e820_map[i].type ! E820_USABLE) continue; uint32_t start_page align_up(e820_map[i].base, PAGE_SIZE) / PAGE_SIZE; uint32_t end_page (e820_map[i].base e820_map[i].length) / PAGE_SIZE; for (uint32_t p start_page; p end_page p total_pages; p) { pmm_bitmap[p / 8] ~(1 (p % 8)); } } // 保留前1MB和Bitmap自身所在页 uint32_t reserved_end align_up((uint64_t)pmm_bitmap bitmap_bytes, PAGE_SIZE) / PAGE_SIZE; for (uint32_t p 0; p reserved_end p total_pages; p) { pmm_bitmap[p / 8] | (1 (p % 8)); } used_pages 0; for (uint32_t i 0; i bitmap_bytes; i) { uint8_t b pmm_bitmap[i]; while (b) { used_pages b 1; b 1; } } kprintf([PMM] Total: %d pages (%d MB), Used: %d pages\n, total_pages, (total_pages * PAGE_SIZE) 20, used_pages); }页帧分配与释放// 分配单个4KB页帧返回物理地址失败返回0 uint32_t pmm_alloc_page(void) { uint32_t bitmap_bytes (total_pages 7) / 8; for (uint32_t i 0; i bitmap_bytes; i) { if (pmm_bitmap[i] 0xFF) continue; // 快速跳过满字节 for (int bit 0; bit 8; bit) { uint32_t page i * 8 bit; if (page total_pages) return 0; if (!(pmm_bitmap[i] (1 bit))) { pmm_bitmap[i] | (1 bit); used_pages; return page * PAGE_SIZE; } } } kprintf([PMM] ERROR: Out of physical memory!\n); return 0; } // 释放单个4KB页帧 void pmm_free_page(uint32_t phys_addr) { uint32_t page phys_addr / PAGE_SIZE; if (page total_pages) { kprintf([PMM] WARNING: Free invalid addr 0x%x\n, phys_addr); return; } pmm_bitmap[page / 8] ~(1 (page % 8)); used_pages--; }集成测试在内核入口函数中添加void kernel_main(void) { serial_init(); kprintf([Kernel] Booting...\n); memory_init(); // 测试分配与释放 uint32_t p1 pmm_alloc_page(); uint32_t p2 pmm_alloc_page(); kprintf([Test] Allocated: 0x%x, 0x%x\n, p1, p2); pmm_free_page(p1); uint32_t p3 pmm_alloc_page(); kprintf([Test] After freerealloc: 0x%x (should 0x%x)\n, p3, p1); kprintf([Kernel] PMM test passed.\n); }预期串口输出[PMM] Total: 32768 pages (128 MB), Used: 256 pages [Test] Allocated: 0x110000, 0x111000 [Test] After freerealloc: 0x110000 (should 0x110000) [Kernel] PMM test passed.关键细节解析1. Bitmap为什么放在Usable区域内Bitmap本身也需要内存。将它放在已探测的Usable区域末尾而非硬编码地址可以保证不会覆盖未知设备MMIO。但必须注意Bitmap所占的页帧必须在初始化时立即标记为已占用否则会被分配出去导致数据损坏。2. 为什么先全置1再清零Usable区域这是一种防御性编程策略。默认所有内存不可用只显式信任E820报告为Usable的区域。如果E820探测遗漏了某段内存它不会被误分配最多浪费一些RAM。反之如果默认全0遗漏的Reserved区域被分配将导致系统崩溃。3. 当前实现的局限性线性扫描pmm_alloc_page最坏情况O(n)后续可优化为空闲链表或Buddy System。无并发保护单核裸机环境暂不需要锁多核章节将补充自旋锁。仅支持单页分配连续多页分配需要扩展接口留待虚拟内存章节。调试Checklist内存分配异常排查症状可能原因排查方法Total pages 0E820数组未正确传递 / count0在实模式阶段kprintf打印e820_count验证分配的地址与内核重叠Bitmap放置位置未避开内核镜像检查linker.map确认内核结束地址Bitmap起始必须大于它free后重新分配到不同地址Bitmap位操作错误 / 未正确清零添加单元测试alloc→free→alloc断言两次地址相同串口报Out of memory但实际有RAMUsable区域未被正确标记为空闲打印每个E820条目的base/len/type手动验算bit范围黄金法则永远不要相信E820返回的地址可以直接使用。每次分配后用QEMU监控器xp /x [addr]验证该物理地址确实可读可写。本章小结与下一步今天我们完成了内核资源管理的第一个里程碑✅ 掌握了E820内存探测的原理与安全实践✅ 实现了生产级可用的Bitmap物理页帧分配器✅ 建立了“分配-使用-释放”的内存管理范式从此内核不再是一个靠硬编码地址苟活的玩具而是一个能自主管理资源的真正操作系统雏形。物理内存管理器是所有高级内存特性的基石。下一章预告《虚拟内存初探启用分页让每个程序拥有独立的4GB幻觉》有了物理分配器我们终于可以搭建页表、启用CR3、打开分页开关。下一章将实现恒等映射高半核映射让内核运行在3GB以上的虚拟地址空间为用户态进程的隔离打下基础。参考资料OSDev Wiki - Detecting Memory (E820)OSDev Wiki - Physical Memory ManagerIntel SDM Vol.3 Chapter 4 (Paging)本系列完整代码[你的GitHub仓库链接]Commit:m0n1o2p作者注这是《从零手写操作系统》系列的第05篇。物理内存管理器是第一个让你感受到“系统在生长”的章节。建议在分配器的每个关键路径都保留kprintf日志直到你完全信任它的正确性后再条件编译关闭。下一章我们开启分页时代
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

全球大模型一览表(2026年6月):TaoToken 统一 Key 接入 Claude Code 与通义灵码 2026/9/26 16:15:31

全球大模型一览表(2026年6月):TaoToken 统一 Key 接入 Claude Code 与通义灵码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
手写数字识别Python实战:MNIST模型训练与推理脚本封装 2026/9/26 16:15:31

手写数字识别Python实战:MNIST模型训练与推理脚本封装

简介:这份资源面向Python初学者与深度学习入门者,聚焦手写数字识别这一计算机视觉经典任务,帮助读者理解卷积神经网络如何自动提取图像特征并完成分类。包内共13个文件,以10张jpg手写数字样本图、1个py主程序、1个hdf5训练权重文件…

阅读更多 →
重磅福利!大学生速领:免费一年Cursor Pro专属特权(附TaoToken配置教程) 2026/9/26 16:15:25

重磅福利!大学生速领:免费一年Cursor Pro专属特权(附TaoToken配置教程)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
YOLOv5+OpenPose摔倒检测:从环境搭建到时序建模的完整实战 2026/9/26 16:15:25

YOLOv5+OpenPose摔倒检测:从环境搭建到时序建模的完整实战

简介:这份资源面向计算机视觉方向的本科毕业生与深度学习入门者,提供一套可直接运行的摔倒检测完整方案,解决人体姿态识别与跌倒行为判定的工程落地问题。项目以YOLOv5完成人体目标检测,结合OpenPose提取骨骼关键点,再…

阅读更多 →
MCP工具多了咋办,效率高吗?用TaoToken统一Key管好工具列表 2026/9/26 16:15:12

MCP工具多了咋办,效率高吗?用TaoToken统一Key管好工具列表

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Codex 接入真实项目:效率提升还是流程翻车?TaoToken 统一 Key 配置与回滚验证 2026/9/26 16:15:06

Codex 接入真实项目:效率提升还是流程翻车?TaoToken 统一 Key 配置与回滚验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉