ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

页式内存管理与地址转换:缺页处理、页面置换到Linux实践

页式内存管理与地址转换:缺页处理、页面置换到Linux实践 课堂练习4.2 页式内存管理这一题我带过几届人做规律特别明显真正卡住大家的从来不是“页”这个概念本身而是把它和实际地址计算、页表组织、缺页处理串起来的那几步。很多人能顺口背出“页式内存管理把逻辑地址空间切成等长的页把物理内存切成等长的页框”可真给他一个十六进制逻辑地址和一张页表让他算物理地址手就开始抖——要么忘了页内偏移原样保留要么把页号当成了页框号要么在被问到“为什么不用单级页表”的时候只会说“因为太大了”说不出大多少、大在哪里。内存管理这四个字听着抽象其实是操作系统里最讲“算术”的一块内容凡是能用公式推、能用代码跑出来的东西都不该靠背。这篇就按我在机房带练习的实际顺序走先把页式管理要解决的东西讲透再把地址转换这条主线走一遍并配手算然后给两段能直接编译运行的代码一段模拟 MMU 做地址翻译一段把 FIFO、LRU、Clock、OPT 四种页面置换算法放在同一条引用串上跑出对比结果最后落到真实系统看看 Linux 内存管理子系统里 mm_struct、vm_area_struct 这些数据结构到底是怎么把课堂上的抽象页表变成能跑的东西。学有余力的可以照着改参数做实验只求过关的把第 2 章和第 6 章的表格抄进笔记考试和作业都够用。1. 先搞清楚页式内存管理到底在解决什么问题1.1 连续分配的三个坑踩过才知道疼要理解分页为什么出现得先看它替代的是什么。早期的内存分配走的是连续分配路线一个进程进来就给它一整块连续的物理内存从某个基址开始长度等于进程大小。这做法直观地址转换也简单基址寄存器加偏移就完事硬件代价极低。但它有三个绕不过去的坑而且一个比一个难缠。第一个坑是外部碎片。内存里进程反复地申请和释放日子久了就会剩下一堆零散的小空洞。这些空洞加起来可能有好几百 MB但没有一个足以装下新来的进程。解决办法是把进程挪一挪、把空洞挤到一起也就是“紧凑”可紧凑要拷贝整块内存、要暂停进程、要更新所有地址引用代价高得离谱绝大多数系统根本不常用。第二个坑是内部碎片。为了避免外部碎片有的方案改成按固定大小的分区分配但进程大小是任意的分区给大了就浪费给小了又装不下。这部分被浪费在分区内部、进程用不到的空间就是内部碎片。有意思的是分页其实也有内部碎片只是它把碎片限制在“最后一页”这个很小的范围内这就是取舍的艺术。第三个坑是进程空间必须连续。连续分配要求进程的物理内存是连续的一整块这意味着内存分配器得像停车场找车位一样必须找到一块够大的连续区域。而进程还常常需要动态增长比如堆往上长、栈往下长连续分配下你得提前预留空间预留多了浪费少了又不够。分页把这些约束一次性全打开了——进程的页可以散落在物理内存的任意位置只要页表记得住就行。1.2 分页的核心思想等长切块加一张对照表分页的思路其实非常朴素。把进程的逻辑地址空间按固定大小切成一块块叫页把物理内存也按同样大小切成一块块叫页框有的教材叫物理块、帧。页和页框大小完全一致这是硬性前提。然后进程的每一页可以装进任意一个空闲页框里不需要连续。哪个页放在哪个页框由一张页表记录。打个生活化的比方。你把一本三百页的书拆散每一页单独塑封然后随便塞进图书馆书架的任意空位。书页之间不再要求挨着但你在书末的目录里记一笔“第 17 页在 B 区 3 层第 5 格”。想找第 17 页先查目录拿到位置再过去取。页表就是这本目录页号是目录条目的编号页框号是书架上格子的编号。这样做的收益很直接只要有空位就能塞外部碎片直接消失了而且目录本身也可以拆散存放进一步省空间。代价也很清楚。第一每次访问内存都要先查页表内存访问次数翻倍这就是为什么必须引入 TLB。第二页表本身占用内存页表规模一大就得想办法压缩于是有了多级页表。第三页是物理单位不是逻辑单位它不管你的代码段、数据段边界在哪一个函数可能正好跨在两页上。课堂练习里的很多题考的就是你有没有把这些代价算清楚。1.3 页式和分段最容易搞混的几个点练习卷里最常见的一种送命题是把分页和分段混在一起问。两者确实都用了“离散分配”这个手段但出发点完全不同用一张表把它们摆开看最省事。对比维度页式管理分段管理划分依据物理单位按固定大小机械切分逻辑单位按程序结构切分代码段、数据段、栈段大小固定由硬件决定如 4KB可变由程序逻辑决定对用户可见性对程序员透明看不见页的存在对程序员可见段是程序的一部分地址结构页号 页内偏移段号 段内偏移主要解决的问题外部碎片、内存利用率共享、保护、动态增长碎片类型只有最后一页的内部碎片主要是外部碎片真正考试里更狠的是“段页式”也就是先分段再分页逻辑地址变成段号、段内页号、页内偏移三段。我第一次做这种题的时候就是漏掉了段表里还要存页表长度导致越界判断写错。记住一条段页式里每个段有自己的页表段表项指向该段页表的基址同时记录该段的页数用于越界检查。这句话能挡掉一大半概念题。2. 地址转换这条主线从逻辑地址到物理地址2.1 地址结构的位拆解与两条公式页式管理的所有计算都建立在同一个拆解上逻辑地址被切成两部分高位是页号低位是页内偏移。设页大小为 2 的 n 次方字节那么逻辑地址的低 n 位就是页内偏移剩下的高位就是页号。用除法和取模表达就是页号 逻辑地址 / 页大小整数除法向下取整 页内偏移 逻辑地址 % 页大小取余高位存放页号、低位存放偏移这个设计不是随便定的它有个非常实际的好处页大小取 2 的整数次幂时除法和取模退化成移位和按位与硬件一条指令就能完成。这就是为什么实际系统里的页大小几乎都是 4KB、8KB、16KB、2MB、1GB 这种 2 的幂而不是 5000 字节之类的“整数”。你去看 Linux 支持的页大小全是 2 的幂原因就在这里。换算成位运算页大小 4KB 等于 2 的 12 次方所以偏移占低 12 位页号是逻辑地址右移 12 位// 页大小 4KBPAGE_SHIFT 12 #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL PAGE_SHIFT) // 4096 #define PAGE_MASK (PAGE_SIZE - 1) // 0xFFF unsigned long page_offset addr PAGE_MASK; // 页内偏移低12位 unsigned long page_number addr PAGE_SHIFT; // 页号高位这两行代码建议直接背下来。后面的所有题目无论是手算还是写程序都是在这两行上做文章。我踩过的一个坑是页大小换成 8KB 的时候忘记把 PAGE_SHIFT 从 12 改成 13结果所有地址都算错了一倍排查了半天才发现是宏没跟着改。2.2 手算一遍单级页表转换把每一步都写清楚光有公式不够练的时候必须落到具体数字上。假设这样一个场景32 位逻辑地址空间页大小 4KB页表内容如下表现在要算逻辑地址 14927十六进制 0x00003A4F对应的物理地址。页号页框号0512293841第一步求页号14927 ÷ 4096 3 余 2639所以页号是 3页内偏移是 2639。用位运算验证一下14927 的十六进制是 0x3A4F低 12 位是 0xA4F 也就是 2639高位是 0x3 也就是 3对得上。第二步查页表页号 3 对应的页框号是 8。第三步算物理地址物理地址 页框号 × 页大小 页内偏移 8 × 4096 2639 32768 2639 35407十六进制是 0x8A4F。注意最后这个结果的结构高位 0x8 就是页框号低位 0xA4F 就是页内偏移偏移部分一个字都没变。这是页式地址转换最关键的性质也是我自己讲过很多遍还会有人写错的地方——偏移量在转换前后完全相同你只需要把页号那一段替换成页框号。理解了这一点很多题可以直接用“拼接”的方式口算不用真的去做乘法和加法。还有两个细节要留意。一是越界检查查表之前必须先比较页号和页表长度如果页号大于等于页表长度说明访问越界了要触发地址越界错误而不是去查一个不存在的表项。二是有效位检查表项里通常有个有效位valid bit标记这一页当前是否真的在内存里无效就得触发缺页中断。练习题里经常故意给一个有效位为 0 的表项看你会不会直接拿去算物理地址。我见过最典型的错误就是拿到页表项直接用完全无视有效位。2.3 多级页表为什么必须存在把账算给质疑的人看每次讲到这里都有人问单级页表不是挺好吗为什么要搞两级、三级、四级答案很简单把内存占用量算出来就一目了然了。还是 32 位地址空间、页大小 4KB 这个配置。页内偏移占 12 位剩下 20 位是页号也就是说一个进程最多有 2 的 20 次方也就是约 104 万个页。每个页表项假如占 4 字节那么一个进程的页表就要 1048576 × 4 字节 4MB。如果系统同时有 100 个进程在跑光页表就要 400MB。这在 32 位系统只有 4GB 地址空间的年代简直是灾难。多级页表的思路是给页表本身也分页。改成两级之后20 位的页号被拆成两段各 10 位高 10 位是页目录索引低 10 位是页表索引。顶级页目录有 2 的 10 次方也就是 1024 个条目每个条目 4 字节总共 4KB每个二级页表也是 1024 个条目、4KB。关键在于二级页表可以按需存在。一个进程实际用到的地址空间往往只集中在少数几个区域比如代码段、数据段、堆、栈对应的二级页表可能只有几个其余的根本不用分配。这样实际占用的页表内存可能只有几十 KB比 4MB 小了两个数量级。不过这算盘不能只算一半。多级页表也有它的代价一次地址转换需要多次访存。两级页表要访问两次内存才能拿到真正的页框号四级就是四次这是实打实的开销。所以多级页表必须和 TLB 配合使用才有意义——TLB 命中时这些多级查找全部被跳过。这也就解释了一个常见疑问为什么 Linux 决定用四级页表多一级不是更慢吗因为多出来的那一级只在 TLB 未命中时才起作用而 TLB 命中率在实际负载下通常能到 98% 以上用一点点未命中路径的开销换取页表空间的巨大节省这笔账非常划算。3. 课堂练习4.2 的动手实现两段能直接跑的代码3.1 第一段模拟硬件 MMU 做地址翻译课堂上写地址转换很多同学是拿笔一步步算算完也不知道对不对。我的建议是当场写个小程序把规则固化成代码跑几个用例验证心里就踏实了。下面这段 C 代码实现了一个简化版的 MMU给定页表数组和一个逻辑地址输出物理地址同时做越界检查和有效位检查。#include stdio.h #include stdint.h #define PAGE_SHIFT 12 #define PAGE_SIZE (1u PAGE_SHIFT) /* 4096 */ #define PAGE_MASK (PAGE_SIZE - 1) #define PT_ENTRIES 16 /* 本练习只用 16 个页表项 */ #define VALID 1 #define INVALID 0 typedef struct { uint32_t frame; /* 页框号 */ uint8_t valid; /* 有效位1 在内存0 不在 */ } pte_t; /* 返回 0 成功-1 越界-2 缺页 */ int translate(const pte_t *pt, uint32_t vaddr, uint32_t *paddr) { uint32_t pageno vaddr PAGE_SHIFT; uint32_t offset vaddr PAGE_MASK; if (pageno PT_ENTRIES) { printf(地址越界页号 %u 超过页表长度 %d\n, pageno, PT_ENTRIES); return -1; } if (pt[pageno].valid ! VALID) { printf(缺页页号 %u 不在内存需触发缺页中断\n, pageno); return -2; } *paddr (pt[pageno].frame PAGE_SHIFT) | offset; printf(逻辑地址 0x%08X - 页号 %u, 偏移 %u - 页框 %u - 物理地址 0x%08X\n, vaddr, pageno, offset, pt[pageno].frame, *paddr); return 0; } int main(void) { pte_t pt[PT_ENTRIES] {0}; pt[0].frame 5; pt[0].valid VALID; pt[1].frame 2; pt[1].valid VALID; pt[2].frame 9; pt[2].valid VALID; pt[3].frame 8; pt[3].valid VALID; pt[4].frame 1; pt[4].valid INVALID; /* 故意制造缺页 */ uint32_t paddr; translate(pt, 0x00003A4F, paddr); /* 应输出页框 8物理地址 0x8A4F */ translate(pt, 0x00004000, paddr); /* 页号 4缺页 */ translate(pt, 0x00020000, paddr); /* 页号 32越界 */ return 0; }这段代码有几个点值得对着练习卷看。第一frame PAGE_SHIFT | offset这个写法就是前面说的“拼接”比乘法加法更能体现结构。第二越界检查和有效位检查的顺序有讲究必须先判越界再判有效位否则可能去读数组外面的内存那是未定义行为。第三把pt[4].valid设成 INVALID 是故意的方便你亲眼看到缺页分支被走到。编译运行的话gcc -o mmu mmu.c ./mmu就行不需要任何额外依赖。3.2 第二段四种页面置换算法放在同一条引用串上对比缺页了就得从内存里挑一页换出去挑谁就是置换算法的事。练习里最爱考的四个是 FIFO、LRU、Clock 和 OPT。FIFO 按进入内存的先后顺序淘汰实现最简单但有 Belady 异常——页框数增加缺页反而可能变多。LRU 淘汰最久没被访问的页理论效果好但精确实现代价高需要记录每次访问的时间戳或者维护访问顺序链表。Clock 是 LRU 的近似用一个循环指针和访问位命中就置 1淘汰时扫到 0 就换出、扫到 1 就清 0 继续实现便宜且效果不错Linux 的页面回收就用了类似思想。OPT 是理论最优淘汰未来最长时间不会被访问的页但它需要预知未来只能用作评价其他算法的基准。下面这段 Python 把四个算法都实现了一遍用同一条经典引用串跑出对比def fifo(ref, frames): mem, faults [], 0 for p in ref: if p not in mem: faults 1 if len(mem) frames: mem.append(p) else: mem.pop(0) mem.append(p) return faults def lru(ref, frames): mem, faults [], 0 for p in ref: if p in mem: mem.remove(p) mem.append(p) # 访问过就移到队尾队首是最久未用 else: faults 1 if len(mem) frames: mem.pop(0) mem.append(p) return faults def opt(ref, frames): mem, faults [], 0 for i, p in enumerate(ref): if p in mem: continue faults 1 if len(mem) frames: mem.append(p) else: far, victim -1, None for q in mem: try: nxt ref.index(q, i 1) # 下一次被访问的位置 except ValueError: nxt float(inf) # 以后不再访问优先淘汰 if nxt far: far, victim nxt, q mem.remove(victim) mem.append(p) return faults def clock(ref, frames): mem, use, faults, hand [None]*frames, [0]*frames, 0, 0 for p in ref: if p in mem: use[mem.index(p)] 1 continue faults 1 while True: if mem[hand] is None: mem[hand], use[hand] p, 1 hand (hand 1) % frames break if use[hand] 0: mem[hand], use[hand] p, 1 hand (hand 1) % frames break use[hand] 0 hand (hand 1) % frames return faults if __name__ __main__: ref [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] for f in (3, 4): print(f页框数{f}) print( FIFO :, fifo(ref, f)) print( LRU :, lru(ref, f)) print( CLOCK:, clock(ref, f)) print( OPT :, opt(ref, f))把这条引用串跑出来3 个页框时的结果是 FIFO 15 次缺页、LRU 12 次、OPT 9 次。如果换成 4 个页框FIFO 会变成 10 次——页框多了缺页反而只降了 5 次而且历史上这个例子正是用来展示 Belady 异常的经典材料在某些引用串下FIFO 的缺页数会随着页框数增加而上升。代码你可以自己改引用串验证这是理解算法差异最快的办法比看书上的表格管用得多。算法3 页框缺页数缺页率是否会出现 Belady 异常实现代价FIFO1575%会极低LRU1260%不会高Clock12~14依赖具体实现约 60%~70%不会低OPT945%不会无法实用注意不同教材对 Clock 算法的细节处理不一样比如命中时是否把访问位置 1、初始指针位置在哪都会让缺页计数差一两次。考试遇到时看清题目给的前提再动手别拿代码里的结果硬套。3.3 用实验数据反推概念比死记结论强得多写完代码之后我一般还会让做练习的人干一件事把页框数从 1 试到 8把四种算法的缺页曲线画出来用表格记录即可。你会发现几条规律页框数到一定程度之后所有算法的缺页数都会快速下降然后趋于平缓这是局部性原理在起作用——程序在一段时间内只会集中访问一小撮页只要页框数能覆盖这个工作集缺页就很少了。反过来如果页框数长期小于工作集大小缺页率会居高不下系统把大量时间花在换页上而不是执行指令上这就是抖动。这也解释了为什么置换算法的好坏不能只看单条引用串。OPT 在每条串上都最优但它不实用FIFO 在某些串上会反常LRU 效果好但代价高。实际系统选的是近似 LRU 的 Clock 类算法加上访问位的定期清零用一个很小的硬件代价换到接近 LRU 的效果。课堂练习里很多人只记住“LRU 比 FIFO 好”但说不出为什么好、好在什么条件下跑一遍数据这些问题就全通了。4. 从课堂练习延伸到真实系统Linux 内存管理子系统里的关键结构4.1 mm_struct、vm_area_struct、page 三者怎么串起来课堂上的页表是一维数组加一个页表基址寄存器真实内核里要复杂得多但骨架是相通的。拿 Linux 举例每个进程有一个 task_struct里面有个指针指向 mm_struct这个结构描述整个进程的地址空间。mm_struct 里维护着一棵或一条 vm_area_struct 组成的区间树现代内核用红黑树加链表每一个 vm_area_struct 描述一段连续的、权限相同的虚拟地址区间比如代码段、数据段、堆、栈、mmap 映射的共享库各自一个 vm_area_struct。逻辑层次是这样的进程要访问某个虚拟地址CPU 先查 TLBTLB 未命中就去走多级页表页表里的表项最终指向一个物理页。而物理页由 struct page 描述内核为每个物理页维护一个 struct page记录引用计数、映射信息、所属 zone 等。页表负责“虚拟到物理”的映射关系struct page 负责“这个物理页被谁用着、用了多久”。两者配合才构成完整的内存管理。这个结构和课堂练习的对应关系其实很直接页表项对应页表数组的一个元素页框号对应物理页的页帧号有效位对应页表项里的 present 位。区别只在于维度——课堂上一张页表管所有页内核里每个进程有自己的页表而且页表是多级的vm_area_struct 负责在缺页时判断这次访问到底合不合法、该从哪加载数据。4.2 四级页表与地址翻译的完整路径64 位 Linux 上常见的四级页表是 PGD、PUD、PMD、PTE。48 位虚拟地址被切成五段9 位 PGD 索引、9 位 PUD 索引、9 位 PMD 索引、9 位 PTE 索引、12 位页内偏移加起来正好 48 位。PGD 的物理基址存在 CR3 寄存器里这是每次进程切换都要改的东西——切换进程时换 CR3就等于切换了整个页表这是进程地址空间相互隔离的硬件基础。翻译路径是这样CPU 从 CR3 拿到 PGD 基址用虚拟地址高 9 位做索引找到 PUD 表PUD 表里再取 9 位找 PMD 表PMD 表取 9 位找 PTE 表PTE 表取 9 位拿到最终物理页框号再拼上 12 位偏移得到物理地址。这条链一次最多访问四次内存所以 TLB 的存在非常关键。内核里有一整套 pgd_offset、pud_offset、pmd_offset、pte_offset 宏来走这条路径你去看arch/x86/include/asm/pgtable_64.h就能找到对应的位掩码定义和课堂上的 PAGE_MASK、PAGE_SHIFT 完全是一脉相承的思路。4.3 malloc 到物理页一次分配的完整链路很多做练习的人分不清 malloc 和页式管理的关系这里梳理一条从调用 malloc 到真正拿到物理内存的完整链路。第一步malloc 在用户态先看自己的空闲链表里有没有可用块有就直接返回这时候根本没有发生任何系统调用也没有触碰页表。第二步如果没有malloc 通过 brk 或 mmap 向内核申请一批地址空间内核做的事情是修改 mm_struct 里的 vm_area_struct记录“这个地址区间现在归你了”但此刻并没有分配任何物理页页表项也还是空的。第三步真正关键的一步——当你的程序第一次往这块地址写数据时CPU 查页表发现对应的页表项无效触发缺页异常。第四步内核的缺页处理程序接手根据出错地址在 vm_area_struct 里找到对应的区间判断这次访问合不合法、权限对不对。第五步合法的话从伙伴系统分配一个物理页必要时从磁盘把数据读进来然后填写页表项设置 present 位。第六步异常处理返回重新执行刚才那条出错指令这次页表有效顺利通过。这个“延迟分配”的策略非常重要它意味着你 malloc 了 1GB 内存只要不真的去写物理内存和页表项都不会产生。我第一次用top观察一个申请了大块内存但没用的程序发现 RSS 几乎为 0 的时候还挺惊讶后来才明白这就是按需分页的实际效果。课堂练习里的缺页中断和有效位在真实系统里就是这条链路上最核心的一环。5. 参数怎么选页大小、TLB 命中率的量化权衡5.1 页大小选择背后的计算页大小不是随便定的它牵动着一整串指标。页越大页表项越少、页表越省空间、TLB 一条表项能覆盖的地址范围越大命中率越高但页越大最后一页的内部碎片越严重平均浪费是页大小的一半一个 4KB 的页平均浪费 2KB2MB 的大页平均浪费 1MB这在大量小进程场景下会很浪费内存。反过来页越小碎片越小但页表越大、TLB 覆盖范围越小。算一笔账。假设某程序平均大小是 10KB用 4KB 页需要 3 页最后一页用掉 2KB 浪费 2KB浪费率 2/(102) 约 16.7%。用 64KB 页需要 1 页用掉 10KB 浪费 54KB浪费率超过 84%。这就是为什么现代系统在保留 4KB 基础页的同时还会提供 2MB 的透明大页——大页不是给所有场景用的它是给那些占用大块连续内存、访问又密集的应用比如大型数据库、虚拟机准备的用可控的碎片代价换 TLB 效率和页表空间。5.2 TLB 命中率对有效访问时间的影响有效访问时间EAT这个公式是练习里的常客必须会算。设 TLB 查找耗时 t一次内存访问耗时 mTLB 命中率 h。命中时查 TLBt加访问内存一次m未命中时查 TLBt加访问页表取页框号m再加访问数据m也就是 t 加两次内存访问。EAT h × (t m) (1 − h) × (t 2m)代进去算一下。设 t 10nsm 100nsh 0.98那么 EAT 0.98 × (10 100) 0.02 × (10 200) 0.98 × 110 0.02 × 210 107.8 4.2 112ns。如果命中率掉到 0.90EAT 0.9 × 110 0.1 × 210 99 21 120ns多出来的 8ns 就是 TLB 未命中的代价。可以看出即使命中率从 98% 掉到 90%EAT 也只涨了约 7%这个结果常常出人意料——因为 TLB 未命中只是多访问一次内存代价约为一次内存访问时间而不是数量级的惩罚。真正致命的是缺页那是毫秒级的磁盘操作和纳秒级的内存访问差六个数量级。这也再次说明练习里的算法选择要分轻重TLB 命中率优化收益有限缺页率优化才是重中之重。参数典型值变化趋势对性能的影响页大小4KB基础/ 2MB大页增大降低页表开销、提升 TLB 覆盖但加剧内部碎片TLB 条目数64~1536 条增大提升命中率但硬件成本和查找延迟上升TLB 命中率95%~99%每降 1% 大约多几次内存访问影响小于 10%缺页率追求趋近 0每次缺页毫秒级是数量级的惩罚页表项大小8 字节64 位影响多级页表总空间占用6. 常见问题与排查技巧实录6.1 高频错误速查表这批练习批下来错的点高度集中我把最常见的问题整理成一张表考前扫一遍能省不少分。现象根本原因正确做法把页号直接当页框号用混淆了逻辑地址的页号和物理内存的页框号页号是索引必须查页表得到页框号物理地址算出来偏移变了以为偏移也要重新计算页内偏移转换前后完全相同直接拼接页大小改了代码没改宏 PAGE_SHIFT 没同步更新页大小始终是 1 左移 PAGE_SHIFT 位改大小只改 SHIFT忽略有效位直接算地址没有检查页表项的 present 位先查有效位为 0 走缺页处理忘记越界检查没有比较页号和页表长度越界检查必须在查表之前多级页表页号拆分搞错不知道每一级占多少位位数 log2(每级表项数)从高位往低位依次切Belady 异常判断错以为所有算法都遵守“页框多缺页少”只有 FIFO 会出现LRU 和 OPT 不会6.2 自查方法把答案代回题目验证手算类的题做完别急着交卷用两个办法自检能抓出八成错误。第一个办法是区间验证算出物理地址之后反推它的页框号和偏移看页框号是不是页表里那个值、偏移是不是和原逻辑地址一致。如果偏移对不上那一定是算错了。第二个办法是边界试探拿刚好跨页的地址试一下。比如页大小 4KB测试 0x0FFF本页最后一个字节和 0x1000下一页第一个字节看页号是不是分别加了一。这个测试特别能暴露位运算的边界错误像写成、掩码少算一位之类的一试就露馅。我自己写页码计算的时候习惯性会跑一遍跨页测试这个习惯帮我省过好几次熬夜排查。对算法模拟类的题还有第三个办法用极端输入测。页框数设为 1看结果是不是每次都缺页页框数设得比不同页数还多看结果是不是等于不同页的数量因为这时候一次都不会置换。如果这两个极端都符合预期中间情况通常就没问题。6.3 几个从实操里攒下来的经验最后分享几个在机房带练习时反复强调的点都是文档里不太会写的东西。第一先画地址位图再动笔算。拿张草稿纸把逻辑地址的二进制或十六进制写出来在上面标出哪几位是页号、哪几位是偏移再从中间切开。这个动作看起来笨但能避免绝大多数移位错误尤其是多级页表里要切好几刀的时候。第二页表项的结构要当成结构体来记。别只记“页框号”要记住一个页表项里至少还有有效位、保护位、访问位、脏位、修改位这些东西。练习里经常问“访问位置 1 是什么时候”“脏位有什么用”答案就是访问位用于置换算法判断最近是否被访问脏位标记这一页是否被修改过、换出时是否需要写回磁盘。Clock 算法就是靠访问位工作的脏位则决定换出时的 I/O 代价。第三别把页和缓存块混了。页是虚拟内存管理的单位缓存块是 CPU 缓存和主存之间传输的单位两者都是 2 的幂但大小和用途完全不同。我见过有人在缺页计算里套用缓存块的映射方式结果整题错。记住页和页框尺寸一致缓存块和主存块尺寸一致这是两套独立机制。第四遇到“抖动”相关的题先找工作集。抖动就是分配的页框数小于工作集大小导致频繁换页。判断的关键是看引用串里的局部性通常连续一小段里反复出现的页就构成当前的工作集。只要能估算出工作集大小并和页框数比较这类题就有清晰的判断依据不用靠感觉猜。第五写代码验证比反复看笔记划算。前面那两段代码加起来不到 200 行但把它们跑一遍、改改参数看输出变化抵得上抄好几遍定义。尤其是置换算法这类题用代码跑出来的缺页计数表格比死记结论可靠得多也更容易在考场上回忆起推导过程。这套练习的价值其实不在于记住几个数字而在于建立起“地址怎么拆、表怎么查、页怎么换”这条完整的思维链。把这条链在自己的脑子里走顺了后面学到虚拟内存、写时复制、内存映射文件这些内容时你会发现它们都是在这条链上继续往上搭的不会再有那种突然断片的感觉。
返回列表