首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
ucore Lab3物理内存管理:从页表到首次适应算法的避坑指南
📅 2026/10/22 5:11:56
✍️ 爱科研究院
👁 阅读 3,247
1. 从Lab2的页表说起为什么物理内存管理是绕不过去的坎做ucore实验的人到了Lab3这里往往会卡住。Lab2刚把页表机制摸清楚虚拟地址到物理地址的映射链路刚刚跑通结果Lab3一上来就要求你管理物理内存的分配和回收。很多人第一反应是页表都搞定了物理内存还有什么好管的直接拿一块用一块不就行了这个想法在单任务、小内存的场景下确实没问题。但ucore是一个教学操作系统它要模拟的是真实操作系统面临的多任务、内存竞争、碎片化等一系列问题。物理内存管理是整个内存子系统的地基页表是建立在这个地基之上的映射规则。如果地基没打好上层再怎么折腾都是空中楼阁。我在第一次做Lab3的时候犯了一个很典型的错误把物理内存管理和虚拟内存管理混为一谈。具体来说我在实现alloc_pages的时候直接返回了虚拟地址然后在页表里做映射。结果跑测试用例的时候check_alloc_page那一关直接挂了。后来才想明白物理内存管理器只负责物理页的分配和回收它返回的必须是物理地址虚拟地址的映射是页表机制该干的事。这个边界如果搞混后面所有实验都会出问题。Lab3的核心任务其实就三件事第一搞清楚物理内存是怎么被组织起来的也就是pmm_manager这个结构体到底在管什么第二实现首次适应或者最佳适应算法来分配和释放物理页第三把物理内存管理和页表机制对接起来让虚拟地址能正确映射到分配到的物理页上。这三件事听起来简单但每一件都有坑。比如第一件事ucore把物理内存划分成了几个区域内核代码段、数据段、空闲内存区这些区域的边界在哪里怎么计算直接影响到后面分配器的正确性。第二件事首次适应算法虽然简单但空闲块链表的管理、合并相邻空闲块的逻辑写起来很容易出bug。第三件事页目录和页表的物理地址怎么和分配到的物理页关联boot_map_segment和page_insert这些函数的调用时机和参数传递稍不注意就会导致页表项错误。所以这篇文章不会给你一个标准答案让你抄而是把我踩过的坑、想明白的道理、以及最终跑通测试的完整思路拆开来讲。如果你正在做Lab3或者做完之后觉得似懂非懂这篇文章应该能帮你把物理内存管理这条线彻底理清楚。2. 物理内存的账本ucore到底是怎么给内存记账的2.1 从链接脚本看内存布局的起点要管理物理内存首先得知道物理内存里有什么。ucore的链接脚本kernel.ld定义了内核的加载地址和各个段的布局。在Lab3的语境下你需要关注几个关键符号end、KERNBASE、KERNTOP。end是内核代码段和数据段的结束地址这个符号之后的内存才是真正可以拿来分配的。但这里有一个容易忽略的细节end是虚拟地址还是物理地址在ucore的启动流程中内核被加载到KERNBASE通常是0x80000000开始的虚拟地址空间但物理上它可能被加载到0x1000001MB开始的位置。所以end这个符号的值是虚拟地址你需要把它转换成物理地址才能用于物理内存管理。我当时的做法是在pmm_init函数里先通过PADDR(end)把end转换成物理地址然后向上对齐到页边界得到freemem的起始物理地址。这个freemem就是物理内存分配器的起点。很多同学在这里直接用end的虚拟地址去初始化分配器结果分配出来的物理页根本不在实际物理内存范围内页表映射自然就错了。2.2 物理页描述符数组给每个物理页建一张身份证ucore用struct Page来描述一个物理页。这个结构体里最重要的字段是ref引用计数和flags标志位。ref记录了这个物理页被多少个页表项引用flags标记这个页是保留页、空闲页还是已分配页。pages数组的大小是怎么确定的在pmm_init里通过npage maxpa / PGSIZE计算得出。maxpa是物理内存的最大地址通常由npages或者硬件探测得到。在QEMU模拟环境下默认物理内存大小是128MB所以npage就是32768。每个struct Page占用的空间不大32768个描述符也就几百KB放在内核数据段里完全没问题。这里有一个设计上的取舍值得说一下为什么ucore选择用数组而不是链表来管理所有物理页描述符数组的好处是随机访问快通过物理地址除以页大小就能直接索引到对应的Page结构体时间复杂度O(1)。链表的话查找一个物理页对应的描述符需要遍历效率太低。但数组的缺点是大小固定不能动态扩展。对于教学操作系统来说物理内存大小是已知的数组方案完全够用。2.3 空闲页链表与分配器接口pmm_manager是ucore定义的物理内存管理器接口它包含init、init_memmap、alloc_pages、free_pages、nr_free_pages这几个函数指针。Lab3要求你实现的是default_pmm_manager也就是默认的物理内存管理器。init_memmap的作用是把一段物理内存标记为空闲并加入到空闲页链表中。这个函数的参数是struct Page *base和size_t n表示从base开始的n个物理页都是空闲的。在pmm_init里你会看到它被调用来初始化freemem到物理内存末尾之间的所有页。空闲页链表的管理方式直接决定了分配算法的效率。ucore的默认实现用的是双向链表每个空闲页的page_link字段串联起来。free_list是链表头nr_free记录空闲页总数。分配的时候从链表头取释放的时候插回链表头。这种设计实现简单但分配效率取决于空闲页的排列顺序。注意init_memmap里有一个容易写错的地方——base指针的偏移计算。base是pages数组中的某个元素的地址而n是要初始化的页数。你需要用for循环遍历这n个页把每个页的ref置0、flags置为PG_property然后插入空闲链表。如果循环边界写错比如写成i n就会多初始化一个页导致后续分配时越界。3. 首次适应算法从链表头取页到底哪里不靠谱3.1 首次适应的实现逻辑与边界条件首次适应算法的核心思想很简单从空闲链表头开始找到第一个满足要求的空闲块。但在ucore的Lab3里分配的单位是单个物理页不是任意大小的内存块。所以实际上alloc_pages每次只分配一个页首次适应退化成了“从链表头取第一个空闲页”。这个实现看起来毫无技术含量但边界条件特别多。首先分配之前要检查nr_free是否大于0如果空闲页数为0直接返回NULL。其次从链表头取下一个页之后要把这个页从链表中删除nr_free减1ref加1flags清除PG_property标志。最后返回这个页对应的物理地址也就是page2pa(page)。我见过很多同学在删除链表节点的时候写错指针操作。比如list_del(page-page_link);这行代码本身没问题但如果你在list_del之后还去访问page-page_link.next就会读到已经被修改的指针导致不可预期的行为。正确的做法是先用一个临时变量保存page指针删除之后再操作这个临时变量。3.2 释放页时的合并逻辑为什么需要检查相邻页free_pages的实现比alloc_pages复杂得多。释放一个物理页的时候你需要做几件事把ref减1如果ref变成0说明这个页不再被任何页表引用可以回收把flags重新设置为PG_property把页插回空闲链表nr_free加1。但这里有一个关键问题释放的页是否和空闲链表中已有的页相邻如果相邻应该合并成一个更大的空闲块。虽然Lab3的分配单位是单页合并与否不影响分配的正确性但合并可以减少空闲链表的长度提高后续分配的效率。ucore的默认实现其实没有做合并因为它的分配粒度就是单页合并的意义不大。但如果你要实现一个更通用的物理内存管理器比如支持连续多页分配的buddy系统合并就是必须的。这也是Lab3之后Lab4要引入buddy系统的原因。3.3 空闲页数量统计与调试技巧nr_free_pages函数直接返回nr_free变量即可。但nr_free的维护很容易出错。比如在alloc_pages里忘记减1或者在free_pages里忘记加1都会导致空闲页数量统计错误。这个错误不会立刻导致崩溃但会在后续的内存分配中逐渐暴露出来——要么提前耗尽内存要么分配了已经被占用的页。我的调试方法是在alloc_pages和free_pages里加打印语句每次操作后输出nr_free的值和当前空闲链表的长度。如果两者不一致说明链表操作有问题。另外可以用一个简单的测试用例连续分配10个页再连续释放10个页检查nr_free是否回到初始值。如果没回到说明释放逻辑有bug。提示在QEMU里跑ucore的时候可以用make qemu启动然后在pmm_init结束后打印nr_free的初始值。这个值应该等于(maxpa - freemem) / PGSIZE。如果对不上说明init_memmap的调用范围有问题。4. 物理内存与页表的对接那些让你怀疑人生的映射错误4.1 boot_map_segment与page_insert的分工boot_map_segment和page_insert是两个容易混淆的函数。boot_map_segment用于在启动阶段建立内核空间的映射它直接操作页目录和页表把一段连续的虚拟地址映射到连续的物理地址。page_insert用于在运行时建立用户空间的映射它需要分配新的页表页并处理页表项已存在的情况。在Lab3里boot_map_segment被用来映射内核代码段、数据段和pages数组。这些映射是恒等映射或者线性映射虚拟地址和物理地址之间只差一个KERNBASE的偏移。而page_insert在Lab3里主要用于测试用例验证物理页分配和页表映射的协同工作。我踩过的一个坑是在page_insert里调用alloc_page分配页表页的时候忘记检查返回值是否为NULL。如果物理内存耗尽alloc_page返回NULL后续的memset操作就会导致空指针解引用直接panic。正确的做法是struct Page *p alloc_page(); if (p NULL) { return -E_NO_MEM; }这个错误在测试用例里不一定能触发因为测试用例通常不会耗尽内存。但在实际运行中内存耗尽是一个必须处理的边界情况。4.2 页表项权限位与物理页标志的联动页表项里的权限位PTE_P、PTE_W、PTE_U和物理页的flags字段是两套独立的机制但它们之间有联动关系。比如当一个物理页被映射为只读时页表项的PTE_W位应该清除当物理页被多个进程共享时ref计数应该大于1。在Lab3的测试用例里有一个check_page_insert的测试它会检查page_insert之后页表项的权限位是否正确。如果你在page_insert里没有根据perm参数设置页表项这个测试就会失败。具体的设置逻辑是pte_t *ptep get_pte(pgdir, la, 1); if (*ptep PTE_P) { if (page2pa(page) ! PTE_ADDR(*ptep)) { page_remove(pgdir, la); } } *ptep page2pa(page) | PTE_P | perm;注意PTE_P是必须设置的表示页表项有效。perm参数包含了PTE_W和PTE_U等权限位。4.3 物理页引用计数与释放时机的判断ref计数是物理页管理的核心。每次page_insert成功建立映射ref加1每次page_remove解除映射ref减1。当ref减到0时物理页才可以被回收。这里有一个经典的错误在page_remove里直接调用free_page而没有检查ref是否已经减到0。如果ref还大于0说明还有其他页表项引用这个物理页此时释放会导致其他映射失效。正确的做法是if (--(page-ref) 0) { free_page(page); }另外page_remove还需要清除页表项的有效位并刷新TLB。ucore提供了tlb_invalidate函数来完成这个操作。如果不刷新TLBCPU可能仍然使用旧的页表项缓存导致访问到已经被释放的物理页。注意tlb_invalidate在单核环境下可以简化为直接刷新整个TLB但在多核环境下需要发送核间中断。ucore是单核教学系统所以直接调用invlpg指令即可。5. 测试用例背后的设计意图check_alloc_page到底在查什么5.1 check_alloc_page的检查项拆解check_alloc_page是Lab3的第一个测试用例它主要检查物理内存管理器的初始化是否正确。具体来说它会验证nr_free的初始值是否等于预期值以及alloc_pages和free_pages的基本功能是否正常。预期值的计算方式是(maxpa - freemem) / PGSIZE。maxpa是物理内存的最大地址freemem是内核结束后的第一个空闲物理页的地址。如果你在pmm_init里没有正确计算freemem这个测试就会失败。我当时的错误是把freemem设置成了end的虚拟地址而不是物理地址。结果nr_free的初始值比预期值大了很多因为虚拟地址0x80xxxxxx远大于物理地址0x1xxxxx。这个错误在测试输出里表现为nr_free的数值异常大一眼就能看出来。5.2 check_page_alloc与check_page的递进关系check_page_alloc检查的是alloc_pages和free_pages的多次调用是否正确。它会连续分配多个页然后检查这些页的物理地址是否在合法范围内以及nr_free是否相应减少。接着释放这些页检查nr_free是否恢复。check_page则更进一步它检查页表映射和物理页分配的协同工作。它会调用page_insert建立映射然后通过虚拟地址访问物理页验证数据是否正确。这个测试用例会暴露page_insert里的权限位设置错误、ref计数错误等问题。这两个测试用例的递进关系体现了ucore实验的设计思路先验证基本功能再验证协同工作。如果你在check_alloc_page就挂了说明物理内存管理器的初始化有问题如果check_alloc_page过了但check_page挂了说明页表映射和物理页管理的对接有问题。5.3 测试失败时的排查顺序当测试失败时不要盲目改代码。按照以下顺序排查先看pmm_init里的freemem计算是否正确。打印freemem的物理地址和nr_free的初始值和预期值对比。再看init_memmap的循环边界是否正确。确认n的值是否等于(maxpa - freemem) / PGSIZE。然后看alloc_pages和free_pages的链表操作是否正确。用简单的分配-释放测试验证nr_free是否恢复。最后看page_insert和page_remove的ref计数和权限位设置。用check_page的输出来定位具体是哪个页表项出了问题。这个排查顺序是从底层到上层从简单到复杂。很多同学一上来就盯着page_insert看结果发现是freemem算错了浪费了很多时间。6. 从Lab3到Lab4物理内存管理的扩展方向6.1 连续多页分配的需求与挑战Lab3的分配粒度是单页但实际操作系统经常需要分配连续的多个物理页比如DMA缓冲区、大页映射等。连续多页分配比单页分配复杂得多因为需要找到足够大的连续空闲块而且分配和释放过程中会产生外部碎片。解决外部碎片的经典方案是buddy系统。buddy系统把物理内存按2的幂次方划分成块分配时找到最小的满足要求的块释放时检查相邻的buddy块是否空闲如果空闲就合并成更大的块。Lab4会引入buddy系统替换掉Lab3的默认分配器。6.2 slab分配器与内核对象缓存除了buddy系统Linux内核还引入了slab分配器来管理小对象。slab分配器从buddy系统获取大块内存然后切割成固定大小的小块用于分配内核对象如task_struct、inode等。这样可以减少内部碎片提高分配效率。ucore的教学实验没有涉及slab分配器但理解slab的设计思想对理解真实操作系统的内存管理很有帮助。slab的核心是对象缓存每种内核对象有一个对应的cachecache里维护了若干个slab每个slab包含多个对象。分配对象时从slab的空闲链表中取释放时放回空闲链表。6.3 物理内存管理器的可扩展性设计ucore的pmm_manager接口设计得很好它把物理内存管理的具体算法抽象成了函数指针。你可以实现多个不同的pmm_manager然后在pmm_init里选择使用哪一个。这种设计模式叫策略模式它让算法的替换变得非常灵活。如果你要扩展ucore的物理内存管理可以考虑实现一个基于红黑树的分配器用红黑树来管理空闲块按块大小排序分配时快速找到最合适的块。或者实现一个基于位图的分配器用位图来标记每个物理页的占用情况分配时扫描位图找到空闲页。这些方案各有优劣选择哪种取决于具体的应用场景。提示在实现新的pmm_manager时记得在pmm_init里把pmm_manager指针指向你的实现并确保所有函数指针都正确初始化。如果某个函数指针是NULL调用时会直接panic。7. 那些让我熬夜的bug物理内存管理中的典型错误7.1 页表项地址计算错误导致的映射偏移页表项里存储的是物理地址但页表项本身的地址是虚拟地址。在get_pte函数里你需要通过KADDR宏把页目录项的物理地址转换成虚拟地址然后才能访问页表。如果忘记转换直接使用物理地址作为指针在开启分页之后就会访问到错误的地址。我当时的错误是在pgdir_walk函数里直接用pte_t *ptep (pte_t *)pgdir index来计算页目录项的地址。pgdir是页目录的虚拟地址加上索引之后还是虚拟地址这没问题。但页目录项里存储的页表物理地址需要用KADDR转换成虚拟地址才能访问。我漏掉了这一步结果访问页表时直接用了物理地址导致页表项读写错误。7.2 空闲链表操作中的指针悬挂问题链表操作是物理内存管理里最容易出bug的地方。list_del和list_add是ucore提供的链表操作宏它们直接操作list_entry结构体的prev和next指针。如果你在删除节点之后还去访问这个节点的next指针就会读到已经被修改的值。我遇到的一个典型场景是在free_pages里我先调用list_add把页插回空闲链表然后检查相邻页是否需要合并。但在检查相邻页的时候我用了page-page_link.next来获取下一个页而这个指针已经被list_add修改了指向了链表头。结果合并逻辑完全错乱。正确的做法是在修改链表之前先用临时变量保存需要的指针。比如struct Page *next list_next(page-page_link); list_add(page-page_link, free_list); // 使用next变量而不是page-page_link.next7.3 引用计数不平衡导致的页泄漏ref计数不平衡是另一个常见问题。每次page_insert应该让ref加1每次page_remove应该让ref减1。如果page_insert里漏掉了加1或者page_remove里漏掉了减1就会导致ref计数错误。ref计数偏小会导致物理页被提前释放其他映射访问到已释放的页产生不可预期的行为。ref计数偏大会导致物理页永远无法释放造成内存泄漏。在测试用例里内存泄漏可能不会立刻暴露但跑长时间的压力测试就会耗尽内存。我的调试方法是在alloc_pages和free_pages里加打印输出每次操作的物理页地址和ref值。然后手动跟踪一个物理页的完整生命周期分配时ref从0变1page_insert时ref从1变2page_remove时ref从2变1free_pages时ref从1变0。如果中间有任何一步对不上就说明有bug。7.4 物理地址与虚拟地址混用引发的panicucore里有两套地址物理地址和虚拟地址。PADDR宏把虚拟地址转物理地址KADDR宏把物理地址转虚拟地址。这两个宏用错了轻则映射错误重则直接panic。我印象最深的一次是在page_insert里我用page2pa(page)获取物理页的物理地址然后直接把这个物理地址赋给了页表项。这本身没问题页表项里存的就是物理地址。但接下来我用memset清零这个物理页的时候直接用了物理地址作为指针memset((void *)page2pa(page), 0, PGSIZE); // 错误在开启分页之后memset的参数必须是虚拟地址。物理地址0x1xxxxx在虚拟地址空间里可能映射到完全不同的位置甚至可能没有映射导致页错误。正确的做法是用page2kva(page)获取虚拟地址memset(page2kva(page), 0, PGSIZE); // 正确这个错误在QEMU里表现为一个页错误异常然后ucore打印出错误信息并进入死循环。排查的时候需要看CR2寄存器的值确认出错的虚拟地址是什么然后反推是哪个指针用错了。8. 写在最后物理内存管理教会我的事做完Lab3之后我对操作系统内存管理的理解上了一个台阶。以前觉得内存管理就是分配和释放没什么大不了的。真正动手实现之后才发现每一个看似简单的操作背后都有大量的边界条件和细节需要考虑。物理内存管理最核心的思想是“记账”每个物理页的状态空闲、已分配、被引用都必须准确记录任何一笔账记错了整个系统就会出问题。ref计数、flags标志、空闲链表这些都是记账的工具。工具本身不复杂难的是保证每一笔账都记得对、记得全。另一个体会是地址转换是操作系统里最容易出错的地方。物理地址和虚拟地址的混用、页表项的权限位设置、TLB的刷新时机这些细节稍不注意就会导致难以调试的bug。我的建议是在写任何涉及地址操作的代码之前先画一张图把虚拟地址、物理地址、页目录、页表、页表项之间的关系标清楚。图画明白了代码自然就写对了。最后说一个实用的调试技巧在QEMU里跑ucore的时候可以用-d int参数打开中断和异常的调试输出。当发生页错误时QEMU会打印出错误码和出错的虚拟地址。结合ucore自己打印的panic信息可以快速定位到出错的代码位置。这个技巧在调试Lab3的页表映射问题时特别有用。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/22 5:11:56
Java老兵转型AI Agent实战复盘:从思维范式到工程落地的四个月
2026/10/22 5:11:56
大模型选型实战:Gemini 4 Argon、GPT-6、Claude 成本与用途全解析
2026/10/22 5:06:55
自己维护一份AI日报:从信息焦虑到信号筛选
2026/10/22 7:22:03
LangChain4j 流式输出:打字机效果 + SSE,回答不再干等三秒
2026/10/22 7:22:03
【脚本】 【Linux】重命名网卡
2026/10/22 7:22:03
Webrtc本地服务器hub实现细节和信令交互
2026/10/22 7:22:03
淘宝店小蜜有必要开吗?商家实测后的取舍
2026/10/22 7:22:03
【研发类-框架和库Skills】django-pro 技能
2026/10/22 7:17:03
create_agent 实战:一个 Agent 是怎么被组装出来的
2026/10/22 0:01:24
电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
2026/10/22 0:01:24
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
2026/10/22 0:01:24
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定
2026/10/21 19:05:16
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/21 14:47:26
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/21 19:05:04
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/22 5:06:15
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/22 2:07:18
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)