ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

段页式内存管理地址变换、越界检查与EAT计算

2026/9/30 7:51:19 拓冰建站 浏览量
段页式内存管理地址变换、越界检查与EAT计算 1. 为什么非要把分段和分页凑在一起“课堂练习4.3段页式内存管理”——如果你正在啃操作系统第四章这个标题大概率是你作业本上的一道题。它通常不长给一张段表、几张页表再扔来几个逻辑地址让你算物理地址、数访存次数、求有效访问时间。很多人第一次做这类题步骤能背下来心里却没底为什么一定要先查段表再查页表段长到底是按字节存还是按页数存页表和段表自身占的内存要不要算进去越界判断该在哪一步做段页式内存管理说白了就是分段和分页两个方案的合体。它先用“段”把程序按逻辑意义切成代码段、数据段、堆栈段再用“页”把每个段切成大小固定的块。这样一个逻辑地址就被劈成三段段号、段内页号、页内偏移。做题的核心就是把这三段数字一层层翻译成物理地址。这篇文章不打算给你复述教材定义而是围绕这道练习把每个数字背后的来路、每一步的意图、每一种常见问法的解法讲透。适合正在备考、赶作业、或者想真正搞懂地址变换的学生和转行者看。看完你应该能做到拿到任意一张段表页表自己出题、算题、验算心里不慌。1.1 纯分段方案的尴尬在哪先聊聊为什么单用分段不够。分段的出发点是“程序视角”一段代码、一块数据、一个栈各自独立长度随时可能变。这很符合人对程序的直觉编译器和链接器也喜欢这种划分因为不同段可以有不同的访问权限——代码段只读可执行数据段可读可写。问题出在物理内存分配上。段长是任意的从几十字节到几兆字节都可能。当一段程序运行结束、一段新的进来内存里就会留下各种大小不一的空洞。时间一长空洞碎成一片明明总空闲空间够却找不到一块连续区域放新段。这就是外部碎片。为了缓解它系统要么花时间做内存紧凑把内存里所有段挪到一起要么在分配时来回试探。这两种代价都不便宜。更麻烦的是段的长度可变意味着“段表项”里必须记录一个段长地址变换时得先比较偏移是否越界。计算量本身不大但它把内存分配变成了一场打补丁的游戏。1.2 纯分页方案的遗憾在哪那分页呢分页把内存切成固定大小的页框程序也切成同样大小的页。这样一来任何空闲页框都能直接用外部碎片基本消失只剩最后一页可能浪费一点内部碎片。分配变得极其简单。但分页丢掉了程序的逻辑结构。一整个程序被均匀切成页编译器眼里的“代码段”“数据段”在物理上荡然无存。结果是共享和权限管理变难——你想让两个进程共享同一份代码或者给某块内存单独设个只读属性在纯分页下都得靠额外的约定和映射不够自然。再者分页的页表只解决“页到页框”的映射它不管这段内存是干嘛的。所以在真实系统里光靠分页很难表达“这是一段可执行代码”这种语义。1.3 段页式的定位一次妥协但很值的妥协段页式就是让两者各司其职用分段表达逻辑结构用分页管理物理分配。每个段内部再按固定页大小拆分于是段长不再是“任意长度”而是“整数个页”。分配内存时只按页框来外部碎片问题被压下去同时段的逻辑边界还在权限和共享都好办。代价是地址变换多了一层。纯分页只要查一次页表纯分段只要查一次段表段页式得先查段表拿到该段的页表再查页表拿到页框号。访存次数从两次变成三次。这也是为什么段页式几乎总是配一个快表来救场。搞清楚了“为什么要三层”后面做题时每次画箭头、每次加偏移你都知道自己在干嘛而不是机械套公式。2. 逻辑地址怎么切段页式的地址结构拆解2.1 三段式地址段号、段内页号、页内偏移段页式的逻辑地址可以写成一个二元组(段号 S, 段内偏移 d)但真正落到硬件里这个“段内偏移”又被拆成(页号 P, 页内偏移 W)。所以完整形式是(S, P, W)三元组。地址的高位放段号中间放段内页号低位放页内偏移。具体各占多少位由系统设计决定。举个例子假设页大小是 4KB也就是 2 的 12 次方字节那页内偏移就得占 12 位假设每段最多 256 页那页号占 8 位假设最多 16 个段段号占 4 位。整个逻辑地址就是 4812 24 位。注意位数的划分不是随便定的它直接决定了“每段最大多少页”“总共多少段”“页多大”。题目里如果只给了地址位数和页大小你要能反推出页号占多少位、段内页号上限是多少。这里最容易翻车的地方是把“段内偏移”当成“页内偏移”用。段内偏移是相对段起点算的包含页号和页内偏移两部分而页内偏移只是相对某一页起点算的低位。做题时务必先分层别一上来就对整个地址取模。2.2 段表和页表的表项到底存什么段页式有两张表各管一段路。段表的每一项通常包含段号有时隐含、段长、该段页表的起始地址。段长用来做越界检查页表起始地址用来找到下一跳。段表本身是一个数组用段号做下标就能定位。页表的每一项通常包含页框号物理块号再加上一些状态位比如有效位、访问位、修改位、保护位。在课堂教学题里往往只保留“页框号”这一列其他位忽略。这里有个关键区别要记住段表项里的“页表起始地址”在不同教材、不同系统里有可能是物理地址也有可能是虚拟地址。如果页表本身也允许被换出内存那它就是个虚拟地址需要再变换一次——这就演变成多级页表了。课堂练习一般默认页表常驻内存段表项里的页表地址就是物理地址直接拿去访存。还有个常被忽略的点页表项本身也占内存。如果页表项是 4 字节一页 4KB 就能装 1024 个页表项。这个数字在算“页表要占多少页”时非常有用别漏。2.3 硬件怎么配合段表寄存器与地址变换机构光有表不行还得有硬件告诉 CPU“表在哪”。这就是段表寄存器的作用。它一般存两个东西段表始址段表在内存里的起始位置和段表长度一共多少项。每次地址变换硬件的动作顺序大致是先从逻辑地址里取出段号 S拿 S 去和段表长度比较判断是否越界没越界就用“段表始址 S × 段表项大小”算出段表项地址读出来拿到段长和页表始址接着比较段内偏移是否小于段长越界就报错然后从段内偏移里拆出页号 P用“页表始址 P × 页表项大小”算出页表项地址读出页框号最后把页框号和页内偏移拼起来得到物理地址。这一串动作里段表长度比较、段长比较是两个不同的检查点分别在两级表访问之前。做题时如果题目问“哪个地址会越界”你得判断它是在第一步段号超范围还是第三步段内偏移超段长被拦下。3. 地址变换全流程手算走一遍才算真懂3.1 六步变换流程详解把上面硬件的动作拆成做题能用的步骤大概是这六步从逻辑地址中分离出段号 S、页号 P、页内偏移 W。用 S 去查段表检查 S 是否小于段表长度。越界则报“段号越界”或“缺段”。取出该段的段长检查“P × 页大小 W”是否小于段长。越界则报“段内地址越界”。取出该段的页表起始地址。用 P 查页表取出页框号物理块号。如果页表项有效位是 0说明该页不在内存触发缺页中断本题通常不考。物理地址 页框号 × 页大小 W。这里第 3 步的检查方式很关键。为什么要算“P × 页大小 W”而不是直接比 P因为段长是以字节为单位的而 P 是页号。要先把它换算回字节才能和段长比。当然你也可以先算出段内最大页号 ⌈段长 ÷ 页大小⌉然后检查 P 是否小于它。两种做法等价但第二种更容易出错——边界值到底是“小于”还是“小于等于”得看清楚。段长能整除页大小时最大页号就是 段长÷页大小 的整数结果不能整除时最后一页只有部分有效页号上限要向上取整。提示判断页号是否越界本质是判断“这个页在段内是否存在”。只要页号小于该段实际占用的页数就算合法哪怕最后一页只用了一部分。3.2 一道典型练习的完整手算过程我们设计一套数据把练习从头走一遍。规定页大小 4KB页内偏移 12 位段内页号 8 位段号 4 位。段表寄存器里段表始址为 0x8000段表长度为 3。段表内容如下段号段长字节页表始址081920x90001122880x9100240960x9200各段的页表只列页框号段号页号 0页号 1页号 2058—1297211——现在依次求下面几个逻辑地址对应的物理地址。第一题逻辑地址 (0, 1, 100)。段号 0 小于段表长度 3合法。段 0 的段长是 8192 字节等于 2 页。页号 1 小于 2且 1×4096100 4196 8192合法。查段 0 的页表页号 1 对应页框号 8。物理地址 8 × 4096 100 32768 100 32868。第二题逻辑地址 (1, 2, 0)。段号 1 合法。段 1 段长 12288 字节等于 3 页。页号 2 小于 3合法。查段 1 页表页号 2 对应页框号 7。物理地址 7 × 4096 0 28672。第三题逻辑地址 (0, 2, 0)。段号 0 合法。但段 0 只有 2 页页号 0、1页号 2 已经超过范围2×4096 8192不小于段长 8192触发段内地址越界。这一步拦下来的原因是页号越界跟段号没关系。第四题逻辑地址 (3, 0, 0)。段号 3 等于段表长度 3超出了允许范围合法段号是 0、1、2触发段号越界。注意这里是在第一步就被拦截根本不会去查页表。逻辑地址段号检查页号/段内检查页框号物理地址(0,1,100)通过通过832868(1,2,0)通过通过728672(0,2,0)通过越界—越界中断(3,0,0)越界——段号越界3.3 两道防线段内越界与页号越界从上面第三、第四题能看出段页式有两个独立的越界关口物理含义完全不同。段号越界是“你访问的段根本不存在”。每个进程的段表长度是固定的段号超出了就像你翻开一本书却要查第 100 章书里没这章。这种错误通常意味着程序逻辑本身有问题或者指针被写坏了。段内地址越界是“段存在但你访问的位置超出了这个段的长度”。段 0 只有 2 页你要访问第 3 页段在但里面没那么多内容。这往往是数组下标越界、缓冲区溢出一类问题的直接表现。很多人做练习时会把这两种情况混成一句“越界”但考试里经常分开问。所以答题时一定要写清楚是段号越界还是段内地址越界分别在第几个检查点被拦下。4. 性能与开销访存次数和有效访问时间怎么算4.1 无快表为什么要访存三次段页式每次地址变换硬件至少要读三块内存第一块是段表项第二块是页表项第三块才是真正要访问的数据。这就是“三次访存”的由来。对比一下会更清楚。纯分页只要读一次页表再一次数据共两次纯分段是读一次段表再一次数据也是两次。段页式因为多了一层间接变成三次。三次访存里前两次纯粹是“寻路”开销跟程序真正要干的事没关系。如果每次读写内存都要先花两个来回找路性能损失是实打实的。正因为这样段页式系统几乎都会配一个快表TLB。快表是个小容量的高速缓存专门缓存最近用过的段号加页号到页框号的映射。命中时直接拿到页框号省掉两次访存。提示快表里缓存的键通常是“段号 页号”的组合而不是单独的页号。因为不同段里可能有相同的页号光凭页号无法区分。4.2 引入快表后的EAT计算有效访问时间Effective Access TimeEAT是这类练习的高频考点。我们设访问快表耗时 10ns访问一次内存耗时 100ns快表命中率 98%。没有快表时每次都要三次访存EAT 3 × 100 300ns。有快表时分成两种情况快表命中先花 10ns 查快表拿到页框号再花 100ns 访问数据共 10 100 110ns。快表未命中查快表 10ns然后依次访问段表 100ns、页表 100ns、数据 100ns共 10 100 100 100 310ns。于是 EAT 98% × 110 2% × 310 107.8 6.2 114ns。从 300ns 降到 114ns提速接近三倍这就是快表的价值。这里有个容易出错的地方把命中率套反或者把命中时的访存次数算成两次以上。记住命中后物理地址已经确定直接访存取数就行。场景快表访问段表访问页表访问数据访问总耗时无快表—100100100300ns命中10——100110ns未命中10100100100310ns4.3 段表和页表自身占多少内存练习里另一种常见问法是问“这套机制要额外花掉多少内存来存表”。这时候要分两块算。段表开销段表项数等于进程的段数乘以每个段表项的大小。比如有 3 个段每个段表项 8 字节段表就是 24 字节。真实系统里段数不多段表通常很小。页表开销每个段都要维护一张自己的页表页表项数等于该段的页数。段 0 有 2 页页表就 2 项段 1 有 3 页页表就 3 项。假设页表项 4 字节段 0 的页表占 8 字节段 1 占 12 字节段 2 占 4 字节合计 24 字节。这套例子里的表开销一共 48 字节小到可以忽略。但真实系统可不是这样。假如一个进程最多有 16 个段每段最多 256 页页表项 4 字节那么每段的页表固定要预留 256 × 4 1024 字节16 段就是 16KB。注意这里无论该段实际用了多少页页表都按最大页数预留——这就是段页式的一个隐性代价。更极端的推算如果段内页号有 20 位那每段页表最多 2 的 20 次方项按每项 4 字节算就是 4MB。每个段都来这么一张段一多内存直接就爆了。这正是真实系统改用多级页表的根本原因——不是页变多了而是单张页表太占地方。想通这一点你对段页式的理解就上了一个台阶。5. 常见错误与排查思路实录5.1 计算题高频错点速查表练习做多了会发现错误翻来覆去就那几类。我把自己踩过和见过的坑整理成一张表对着检查基本能救回来。错误现象真实原因正确做法物理地址算出来偏小直接用页框号加偏移忘了乘页大小物理地址 页框号 × 页大小 页内偏移段号判断反了把“段号 段表长度”写成“≤”合法段号是 0 到 段表长度−1页号判断出错拿页号和段长直接比单位不一致换算成字节或先算段内最大页数段长单位搞混题目给的是页数当成字节用了看清单位必要时乘除页大小访存次数漏算忘了段表本身也要访存无快表是三次访存不是两次EAT 命中率用反命中当未命中算先写清两种情况各自耗时再加权十六进制十进制混算0x1000 当成 1000 了统一进制建议全程用十进制算完再转5.2 单位与进制的坑这是最不起眼、也最容易丢分的地方。段长题里有时写“8192”有时写“8KB”有时写“2 页”。页大小有时是“4K”有时是“4096”。地址有时是十六进制 0x9000有时是十进制。我的建议是所有涉及大小的量统一换算成字节再算。页内偏移是多少字节、段长是多少字节、页大小是多少字节全对齐了就不容易错。十六进制的地址比如页表始址在做单纯查表题时其实用不到只有当你需要算出“页表项在内存中的具体地址”时才用得上那一步再转。举个具体的坑题目说“页表起始地址为 0x9100页表项大小 4 字节求页号 2 对应的页表项地址”。答案是 0x9100 2 × 4 0x9108。这里的 0x9100 是物理地址直接加偏移即可。但如果你误以为它是段式里的“逻辑地址”就会多绕一圈。看清题目给的是物理地址还是逻辑地址能省掉不少无效计算。5.3 和真实系统的对应关系课堂练习里的段页式模型跟真实操作系统有对应也有差异了解这些能帮你判断哪些细节是“题目简化”哪些是“系统本质”。在 x86 体系的早期设计里段式机制是真实存在的逻辑地址先经段寄存器加偏移得到线性地址再经分页机制变成物理地址。这套流程本质上就是段页式。后来为了简化和兼容主流系统把段基址统一设为 0、段界限设为最大相当于“绕开”了分段只保留分页——所以你现在看到的现代系统地址变换基本是纯分页加多级页表。但这个“绕开”不是否认分段的价值而是因为现代编译器、链接器已经能用其他方式表达逻辑结构不需要硬件分段来扛。反过来练习题里坚持考段页式是因为它是理解“逻辑结构如何映射到物理结构”最直观的模型。把段页式吃透再去看多级页表、反向页表会觉得顺理成章。6. 练习拓展自己动手改题目6.1 把题目改难一点的几种方式做完原题如果还有余力可以自己给自己加难度比刷重复的题有用得多。第一种改法是引入二级页表。把段内页号拆成“页目录号 页表号”这样段表项指向的不再是页表而是页目录。地址变换多一层访存次数变成四次EAT 公式也要跟着改。这一改你就能体会到为什么真实系统用多级结构。第二种是改页大小。把页大小从 4KB 改成 1KB页内偏移从 12 位变成 10 位段内页号位数相应增加。算一遍页表大小你会发现页变小会让页表项数暴涨表开销反而更大。这个反直觉的结论值得亲手算一遍。第三种是加入缺页处理。给页表项加上有效位让某个页号对应“不在内存”。这时地址变换要触发缺页中断流程从“查表拼地址”变成“查表—发现无效—中断—调页—重新执行”。把这条路径画清楚对理解虚拟内存很有帮助。6.2 用代码模拟一次地址变换手动算几遍之后写段代码把逻辑固化下来理解会更深。下面这段 Python 用前面那套段表页表完整实现地址变换包括两级越界检查。PAGE_SIZE 4096 OFFSET_BITS 12 PAGE_BITS 8 SEG_BITS 4 # 段表段号 - (段长字节, {页号: 页框号}) SEG_TABLE { 0: (8192, {0: 5, 1: 8}), 1: (12288, {0: 2, 1: 9, 2: 7}), 2: (4096, {0: 11}), } def translate(logical_addr): seg_no (logical_addr (OFFSET_BITS PAGE_BITS)) ((1 SEG_BITS) - 1) page_no (logical_addr OFFSET_BITS) ((1 PAGE_BITS) - 1) offset logical_addr ((1 OFFSET_BITS) - 1) # 第一道防线段号越界 if seg_no not in SEG_TABLE: return 段号越界: {}.format(seg_no) seg_len, page_table SEG_TABLE[seg_no] # 第二道防线段内地址越界 if page_no * PAGE_SIZE offset seg_len: return 段内地址越界: 段{} 偏移{}.format(seg_no, page_no * PAGE_SIZE offset) # 查页表 if page_no not in page_table: return 缺页: 段{} 页{}.format(seg_no, page_no) frame_no page_table[page_no] return frame_no * PAGE_SIZE offset # 验证 for addr in [(0 20) | (1 12) | 100, (1 20) | (2 12) | 0, (0 20) | (2 12) | 0, (3 20) | (0 12) | 0]: print(hex(addr), -, translate(addr))把这段跑一遍输出应该是0x110064 - 32868、0x120000 - 28672、0x120000前面那个0x020000 - 段内地址越界以及0x300000 - 段号越界。对照前面手算的结果能立刻发现自己对位运算或者越界判断有没有理解偏。提示代码里的seg_len是字节数所以越界比较必须先把页号和偏移换算回字节。如果你直接把page_no和seg_len比段长大的时候会误判通过段长小的时候又会误判越界。这个坑我在第一次写的时候就踩过。最后分享一个我自己做题时的习惯每道题都在草稿纸上画一张三列的对照表——段号、页号、页内偏移每算一个地址就往里填一次检查结果。这样即便算到第十个地址也不会把哪一步的中间结果记混。段页式这类练习错往往不是错在难而是错在信息多、步骤长、单位杂。把中间过程摊开写比在脑子里默算靠谱得多。要是你手头还有别的变体题目不妨用这套“先分层、再查表、后检查、最后拼地址”的顺序过一遍基本不会再卡壳。