ARTICLE DETAIL

资讯详情

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

段式内存管理课堂练习4.1:段表、地址转换与越界检查拆解

段式内存管理课堂练习4.1:段表、地址转换与越界检查拆解 段式内存管理这道课堂练习我第一次做的时候在草稿纸上反复擦改了好几遍——段表查对了偏移量的边界忘了判断边界判断对了单位换算又栽了跟头把 1K 当成 1000 来算结果自然对不上。后来把这类题的套路摸清楚才发现它考的从来不是算术而是你对“逻辑地址怎么变成物理地址”这条链路的理解到底扎不扎实。段式内存管理是操作系统存储管理里一个承上启下的知识点往上接着进程的逻辑地址空间往下连着物理内存的分配与保护段表、段基址、段长、越界中断、共享与保护这些概念串起来才是一套完整的东西。这篇文章我就围绕“课堂练习4.1”这类题目把段式内存管理从设计动机、段表结构、地址转换过程到手算细节、易错点全部拆开讲一遍。不管你是刚学到这一章、做题总是差一点的同学还是想回头把基础重新捋一遍的人跟着走一遍应该都能有收获。1. 段式内存管理到底在解决什么问题1.1 从“程序模块”看分段的由来要理解段式管理得先回到一个很朴素的问题程序员写的代码天然是分块的。一个程序里有主函数、有若干个功能模块、有全局数据区、有栈区还有链接进来的库函数。这些部分在逻辑上各自独立长度也不一样——主控逻辑可能就几百行数据处理模块可能很大栈区还会随运行动态增长。如果硬把它们当作一整块连续的东西塞进内存会很难受改一个模块的大小别的模块位置都要跟着挪想共享一段公共代码也没法单独把它标出来。分段的想法就是顺着这个直觉来的既然程序本来就是按逻辑意义分成若干块的那就干脆以“段”为单位来划分和管理。每个段是一组逻辑意义完整的信息比如代码段、数据段、栈段。段内地址连续段与段之间不要求连续。这样一来程序的逻辑结构和内存的物理组织就对上了管理起来自然得多。这也是为什么课本在讲分段时总强调“段是信息的逻辑单位对用户可见”而页是“物理单位对用户透明”。这两个“可见/不可见”的差别是后面所有题目的根。你在做题时看到的段号本质上是程序员知道自己程序里有哪几段而不像页号那样是系统偷偷切出来的。分段的这个出发点直接决定了它的两个天然优势。第一个是便于共享和保护。你想让多个进程共用同一份代码只要让它们的段表项都指向同一个物理段就行粒度刚好是一整个逻辑模块干净利落。第二个是便于动态链接和动态增长。一个还没被调用的模块可以先不装入内存等真要用的时候再装栈段这类会变长的段也可以按需扩展。这些好处在做概念题、简答题时是标准答案但如果你不理解它从哪来背下来也容易忘。1.2 二维地址空间这个概念要建立起来段式管理里最关键的一个思维转变是地址从“一维”变成“二维”。在分页或者不分页的简单模型里逻辑地址就是一个从 0 开始的线性编号你给一个数系统告诉你它落在哪。但到了分段逻辑地址变成了“段号 段内偏移”这样一个二元组记作 (S, W)。S 是第几段W 是在这一段里从段首往后的偏移量。这个二元组的意义在于它把“归属”和“位置”拆开了。S 回答的是“这是哪个逻辑模块的”W 回答的是“在这个模块里偏移多少”。地址转换做的事情就是先拿 S 去段表里查出这个模块到底被放在物理内存的什么位置、有多长再把 W 拼上去。你可以把它类比成找一栋楼里的房间段号是楼号偏移是房间在这一栋楼里的门牌顺序。知道楼号才能知道这栋楼盖在哪条街知道门牌才能知道具体是哪间。一维地址就像只给一个连续编号你得自己猜哪段编号属于哪栋楼非常别扭。建立了二维地址的概念后面几乎所有题目都能顺着盘下来。题目给你一串形如 (0,430)、(1,10) 的逻辑地址你要做的第一件事永远是把它拆成段号和偏移两个数然后分别处理。很多同学做错题根源就在于看到 (0,430) 直接懵了没意识到这是两个独立的信息一个用来查表一个用来加。把这一点想通段式管理的题就完成了从“看不懂”到“有套路”的跨越。1.3 段式与页式的分工不同很多人学到这里会犯迷糊既然分页也能把程序打散放进不连续的内存为什么还要单独搞一个分段答案在于两者解决的根本问题不同。分页解决的是“物理内存怎么高效利用、怎么消除外部碎片”它按固定大小的页来切切的依据是物理内存的管理方便跟程序逻辑没有关系。分段解决的是“程序逻辑怎么被清晰地组织、共享和保护”它按程序自身的模块来切切完长短不一。打个比方分页像把一本书按固定页数撕成小册子方便散着装订分段像按章节把书拆开方便单独拿出来给别人看某一章。前者追求装载效率后者追求逻辑清晰。正因为目的不同页的大小固定段的大小可变页号对用户隐藏段号对用户可见分页的地址空间是一维的分段的地址空间是二维的。这些区别在选择题、判断题里出现频率极高。也正因为各有所长后来才有了段页式——先按逻辑把程序分段每段内部再按固定大小分页。这样既保留了分段在逻辑上的清晰和便于共享保护又用分页解决了段长可变带来的物理内存外部碎片问题。理解了段式和页式各自想干嘛段页式的出现就是水到渠成的事而不是一个孤立要背的知识点。这层逻辑在做综合题时特别有用题目问“为什么引入段页式”你答得上“兼顾逻辑划分和物理分配效率”才算答到点子上。2. 段表结构和地址转换硬件拆解2.1 段表项里每个字段的作用段式管理靠一张段表把逻辑段映射到物理内存进程有几个段段表就有几个表项。一个典型的段表项至少包含三样东西段长有的书写成限长、段基址段的物理起始地址、以及存取权限。有的教材还会加一个“段号”字段但实际上段号往往是用表项在段表中的下标隐式表示的所以并不一定真的存进去。段长这个字段是重中之重它是做越界检查的唯一依据。为什么必须有它因为段是可变的系统不能假设每段都是固定长度所以必须显式记录“这一段到底有多长”否则一个越界的偏移量就可能悄悄跑到别的段的地盘上去把不属于它的数据读出来或者写坏。段基址则是转换公式里那个加法项物理地址 段基址 段内偏移没有它就没法定位。存取权限管的是保护标明这个段能不能读、能不能写、能不能执行比如纯代码段通常只给“可读可执行”不给“可写”。这三样东西里段长和基址是每题必用存取权限主要在共享和保护类题目里登场。我建议你拿到一张段表先扫一眼有没有“存取权限”这一列有的话说明题目可能埋了保护相关的坑比如给出一个“读”的逻辑地址去访问一个只允许执行的段虽然不越界但仍然是非法访问。这类细节不看清列名很容易漏。把段表项的每个字段都搞清楚它们各自防的是什么做题时就不会机械地套公式而是知道每一步在干嘛。字段含义主要用途段长限长该段的实际长度越界检查判断偏移是否合法段基址该段在物理内存的起始地址与偏移相加得到物理地址存取权限读、写、执行等许可访问合法性判断支撑共享与保护段号可选表项对应的段编号多由下标隐式给出2.2 段表寄存器与两道越界检查段表本身也存在内存里那系统怎么知道段表在哪、有多长靠一对硬件寄存器段表基址寄存器记作 STBR和段表长度寄存器记作 STLR。前者存段表在内存中的起始地址后者存段表一共有多少个表项。每次地址转换硬件都要先读这两个寄存器才能定位到段表、再做后续查表。有了这两个寄存器地址转换里就有两道独立的越界检查这是最关键也最容易漏的地方。第一道检查段号 S 是否小于段表长度。如果 S 大于等于段表长度说明这个段号根本不存在直接触发越界中断后面的步骤都不用做了。第二道拿着合法的段号查出段表项后检查段内偏移 W 是否小于该段的段长。如果 W 大于等于段长说明偏移超出了这一段的实际范围同样是越界中断。这两道检查缺一不可。我见过不少同学只做第二道跳过了段号检查遇到 S 超出范围的题就答错。也有的同学只做段号检查忘了偏移检查那更常见。记住一句话段号越界查的是“表里有没有这一项”偏移越界查的是“这一项的范围够不够”。只有两道都过了才能做加法。加法本身没有难度难的是前面这两道门槛。把顺序记牢——先查段号、再查偏移、最后相加这九个字的顺序在脑子里固定下来做题会顺很多。2.3 逻辑地址的切分方式与单位换算逻辑地址怎么切成段号和偏移取决于题目给的地址格式。常见有两种一种是直接用十进制写出二元组像 (3,400)段号和偏移一目了然另一种是给一个完整的二进制或十六进制地址让你自己按位数切开比如高 k 位是段号、低 n 位是偏移。后一种更容易出错因为它把“切分”这一步也变成了考点。遇到第二种你要先看清段号和偏移各占多少位。假设逻辑地址高 4 位是段号、低 12 位是偏移那么段号最多 16 个0 到 15偏移范围是 0 到 4095低 12 位。给一个二进制地址 0001 0000 0000 0011切出来段号就是 1偏移是 0x003也就是 3。切分这步只要位数不看错一般不会错。真正坑人的是单位换算。段长经常写成 1K、2K、512B、768B 这种带单位的形式而偏移往往给一个纯数字。1K 到底是多少在存储管理的题目里K 几乎一律等于 1024不是 1000。所以段长 1K 意味着合法偏移是 0 到 1023偏移 1024 就已经越界。这一点我在开头也提到踩过坑把 1K 当 1000会导致边界附近的判断全错。同理十六进制题里偏移写成 0x0FFF你要能立刻反应出这是 4095。做这类题手边最好习惯性地把每个带单位的值都换算成统一的单位再比大小别一边用 K 一边用字节直接比很容易稀里糊涂地判错。3. 课堂练习4.1的完整演算过程3.1 题目条件整理我把课堂练习里最常见的一类题整理成下面这张段表后面的演算都基于它。段号从 0 到 4段表长度就是 5每个表项给出段长和基址基址单位都是字节。段号段长基址010244096120488192251212288376816384425620480拿到表的第一个动作是确认段表长度。段号是 0 到 4 连续排列一共 5 项所以段表长度 STLR 5合法的段号范围是 0 到 4。段号一旦达到 5 或更大第一道越界检查就直接拦下来。第二个动作把每一段的合法偏移范围在心里过一遍段 0 是 0 到 1023段 1 是 0 到 2047段 2 是 0 到 511段 3 是 0 到 767段 4 是 0 到 255。这一步别嫌麻烦它是后面判断越界的基准先算好就不用在每条地址上现推。整理条件的时候还有一个细节值得留意题目给的偏移量如果正好等于段长是越界还是合法答案是越界。因为偏移从 0 开始计数长度为 l 的段合法偏移是 0 到 l-1。段 2 段长 512合法偏移最大是 511偏移 512 就已经超出这是边界题里最容易错的点之一。我习惯在草稿纸边缘写下每段的最大合法偏移做题时对照着看比每次重新比大小稳得多。3.2 逐条转换手算过程现在有一组逻辑地址需要转换我按顺序一条条走把每一步都写清楚。转换一共三步查段号是否合法、查偏移是否合法、合法则物理地址 基址 偏移。第一条(0, 200)。段号 0小于段表长度 5段号合法。查段表得段 0 段长 1024偏移 200 小于 1024偏移合法。物理地址 4096 200 4296。第二条(1, 2048)。段号 1合法。段 1 段长 2048偏移 2048 不小于 2048越界。这条地址非法产生越界中断。注意这里就是刚才强调的边界问题偏移正好等于段长判定越界。第三条(2, 100)。段号 2 合法。段 2 段长 512偏移 100 小于 512合法。物理地址 12288 100 12388。第四条(3, 767)。段号 3 合法。段 3 段长 768偏移 767 小于 768合法正好落在最后一个合法位置上。物理地址 16384 767 17151。第五条(4, 256)。段号 4 合法。段 4 段长 256偏移 256 不小于 256越界。这条非法。第六条(5, 0)。段号 5不小于段表长度 5段号本身就非法连查表都不用直接越界中断。这是第一道检查拦下来的典型情况。第七条(1, 1500)。段号 1 合法。段 1 段长 2048偏移 1500 小于 2048合法。物理地址 8192 1500 9692。这条是故意放进来验证你对 2K 换算的2K 等于 20481500 在范围内。把上面的过程整理成一张表看起来更清楚逻辑地址段号是否合法偏移是否合法物理地址(0, 200)合法合法200 10244296(1, 2048)合法越界2048 ≥ 2048无越界中断(2, 100)合法合法100 51212388(3, 767)合法合法767 76817151(4, 256)合法越界256 ≥ 256无越界中断(5, 0)越界5 ≥ 5未执行无越界中断(1, 1500)合法合法1500 204896923.3 结果核对与易错点标注算完一遍别急着交卷回头核对的习惯能救回不少分。核对的时候重点看三处一是所有做了加法的条目基址和偏移有没有加错二是所有判越界的条目判断依据是段长还是段表长度有没有混淆三是边界附近的那些值比如偏移等于段长、段号等于段表长度判定方向对不对。这组题里(1, 2048) 和 (4, 256) 是最容易出错的因为它们都是“偏移正好等于段长”。有的同学一看 2048 是 2K段长也是 2K就觉得“一样大应该没问题吧”结果判成了合法。实际上偏移是从 0 开始数的长度为 2048 的空间装不下一个起点为 2048 的偏移最后那一格属于下一个区域。同理 (5, 0) 里段号 5 虽然偏移是 0看起来无害但段表里压根没有第 5 段查表都无从查起所以照样非法。把这两个“看起来无害其实非法”的例子记住以后遇到同类边界题就有免疫力了。还有一点越界判定发生时物理地址这一栏应该是空的或者写“越界中断”不要硬凑一个数出来。我见过有人把越界的地址也照加不误得出一个跑到别段去的物理地址这在原理上是完全错误的——越界检查的意义就是拦住这种越界访问让它根本到不了加法这一步。写答案时把“非法”的标志标清楚比什么都重要。4. 段的共享、保护与段式管理的短板4.1 共享段的段表登记方式段的共享是分段相对分页的一大亮点也是练习里经常单独出题的点。共享的原理其实很简单如果多个进程都要用同一份代码或数据那么它们各自的段表里对应那一项就填上相同的段基址和相同的段长指向物理内存里同一块区域。物理上只有一份逻辑上每个进程都觉得自己有这一段。这跟分页的共享比起来方便得多因为分页的共享需要保证页号和页内偏移都对齐粒度不好控制而段本身就是完整的逻辑模块共享起来边界清晰。共享的时候段表项里的内容不一定完全相同。最典型的是存取权限可以不一样比如进程 A 是这段代码的“主人”权限给的是可读可写可执行进程 B 只是共享调用者就只给可读可执行不给写。这样既能共享同一份物理代码又能防止 B 误改代码。这就是为什么共享类题目里段基址和段长往往要求各进程保持一致而权限字段却故意写得不一样考的就是你有没有意识到“指向同一物理段”和“权限可以不同”这两件事并不矛盾。做共享题还有一个小陷阱题目可能给出两个进程的段表问它们有没有共享某段。判断方法是两段基址相同、段长也相同基本可以判定它们指向同一物理区域。但要注意仅仅段号相同不能说明共享段号只是个逻辑编号进程 A 的段 1 和进程 B 的段 1 完全可能是两段不同的东西。真正的判据是物理地址的落点也就是基址加段长所框定的范围是否重合。把这个判据记牢共享题就不会被段号这个表象骗到。4.2 存取控制位的判分点存取控制位管的是“能干什么”跟越界检查管“能不能够得着”是两码事。一个访问请求即使通过了段号和偏移两道越界检查如果它的操作类型跟段表项里的权限对不上依然是非法的。这是保护机制的第二道防线题目里经常作为一个隐藏考点埋着。举个具体的场景段表里有一段标着权限只有“可执行”现在有一个写操作要到这一段里写数据。地址拆开一看段号合法、偏移也在段长范围内很多同学顺手就判合法了其实不对因为权限里没有“写”这个写请求应该被拒绝产生保护性中断。反过来一个“读”操作去访问一段只标了“可执行”的代码段通常也是非法的除非题目说明该权限包含读取。做题时一定要先看清楚段表项里权限那一列写的是什么再对照访问请求的操作类型。不同类型的段典型权限配置不太一样心里有个参照会快很多。纯代码段一般是可读可执行、不可写数据段是可读可写只读常量段则是只读。这些是常见实践里的约定题目如果没有特别说明按这个来理解基本不会跑偏。遇到权限判定题我建议在草稿上把“请求的操作”和“段的权限”并排写出来一项项比对比凭感觉判断靠谱得多。尤其是多段多请求的组合题一行一行对才不会漏。4.3 外部碎片与紧凑分段有个绕不开的缺点就是外部碎片。因为段长是可变的内存里反复地分配和回收不同大小的段之后会留下许多零散的、装不下新段的小空洞总的空闲内存可能够就是凑不出一块连续的大空间。这跟分页不一样分页用固定大小的页只会有页内的内部碎片不会有这种零零碎碎的外部碎片。解决外部碎片的办法之一是紧凑也就是把内存里正在使用的段挪一挪、往一块挤把分散的空洞合并成一大块。但紧凑的代价很高段在内存里移动了所有指向它的段表项都得跟着改而且移动过程中通常要暂停相关进程开销不小。所以紧凑一般是不得已才用平时更倾向于在分配时就选好策略尽量减少碎片的产生。内存分配算法在这里也用得上。首次适配是沿着空闲区列表找第一个够大的分区速度快但容易在低地址留下小碎片最佳适配是找最接近需求大小的空闲区看起来很省实际上会留下很多难以利用的边角料最坏适配反过来挑最大的空闲区留下的大空洞后续还能用。这些算法在分段管理里同样适用做概念题时经常和外部碎片一起考。理解分段的碎片来自“长度可变”也就理解了为什么后来段页式要把段再切成固定大小的页——本质上就是用分页的整齐去弥补分段的零散。5. 段页式这个延伸考点5.1 三级地址转换链路段页式是分段和分页的结合逻辑地址被切成三部分段号 S、段内页号 P、页内偏移 W。转换过程也比纯分段多了一步。先用段号查段表得到这一段的页表长度和页表起始地址再用页号去查该段对应的页表得到物理块号最后用块号乘以页面大小加上页内偏移才是最终的物理地址。我用一个具体例子走一遍。假设某进程有两个段段 0 的页表在物理地址 1000 处、长度为 4 页段 1 的页表在 2000 处、长度为 2 页页面大小是 1K。现在有一个逻辑地址 (0, 1, 300)意思是段 0、第 1 页、页内偏移 300。先查段表得段 0 的页表起始地址 1000页表长度 4页号 1 小于 4合法。页表项大小按 4 字节算第 1 项的位置是 1000 1×4 1004假设这里存着物理块号 7。那么物理地址 7 × 1024 300 7168 300 7468。整个过程三段信息各司其职缺一不可。注意这里又多了一层越界检查除了段号要小于段表长度页号还要小于该段页表的长度。也就是说段页式里有两级长度检查一级在段表一级在页表。题目如果给的是完整的段页式转换这两级都得判漏一个就会出错。我建议在草稿上把“段号→段表→页表长度→页号→页表→块号→物理地址”这条链子完整画一遍每到一个箭头就做一次检查形成肌肉记忆。5.2 访存次数与快表纯分段和纯分页的地址转换都至少要访问两次内存一次查表一次取数据。段页式因为多了一层页表最坏情况下要访问三次内存——查段表、查页表、取数据。访存次数直接关系到性能三次显然偏多所以就引入了快表联想存储器 TLB这类高速缓存把最近用过的段表项、页表项存进去。命中快表时就不用再去访问内存里的表了访存次数能降下来。这类“问访存次数”的题也经常和段式一起考。做的时候要看清楚题意没有快表、命中率是多少、段表是否常驻内存、是否算了取数据那次。常见问法是“不考虑快表时一次地址转换需要访问几次内存”段页式答案通常是 3 次。如果给了快表命中率就要加权算平均访存次数命中时少访问一次内存没命中时照旧。把这些前提一项项列清楚再算比套一个记不清的公式稳。段页式在概念上其实很好记段表项存的是页表的位置和长度页表项存的才是真正的物理块号。有人会把它和纯分段搞混以为段表项里存的还是段基址。区别就在于纯分段落到的是一段连续的物理内存所以能直接给基址段页式落到的是一组物理块所以必须先经过页表这一跳。把“段表管到页表、页表管到物理块”这条层次关系记牢段页式的题就有了骨架。6. 手算出错的高频位置与排查清单6.1 常见错误速查表我把做这类题最容易翻车的几种情况整理成一张表做题前扫一眼能避开大半的坑。错误表现根本原因正确做法把偏移等于段长判成合法忘了偏移从 0 开始合法偏移是 0 到段长减 1漏查段号是否越界只做了偏移检查先查段号是否小于段表长度把 1K 当 1000 用单位概念不清存储管理里 K 一律取 1024越界地址也照加不误忘了越界会中断越界直接判非法不给物理地址混淆段长和段表长度两个“长度”没分清段长是某段的长度段表长度是表项个数忽略存取权限只看越界不看权限权限不匹配即非法哪怕不越界段页式漏查页号越界少做了一级检查段号、页号两级都要判这张表里的每一条都是我在做题和给别人讲题时反复见到的。尤其是“段长和段表长度”这两个概念名字里都有“长度”特别容易在紧张时混用。你可以给自己一个固定说法段表长度是“有几行”某段段长是“这一行管多长”。每次做题嘴里默念一遍基本上就不会串。至于单位换算我的建议是把所有带 K 的值当场换成不带 K 的字节数写下来再比慢一点但绝不会错。6.2 我自己的检查顺序做段式地址转换我给自己定的检查顺序是固定的一路走下来几乎不会漏。第一步确认段表长度也就是合法段号的取值范围。第二步对每条逻辑地址先拆出段号用第一步的结果判段号是否越界越界就直接收尾。第三步段号合法才去查段长判断偏移是否越界这一步特别注意边界值等于段长的情况。第四步前两步都通过才做加法得到物理地址顺带瞄一眼权限列看操作类型对不对。第五步全部算完回头专挑那些“等号”和“接近边界”的条目重算一遍。这个顺序的价值在于它是“漏斗式”的越在前面被拦下后面的步骤越省事也越不容易出错。段号越界的地址根本不用查段长越界的偏移也根本不用做加法每一层都在缩小范围。很多同学做题出错是因为跳步——比如跳过段号检查直接查段长或者干脆一次性把加法做了再回头看合不合法。跳步看着快实际上把简单问题复杂化了。把这个漏斗顺序练熟速度反而会提上来因为你的注意力都花在真正需要判断的地方。对于段页式我在这个顺序前面再加两道拆三段段号、页号、偏移段号合法后先查页表长度页号也要小于它。其余思路一致。练几次之后这套顺序会变成条件反射看到题就知道从哪下手不再对着一堆数字发懵。6.3 几个容易忽略的判分细节最后说几个判分上的小细节平时不注意考试时容易无缘无故丢分。第一个写越界中断的时候最好把是哪一种说清楚是段号越界还是偏移越界有的题目就按这个细分给分。第二个物理地址算出来是多少就写多少别写成十六进制十进制混用的样子单位要跟题目保持一致。第三个涉及共享的题判断依据要写“基址和段长相同”只说“段号相同”是拿不到分的因为段号只是逻辑编号。还有一个很多同学会忽略的点逻辑地址的段号是编号不是地址别把它和物理地址里的部分搞混。有的题给出逻辑地址是十六进制的 0x1003段号高 4 位是 1偏移是 0x003这时候段号 1 指的是段表里第 1 项不是物理地址 0x1 什么。把这层关系理顺就不容易把编号和地址混为一谈。说实话段式内存管理的题目翻来覆去就是这几类真正拉开差距的不是你记住了多少公式而是你有没有把每一步为什么这么做想明白。我自己带同学做这类练习的时候最常说的一句话就是别把它当成算术题把它当成一条“逻辑地址怎么找到家”的流水线来讲给自己听。段号是门牌号对应哪栋楼段表是楼层索引段长是这栋楼有几层偏移是你上到第几层越界检查是门口的保安。你能完整地把这条故事讲通题目自然就做对了。这套东西往后延伸到段页式、虚拟内存用的还是同一套思维方式早一点把它想透后面学起来会轻松很多。
返回列表