资讯动态

段式内存管理:课堂练习4.1逻辑地址到物理地址变换与越界判断

发布时间:2026/9/30 0:41:06 来源:尧图企业网站定制
段式内存管理这个词第一次在课本上见到的时候我并没太当回事觉得不就是把内存切成一段一段再分配吗能有多难。结果真到课堂练习4.1题目甩过来一张段表让我算某个逻辑地址对应的物理地址还要判断哪些地址访问会越界我当场就卡住了——段号到底从哪一位开始段长和偏移量谁跟谁比基址是加在谁身上一堆问号。后来把这条链路从头到尾捋了一遍才发现段式内存管理的核心其实非常朴素段号去查表拿到基址和段长偏移量先跟段长比一比没越界再拿基址加偏移量。就这么一条主线。这篇就把课堂练习4.1这道题背后牵扯的知识点整个摊开讲。从段式管理为什么会出现、段表和逻辑地址长什么样、地址变换每一步在干什么到怎么用代码把变换过程跑通验证再到做题和实验中踩过的那些坑。正在学操作系统内存管理这一章的同学可以拿来对着看需要快速复习段式地址变换、准备考试或者做课程实验的人也能直接抄作业。1. 段式内存管理到底在解决什么问题1.1 从程序被切成了什么说起要把段式管理搞明白得先跳出内存本身看看一个程序在运行前是怎么被拆开的。我们写的代码编译链接之后并不是铁板一块它天然就分成好几个逻辑上独立的部分有放代码的代码段有放全局变量和常量的数据段有函数调用时用来压栈的栈段还有动态申请的空间堆段。这些部分的作用、增长方式、访问权限完全不一样——代码段一般只读可执行数据段可读可写栈段会随着函数调用不断向下增长堆段则可能向上长。它们之间没有任何理由被绑在一起、当成一整块连续的内存来管理。段式内存管理干的事情就是顺着程序这种天然的模块划分把逻辑地址空间按逻辑意义切成若干段。每个段从 0 开始编址有自己的段号和段内偏移。这样一来程序员的视角和操作系统管理内存的视角就对上了一个函数是一个段一张表是一个段一段共享库也是一个段。段与段之间互相独立各管各的。这跟分页管理里那种不管三七二十一每 4K 切一刀的机械做法思路完全不在一个频道上。我第一次真正理解段是逻辑单位这句话是靠一个类比分页像是把一本书强行按每 50 页装订成册不管你章节在哪结束分段则是按书本身的章、节来装订每一章自成一本。哪种方式更符合书的逻辑结构一想就明白。段式管理之所以适合模块化、共享、保护这些场景根子就在这里。1.2 段式与分页的那条分水岭很多人学到这里会糊涂既然分页和分段最后都要做地址变换都要查表都要检查越界那它们的区别到底在哪。我总结下来最本质的分水岭只有一条——页是物理单位段是逻辑单位。这一条决定了后面所有差异。分页的页大小是系统固定死的比如 4KB由硬件决定用户和程序完全感知不到页的存在对它来说是透明的。分页的地址空间是一维的你给一个线性地址系统帮你切成页号和页内偏移你不需要知道页边界在哪。而段的大小是可变的每段长度由程序本身决定短的几十字节长的几兆都行而且段边界对程序是可见的你自己就知道哪个函数属于哪个段。由此衍生出一连串差别。分页能有效减少内存的外部碎片因为它把内存切成了固定小块最后剩下的零头往往能塞进某一页里去代价是可能产生内部碎片。分段因为段长不固定分配和回收都更麻烦容易留下外部碎片碎片多了还得做内存紧凑。但换来的好处是它在共享、保护和动态增长这三件事上更顺手。为什么因为你可以精确地共享某一个段而不用去担心共享的那一页里混进了别的不该共享的内容。这个点我在第 5 章还会细说这里先埋个伏笔。理解了这条分水岭再看段式管理的一切设计包括段表为什么长那样、地址为什么是二维的就都顺理成章了。2. 段表结构与逻辑地址把这两样吃透题就做对了一半2.1 逻辑地址的二维结构段式管理里一个逻辑地址是个二维坐标写成(段号 S, 段内偏移 W)的形式。这两个量在机器里其实是拼在一个二进制数里的只不过高位表示段号低位表示偏移量中间的分界线由系统规定。比如常见的划分是前 16 位是段号、后 16 位是偏移量那这个系统最多能容纳 2 的 16 次方也就是 65536 个段每个段最大 64KB。到底用几位分给段号、几位分给偏移是要在设计阶段算清楚的因为段号位宽决定了段数上限偏移位宽决定了单段的最大长度。这里有个新手常犯的错误拿到一个逻辑地址的十进制数值不知道该怎么切。正确的做法是先确认地址结构假设段号占高 16 位、偏移占低 16 位那么逻辑地址 0x00010064 就表示段号 1、段内偏移 0x64即 100。计算方式是右移 16 位得到段号与低 16 位掩码相与得到偏移。我见过有人直接把十进制数除以 65536 求段号、求余数当偏移思路对但一旦位宽不是 16 就容易算错还是老老实实按位操作最稳妥。段号从 0 开始编号。这意味着如果一个进程有 n 个段段号的合法范围是 0 到 n-1。这个从 0 开始、合法上限是 n-1的细节是后面越界判断的关键前提记住它。2.2 段表里每一项都是干什么用的每个进程都有一张自己的段表段表本质上是一个数组按段号索引。段表的每一项也叫段描述符、段表项通常包含这么几个字段我按重要性排个序字段作用课堂上最常考段基址该段在物理内存中的起始地址是算物理地址必用段长该段的长度用来做越界检查是越界判断必用存取控制位读、写、执行权限用于保护偶尔考存在位该段当前是否已调入内存偶尔考访问位/修改位供置换算法参考较少考段基址和段长是做题时的绝对主角。物理地址的计算就靠基址越界判断就靠段长。段表的结构可以简化理解成一张二维表格每行对应一个段行号就是段号。有的教材会把段号也作为一个显式字段列在每一项里其实是冗余的因为段号就是索引本身写出来只是为了阅读方便。段长这个字段容易被忽视但它是段式管理和分页管理做题时最大的差异点。分页里页大小固定越界判断只需要偏移量小于页大小即可而分段里每段长度都不一样必须逐段查表拿段长再和偏移量比。这就是为什么分段地址变换不能像分页那样一刀切必须老老实实查表。2.3 段表寄存器与进程切换时会发生什么段表存在内存里CPU 怎么知道当前进程的段表在哪靠一个专门的硬件寄存器——段表寄存器。它通常存两个信息段表在内存里的起始地址以及段表的长度也就是有多少个段表项。每次做地址变换CPU 都先从这里拿到段表的入口再去内存里查表项。为什么还要存段表长度因为这个长度就是段号是否越界的第一道关卡。拿到一个逻辑地址先取出段号 S跟段表长度一比如果 S 大于等于段表长度说明这个段号根本不存在直接报段号越界连查表都省了。这道检查比查段长还靠前是地址变换的第一步。那进程切换的时候呢这就有意思了。每个进程有自己独立的段表进程 A 切换到进程 B操作系统必须把段表寄存器里的内容换成 B 的段表信息否则 B 就会拿着 A 的段表去查自己的地址结果必然错乱。这一步叫切换段表基址是进程上下文切换的一部分和切换页表基址寄存器是一个道理。我在做实验的时候忘了在切换进程时更新段表寄存器导致进程 B 一运行就疯狂报越界排查了半天才发现是这里漏了更新印象特别深。3. 地址变换全过程课堂练习4.1的核心考点逐条拆3.1 从逻辑地址到物理地址的四步把前面铺垫的东西拼起来段式管理的地址变换就是一个固定的四步流程任何一道题都是这四步的排列组合。我把它写死做题时照着套拆分从逻辑地址里按位宽切出段号 S 和段内偏移 W。查段号越界判断 S 是否小于段表长度。不满足就抛段号越界异常流程结束。查段长越界用 S 去段表里取出对应表项判断 W 是否小于该段的段长。不满足就抛段内偏移越界异常流程结束。算物理地址物理地址 段基址 段内偏移 W。这四步里最容易出错的不是加法而是两个越界判断的顺序和比较对象。步骤 2 比的是段号和段表长度步骤 3 比的是偏移量和段长两个都是严格小于才合法不满足就报错。很多人图省事直接跳过越界判断去加基址结果题目问能不能正常访问的时候就答不上来。我用一张流程性质的清单再强调一遍逻辑顺序注意这里不用图表就按文字走先看段号在不在范围内再看偏移量在不在这一段长度内两道都过了才有资格做基址加偏移。越界检查是地址变换的安全闸门不是可有可无的装饰。3.2 题型一给段表算物理地址这是课堂练习里出现频率最高的题型。题目给一张段表再给一个或几个逻辑地址让你算出物理地址。我拿一组典型数据来演示这套数据是我自己造的贴合教材常见的数值风格段号段长段基址0100020001200050002300080003100012000段表长度是 4也就是有 4 个段合法段号是 0、1、2、3。现在来逻辑地址 (1, 500)意思是段号 1、段内偏移 500。第一步拆分没问题S1W500。第二步查段号1 小于 4通过。第三步查段长段号 1 的段长是 2000500 小于 2000通过。第四步算物理地址段基址 5000 加上偏移 500结果是 5500。答案就是 5500。再看一个 (2, 2900)。S2W2900。段号合法段号 2 的段长是 30002900 小于 3000通过。物理地址 8000 2900 10900。这两个例子都算得干净利落但真正的考点往往藏在陷阱地址里这就引出下一类题型。3.3 题型二越界判定别只会算加法还是上面那张段表如果题目给的是逻辑地址 (2, 3500)你会怎么做要是直接加基址8000 3500 11500看起来像个答案但这其实是错的。因为段号 2 的段长只有 3000偏移量 3500 已经超过了 3000属于段内偏移越界这次访问压根不合法根本不该产生物理地址。这就是段式地址变换里最关键的思维转变算物理地址之前必须先通过越界检查这道门。分页做多了会养成肌肉记忆看到地址就往下加但分段不一样每段长度不同必须查。这道题正确答案是越界无法访问。再看一个段号越界的例子逻辑地址 (5, 0)。段表长度是 4段号范围是 0 到 3而这里段号是 55 不小于 4段号越界连查段表的机会都没有直接报错。注意区分这两种越界段号越界是段根本不存在段内偏移越界是段存在但这一脚踩到了段外面两者报的异常类型不同有的题目会要求你明确指出是哪种越界。有个细节要提醒段长比较时是严格小于。如果偏移量刚好等于段长比如段长 3000、偏移量 3000这已经越界了因为段内合法偏移是 0 到 2999。0 号偏移是段的第一字节段长 3000 的段最后一字节偏移是 2999。这个差一问题坑过不少人我第一次做练习就把等号当成合法结果整道题的分都丢了。3.4 题型三段号范围和段表长度有一类题不直接给段表而是给地址结构的位宽让你推断段号范围。比如题目说逻辑地址共 32 位其中段号占 8 位、偏移占 24 位。这时候段号范围就是 0 到 2 的 8 次方减 1也就是 0 到 255每段最大长度是 2 的 24 次方字节。这类题考的是你对地址结构的理解知道位宽怎么换算成范围。还有一类题给的是十进制逻辑地址让你先拆再判。假设段号高 16 位、偏移低 16 位逻辑地址 131172 是多少先拆131172 除以 65536 等于 2余数是 131172 - 2×65536 131172 - 131072 100。所以段号 2、偏移 100。然后拿段号 2 去查表看段长够不够按上面那张表段长 3000100 合法物理地址 8000 100 8100。这一段的核心心得是拆地址、查段号、查段长、算结果四步一步都不能少顺序也不能乱。把这三类题型都练熟课堂练习4.1基本就是送分题。真正拉开差距的是有人直接跳步算加法被等号越界段号越界偏移越界这些陷阱反复绊倒。4. 用代码把地址变换跑一遍纸上推演容易有疏漏我习惯把地址变换用代码实现一遍跑几个测试用例对不对一眼就能看出来。下面用 Python 写思路清晰也方便改成其他语言。4.1 段表和段表项的数据结构先定义段表项。为贴合课堂简化模型我只保留段号、段长、基址和存取权限四个字段权限用 3 位表示读、写、执行。class SegmentTableEntry: def __init__(self, base, length, protection0b111): # base: 段基址length: 段长 # protection: 保护位默认读、写、执行全开 self.base base self.length length self.protection protection class SegmentTable: def __init__(self, offset_bits): # offset_bits: 逻辑地址中偏移量占的位数 self.entries [] self.offset_bits offset_bits self.table_len 0 # 段表长度等于段表项数量 def add_segment(self, base, length, protection0b111): entry SegmentTableEntry(base, length, protection) self.entries.append(entry) self.table_len len(self.entries) return self.table_len - 1 # 返回该段的段号这里 offset_bits 很关键它决定了从逻辑地址里怎么切。段号占的位数由段表长度反推其实在拆分的时候我们只需要知道偏移占多少位高位剩下的就是段号。这个设计比写死段号位宽更灵活。4.2 地址变换函数的实现核心的变换函数严格照着四步走任何一步失败都抛异常绝不偷偷跳过。def translate(logical_addr, seg_table): offset_bits seg_table.offset_bits offset_mask (1 offset_bits) - 1 # 第一步拆分段号和段内偏移 seg_no logical_addr offset_bits offset logical_addr offset_mask # 第二步段号越界检查 if seg_no seg_table.table_len: raise ValueError( 段号越界: 段号%d, 段表长度%d % (seg_no, seg_table.table_len) ) # 第三步段内偏移越界检查 entry seg_table.entries[seg_no] if offset entry.length: raise ValueError( 段内偏移越界: 偏移%d, 段长%d % (offset, entry.length) ) # 第四步计算物理地址 physical_addr entry.base offset return physical_addr函数的返回值和异常设计得很直白合法就返回物理地址非法就抛异常并带上诊断信息。这样跑测试用例的时候出错信息直接告诉你卡在段号还是段长省得自己猜。4.3 测试用例跑出来的结果用第 3 章那张段表来搭测试。段基址分别是 2000、5000、8000、12000段长是 1000、2000、3000、1000。为了能构造出逻辑地址数值我把偏移位宽设成 16 位。seg_table SegmentTable(offset_bits16) seg_table.add_segment(base2000, length1000) # 段 0 seg_table.add_segment(base5000, length2000) # 段 1 seg_table.add_segment(base8000, length3000) # 段 2 seg_table.add_segment(base12000, length1000) # 段 3 def make_addr(seg_no, offset): return (seg_no 16) | offset # 用例1合法(1, 500) 期望 5500 print(translate(make_addr(1, 500), seg_table)) # 输出 5500 # 用例2合法(2, 2900) 期望 10900 print(translate(make_addr(2, 2900), seg_table)) # 输出 10900 # 用例3偏移越界(2, 3500) try: translate(make_addr(2, 3500), seg_table) except ValueError as e: print(e) # 段内偏移越界: 偏移3500, 段长3000 # 用例4段号越界(5, 0) try: translate(make_addr(5, 0), seg_table) except ValueError as e: print(e) # 段号越界: 段号5, 段表长度4 # 用例5边界(0, 999) 合法(0, 1000) 越界 print(translate(make_addr(0, 999), seg_table)) # 输出 2999 try: translate(make_addr(0, 1000), seg_table) except ValueError as e: print(e) # 段内偏移越界: 偏移1000, 段长1000用例 5 就是我前面强调的等号越界边界测试。跑通这五个用例基本就把课堂练习4.1所有考点覆盖全了。这里有个操作上的小技巧测试用例一定要把边界值单独拎出来测偏移等于段长、段号等于段表长度、段号为 0 这些位置最容易暴露逻辑错误。我一开始把比较写成了小于等于用例 5 直接失败一秒钟就定位到了问题。5. 段式管理的优势和做题时的踩坑记录5.1 为什么段式在共享和保护上更顺手回到最开始那条分水岭段是逻辑单位这件事决定了它在两个场景上特别占便宜。第一个是共享。多个进程想共用一段代码比如同一个库函数在段式管理下只要让每个进程的段表里对应表项指向同一个物理基址、同样的段长即可。因为共享的粒度正好就是逻辑上的一个完整段不会多也不会少。换成分页共享模块的边界往往和页边界对不齐可能一页里一半是要共享的、一半是不该共享的处理起来就很别扭得分情况讨论。第二个是保护。因为每一段都有明确的存取权限代码段可以设成只读可执行、数据段设成可读可写、栈段设成读写硬件在做地址变换查段表的时候顺手就把权限检查一起做了。想非法写代码段地址变换阶段直接拦截。分页虽然也能加权限位但因为页是按固定大小机械切分的一页里混着代码和数据是常事权限就没法设得那么精细。这也是为什么段式管理在动态增长上更舒服——栈段要向下长、堆段要向上长段本身可以动态调整长度而页的边界是死的。当然代价就是前面说的外部碎片和内存紧凑的开销。世界上没有免费午餐选哪种管理方式本质上是在内存利用率和便于共享保护之间做权衡。段页式管理就是想把两边的优点都拿过来先按逻辑分段方便共享保护段内再分页消除外部碎片。5.2 常见问题速查表我把做题和处理段式管理相关问题时最常踩的坑整理成一张表遇到问题先来这里对号入座。问题现象可能原因解决办法算出的物理地址看似合理但被判错跳过了越界检查非法地址也算出了值牢记必须先做两步越界判断再算加法偏移等于段长时被判合法比较用了小于等于改成严格小于偏移合法范围是 0 到段长减 1段号明明存在却报越界混淆了段表长度和最大段号段号合法范围是 0 到段表长度减 1逻辑地址拆不出正确的段号偏移偏移位宽记错或没按位拆分先确认地址结构位宽用移位和掩码拆进程切换后新进程疯狂报越界忘记更新段表寄存器切换时同步更新段表起始地址和长度两个进程共享同一段但权限冲突各段表项权限位设置不一致统一共享段的存取控制位内存里出现很多小块空洞无法利用段长不一导致外部碎片定期做内存紧凑或改用段页式这张表里的每一条我基本都亲身踩过。尤其是偏移等于段长和段号范围这两条属于经典差一错误看起来是小问题但在考试和实验里能直接让结果翻车。我个人的经验是做这类题的时候别急着算先在草稿上把段号合法性和偏移合法性两个判断写成两行明确的检查式再动笔算加法错漏率能降一大截。用代码实现的时候也一样把两个越界判断拆成两个独立的 if异常信息带上具体数值调试的时候一眼就能看出是哪一步没过。这个习惯我在做课程设计时养成的后来做任何涉及地址映射、数组边界、缓冲区校验的活儿都会下意识先写边界检查再加偏移很少再出越界相关的 bug。原文的图 4-1 展示的是段式管理下逻辑地址到物理地址的变换流程画的是段表寄存器指向段表、段号查表、越界检查、基址加偏移这一整套链路。对照那张图把上面四步走一遍再拿代码跑几个用例验证段式内存管理这块就算真正捏在手里了。段页式管理无非是在这条链路里再加一层页表查询主干逻辑还是今天这一套把段式这关过了后面学段页式会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑