存储管理这块,我当年在电子科大操作系统网课上听得最过瘾的,就是从“简单存储管理”开始的一段。它不像后面的分页、分段那么绕,几乎全靠画图就能理解,但恰恰是这些“简单”的方案,把操作系统的内存管理为什么这么设计给打通了。
如果一句话来概括,存储管理要回答的问题就三个:程序要放在内存的哪里?怎么放?程序运行时要访问的数据在哪个地址,怎么找到?这三个问题在简单存储管理阶段,答案都是“直接找个空的地方塞进去”,但塞进去之后怎么处理地址、怎么分配、怎么回收,细节全在这里。
这篇笔记适合正在学操作系统、背了一堆概念但还没串成线的同学,也适合准备期末复习想快速把存储管理第一部分吃透的人。我会按网课的思路,把地址转换、固定分区、动态分区、四种分配算法这些内容全部过一遍,每一步都附上我自己的理解,尽量把网课里那些“老师一句话带过”但实际很重要的细节也补全。
1. 问题缘起:为什么进程不能随便往内存里塞
1.1 从“程序跑起来”说起
一个C程序从源码到在内存中运行,要依次经过编译、链接、装入三步。
编译阶段,编译器把源文件变成目标模块(.o或.obj),这时候代码里的变量名、函数名还是符号地址,整个模块的地址都还没有确定。链接阶段,链接器把多个目标模块拼在一起,同时把符号地址替换成逻辑地址——也就是用户程序眼中的地址空间,一般从0开始往后排。真正到了装入阶段,才需要把逻辑地址换算成内存中的物理地址。
这里的核心矛盾就是:编译和链接阶段,程序并不知道自己会被放到物理内存的哪个位置。换句话说,程序眼里的“0号地址”和内存条上的“0号地址”根本不是一回事。谁来解决这个矛盾?就是存储管理中的地址重定位机制。
我上课时喜欢用一个搬家类比。程序打包行李的时候,箱子里每个东西都按老房子的房间号做了标记,这是逻辑地址。但搬到新房子之后,东西具体放进哪个房间,得看新房子的空间结构来现排,绝对不能按老房子的房间号直接放。负责“重新安排房间”并让主人依然能找到东西的过程,就是存储管理的地址重定位。
1.2 逻辑地址与物理地址:最容易绕晕的一对
很多人第一次学存储管理,会被逻辑地址和物理地址绕晕,尤其是“逻辑地址从0开始”这句话。所谓逻辑地址,是CPU执行指令时,指令中给出的地址,它是相对于“这个进程自己的地址空间”而言的。而物理地址是内存单元的真实地址,在内存条上对应唯一的存储单元。
如果程序在装入时就已经被安排在固定的物理位置,那么装入程序在把程序代码数据装入内存时,直接一次性把所有逻辑地址改成物理地址,这叫静态重定位,也叫可重定位装入。这种方式实现简单,不需要额外硬件支持,但装进内存后位置就不能动了。
另一种更灵活的是动态重定位,也叫动态运行时装入。它依赖一个叫重定位寄存器的硬件来帮忙,程序在内存里的起始地址放进这个寄存器,CPU每次访问地址时,硬件自动把“逻辑地址+重定位寄存器里的值”算出来,得到真实的物理地址。这种方式的好处是,程序在内存里可以移动,移动之后只要修改寄存器里的起始地址就行。
把静态和动态重定位搞懂,是理解这一章所有分配方案的前提。因为后面有一种叫“紧凑”的技术,如果系统不支持动态重定位,它根本没法实现。这在后面第3节会细讲。
1.3 谁来搬这个“家”:MMU与地址转换
这里就要引入MMU(内存管理单元)的概念。MMU是CPU里负责地址转换的硬件模块。在现代操作系统里,逻辑地址到物理地址的转换绝大部分都是由它完成的,操作系统本身只负责设置规则、维护数据结构,真正每次访问内存都走一遍转换逻辑的,是MMU这个硬件工兵。
为什么要强调这一点?因为在简单存储管理的几种方案里,有些只需要静态重定位就够了(比如单一连续分配),但到了动态分区分配,尤其是我后面要讲的“紧凑”技术,就必须依赖动态重定位。原因很简单:紧凑要把已经运行的程序位置整体移动,如果用的是静态重定位,程序一旦装进内存就“焊死”了,根本挪不动。
MMU还有一个重要职责是越界保护。每次访问地址时,它除了做加法,还要判断算出来的地址是否在分配给该进程的合法范围内,如果越界就直接报错,防止一个进程乱窜去读写别的进程的内存空间。简单存储管理阶段,这个保护机制大多依靠界限寄存器来实现,本质上也是MMU在干活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种简单存储管理方案逐个拆
2.1 单一连续分配:一台机器一次只伺候一个程序
单一连续分配是存储管理里最简单粗暴的方案。它把内存分成两个区域:一块给操作系统常驻内存使用,另一块给用户程序。任何时候,内存里最多只有一个用户程序在运行。
这种方案几乎不用什么数据结构,管理方式就是把内存划分为系统区和用户区,用户程序进入内存时,直接加载到用户区的起始位置,运行结束后释放。它需要处理的问题是:怎么防止用户程序越界访问到系统区?答案是加一个界限寄存器,存放用户区的起始地址和长度,每次访问时硬件检查地址是否越界。
它的优点是简单到几乎不用管理,缺点是内存利用率极低。一个程序只用了用户区的三分之一,剩下的三分之二就完全浪费了。这种浪费还没有专门的叫法,其实就是一种极端的内部碎片——区划在那里,程序没用满,谁也顶不上。
这种方案在现代已经不存在了,但它的意义在于:存储管理要解决的“分配、回收、保护”三件事,它全部以最朴素的方式做了一遍。理解它,后面所有方案都是在“分配方式和空间利用率”上做文章。
2.2 固定分区分配:把内存切成格子间
固定分区分配比单一连续分配进一步:内存被预先切成若干个固定大小的分区,每个分区可以装一个程序。分区的大小可以相等,也可以不等。不等的好处是,既能装下大作业,又能照顾多个小作业,减少空间浪费。
这种方案的核心数据结构是一张分区说明表,记录每个分区的分区号、起始地址、大小、状态(空闲还是已分配)。分配时,从表里找状态为空闲且能装下作业的分区,把作业放进去并把状态改为“已分配”。
固定分区有两个特点值得注意。第一,它支持多道程序设计,多个程序可以同时在内存里,提高了CPU利用率。第二,它仍然会产生严重的内部碎片。因为分区大小是固定的,一个只有1KB的作业放进一个10KB的分区,剩余9KB谁也没法用。反过来,如果分区大小都设成一样,小作业浪费,大作业放不下;如果分区大小不等,又需要考虑到底切成哪些尺寸才能满足各种作业需求。
我记得网课里给过一个例子:内存64MB,切成4个分区,大小分别是8MB、16MB、24MB、16MB。来一个12MB的作业,它可以进16MB分区,但8MB分区就永远装不下它;如果连续来几个小作业,又会把大分区切成碎块。这种分区方案的灵活性很差,作业申请内存时经常处于“要么放不下、要么浪费多”的尴尬状态。
这个方案在实际操作系统里也没有现代版本在用,但它让学生第一次体会到“内部碎片”这个概念。以后学到分页、分段时,你会无数次和“内部碎片”、“外部碎片”打交道,固定分区就是理解内部碎片的第一个模型。
2.3 动态分区分配:用多少给多少,要多大切多大
动态分区分配完全不同,它不在作业运行前把内存边界定死,而是等作业真正要装入时,才根据作业的大小从内存里切出一块正好够用的区域。
这样带来的最大好处是:内存分区的大小和数量是随着作业装入动态变化的,理论上不存在固定分区那种“放不下小作业又浪费大分区”的尴尬。分配时操作系统维护一张空闲分区表或空闲分区链,记录当前所有空闲区域。作业到达时,从空闲分区中找一个足够大的区域分配给它;作业结束时,把这块区域重新标记为空闲。
动态分区听起来完美,但它有一个致命的副产品——外部碎片。随着作业不断装入和退出,内存会逐渐被切割成很多细小、零散的空闲区域。可能所有空闲区域加起来足够大,但任何一个单独的空闲区域都放不下一个新的作业。这就是“有空间但用不上”的典型场景。
我在学习时最深刻的感受是三种方案之间的对立逻辑很清晰:单一连续分配是“一块地全给你”,固定分区是“预先切好格子等你”,动态分区是“你要多少我就切多少”。固定分区的问题是切得不合适,动态分区的问题是切着切着把整块地切碎了。
3. 动态分区分配:四种空闲分区分配算法怎么选
3.1 内存分配与回收的基本流程
在讲四种算法之前,先明确动态分区分配的完整流程。当一个新的作业请求大小为size的内存时,系统在空闲分区表/链中寻找合适的空闲分区。找到后分两种情况处理:如果这个空闲分区大小刚好等于size,直接整体分配;如果大于size,系统把该分区分成两块,一块大小为size分配给作业,另一块剩余部分作为新的空闲分区放回空闲表。
回收时恰恰相反。作业释放内存后,它所占的区域要重新加入空闲分区表。这里最关键的一步是:看看这个区域相邻的上、下两个邻居是不是空闲分区。如果是,必须把相邻的空闲区合并成一个更大的空闲区,否则内存会越来越碎,继续产生更多外部碎片。我见过不少同学做练习题时忘了合并这一步,结果越算越不对劲,就是这个环节出了问题。
所以动态分区的完整流程可以写成这样一套伪代码逻辑:
text复制分配内存:
1. 根据请求大小 size,按某种策略从空闲分区表/链中查找候选分区
2. 若候选分区大小 == size,摘除该节点,整块分配
3. 若候选分区大小 > size,从该分区头部切出 size,剩余部分作为新空闲分区留在表中
4. 更新分区说明表,记录作业号、起始地址、长度
5. 若找不到满足条件的空闲分区,则分配失败,可考虑紧凑后再次尝试
回收内存:
1. 根据释放作业的起始地址和长度,找到其在内存中的位置
2. 检查上邻空闲区,若上邻空闲则合并
3. 检查下邻空闲区,若下邻空闲则合并
4. 更新分区说明表,将合并后的区域标记为空闲
这个分配-回收流程本身不难,难在选择“找哪一块”。同一个空闲分区表,按不同策略选择,最终的内存利用率和碎片情况天差地别。
3.2 首次适应算法:从低地址开始找
首次适应算法(First Fit)的思路很直白:从空闲分区表的第一条记录开始顺序查找,找到第一个能装下作业的分区就立刻分配。
它的优点是倾向于优先利用低地址部分的空闲分区,让高地址部分保留较大的空白区,为后面可能到达的大作业留出空间。同时查找速度快,因为不需要遍历所有分区,只要找到能放下的就停。
但代价也很明显:低地址部分会被频繁分割,产生越来越多的小碎片,每次都要从低地址扫过这些小碎片才能找到合适位置,查找开销会越来越大。解决这个问题的一个思路就是给它加一个“从上次分配位置继续”的变种,也就是循环首次适应。
首次适应算法在网课题目里出现频率极高,也是最常考的一种。需要特别注意它的一个性质:空闲分区按地址递增排列时,它会优先使用低地址空间。画内存分配图时,我会把空闲分区表按地址从低到高排好,然后从表头开始找,这个习惯能避免很多低级错误。
3.3 循环首次适应算法:接着上次的位置找
循环首次适应算法(Next Fit)和首次适应的唯一区别是:它不从链表头开始找,而是从上次分配的下一个位置开始顺序查找,查找到尾部之后,再循环回到头部继续。
这个设计针对的是首次适应算法“总是从低地址开始扫描、导致低地址碎片越来越多”的毛病。循环首次适应能让整个内存的空闲分区被均匀使用,减少低地址区域的碎片堆积。
不过它也有新问题:为了均匀使用,它可能会把大块空闲区域也切碎,导致高地址部分没有足够的连续大空间。换句话说,它的内存分配效率通常比首次适应稍差,但分配速度稳定、开销均匀。
这两种算法各有侧重,网课考点上经常拿它们作对比:首次适应倾向于保留高地址大空间,适合大作业较多的场景;循环首次适应分配更均匀,但大空间容易被拆散。如果面试被问到“首适和循环首适怎么选”,我的理解是:如果作业大小分布比较均衡,循环首次适应更稳;如果经常有大作业需要连续大块空间,首次适应更合适。
3.4 最佳适应算法:找最接近的分区
最佳适应算法(Best Fit)的原则是:把作业放进所有空闲分区中最小的、但又能装下它的那个分区里。这样做的好处是每次分配的剩余空闲区最小,从源头上减少了产生大碎片的机会。
听起来很美,但实际操作中最住适应算法反而最容易造成大量微小碎片。因为每次都找最接近作业大小的分区,分完后留下的剩余区间往往只有几十字节、几百字节,这些微小碎片几乎没法再被任何作业利用,最终变成外部碎片。
所以“最佳”二字名不副实,它只在单次分配上省了空间,但整体上可能让内存碎片化更严重。实现上,最佳适应算法需要每次都遍历整个空闲分区表才能找到“最合适”的分区,时间开销也最大。
网课里给过一个很直观的例子:空闲分区有200KB、150KB、100KB三个,来了一个60KB的作业。最佳适应会选100KB的分区,分完剩40KB,这40KB基本就被“锁死”了,后面很难再装下任何作业。如果换成首次适应,作业会进200KB的分区,剩下的140KB还能继续用。单看这一次分配,最佳适应省了空间,但整体的代价其实更高。
3.5 最坏适应算法:专挑大的块下手
最坏适应算法(Worst Fit)和最佳适应正好相反,它每次都选择最大的空闲分区来分配,先把最大的蛋糕切一块下来。
这么做的逻辑是:大分区被分掉一部分后,剩余的部分还是足够大,仍然能装载其他作业。这样一来,它避免产生大量微小的、无法利用的碎片,对中小型作业比较友好。缺点是最大的连续空闲区会被一次次拆小,如果有大作业在后面到达,可能会因为找不到足够大的连续区间而失败。
四种算法各有利弊,上课时最有效的方法是把它们放到同一个例子中模拟一遍。给空闲分区表设几个分区,按不同算法分配同一个作业,观察剩余空间的变化,比背十遍定义都有用。
我整理了一个表,方便对比记忆:
| 算法 | 选择策略 | 碎片特点 | 查找开销 | 适用场景 |
|---|---|---|---|---|
| 首次适应 | 第一个满足的分区 | 低地址碎片多 | 低 | 通用、最常用 |
| 循环首次适应 | 从上次位置找下一个 | 分配均匀但大分区易碎 | 中 | 作业大小较均衡 |
| 最佳适应 | 最小的满足分区 | 微小碎片多,最严重 | 最高 | 不推荐单独使用 |
| 最坏适应 | 最大的分区 | 大分区被拆散 | 中 | 中小作业为主 |
3.6 碎片与紧凑:不解决碎片,算法再精巧都白搭
不管是哪种动态分区算法,都无法避免外部碎片的产生,只是程度不同。外部碎片的本质是:内存中有许多零散的空闲区,但它们不连续,无法被任何一个作业整体利用。
解决外部碎片有两个方向。第一个方向是紧凑:把所有已分配的分区移动到内存一端,使分散的空闲区集中成一整块连续的大空闲区。这一步需要硬件支持动态重定位,因为移动程序后要更新重定位寄存器的值。紧凑的代价是时间开销很大,每移动一次,所有进程都要一起搬家,期间CPU不能并行处理这些程序。
我记得网课里特别强调了一句:紧凑不是靠改程序完成的,它搬的是程序在物理内存中的位置,同时更新重定位寄存器。程序自身携带的逻辑地址完全不用动。这个理解很关键,否则你会在做作业题时纠结“程序挪了之后里面的地址怎么办”,一旦明白了,整个过程就清晰了。
第二个方向是彻底放弃“程序必须放在连续区域”的假设——这就是分页存储管理的起点。分页把逻辑地址空间和物理地址空间都切成等长的页和页框,然后再按页对应,再也不要求多个页框物理连续。这部分内容是网课“存储管理2”的核心,但它的所有动机都来自于简单存储管理阶段的碎片问题。所以,如果你觉得分页难,多半不是分页本身难,而是没把碎片的来龙去脉搞清楚。
4. 在课程知识地图中的位置:它为什么被称为“地基”
4.1 简单存储管理的边界
简单存储管理这三种方案的共同点,是都要求程序在内存中必须占一段连续的物理地址空间。这个假设是整个简单存储管理最大的边界,也是后续所有问题的源头。
因为要求连续,单一连续分配和固定分区就必然产生内部碎片;因为要求连续,动态分区就必然产生外部碎片。内部碎片和外部碎片的本质区别就在这里:内部碎片是已经分给作业、但作业用不上的那一部分空间;外部碎片是没分给任何作业、却因为不连续而无法被整体使用的空间。
网课在这里花了很大篇幅强调,接下来学到的所有内存管理技术,本质都是在“如何打破连续性的限制”上面下功夫。分页打破物理连续性,分段打破逻辑连续性,段页式想两者兼得,虚拟存储则进一步想“连内存空间都可以不一次装完”。把简单存储管理这一层的矛盾看透,后面的课程就变成了“追着问题看答案”,而不是硬背方案。
4.2 为什么说它是理解分页的基础
很多人觉得分页很难,跳过了前面直接去学分页,结果是背了一堆术语却不知道为什么要分页。如果先认真学完动态分区和碎片,你会在学到分页那一章时立刻明白:分页要解决的就是外部碎片和连续分配互相绑架的问题。
分页的思路是把逻辑地址空间分成等大的“页”,物理内存分成等大的“页框”,页和页框大小相同。通过页表建立逻辑页到物理页框的映射,一个程序的第一页可以放在物理内存的页框3,第二页放在页框8,第三页放在页框1,互不相邻也完全没关系。这样连续性问题在物理层面被消解了,外部碎片被消解了,只剩下页表带来的内存开销和管理复杂度。
而我之所以说简单存储管理是基础,是因为分页的每一步设计都是对简单存储管理某一个痛点的直接回应。不了解痛点,就不理解设计。就好比一个人不知道骨折病人为什么要打钢板,直接背“打钢板的步骤”是学不会骨科手术的。
4.3 网课复习建议与考点提示
如果你正在跟这门网课,我的建议是复习这一章时抓住一条主线:从连续到不连续,从静态到动态。这条主线上的每一个概念,都尽量用“它到底解决了什么问题”来问自己。
常见的考点可以整理成这么几类:一是逻辑地址和物理地址的换算,尤其是动态重定位下CPU访问地址的计算;二是各种分配算法的模拟,给出一组空闲分区和作业请求序列,让你写出分配结果;三是比较题,比较固定分区和动态分区的优缺点,或者比较四种动态分区算法的差异;四是碎片类型判断题,给定一个内存状态图,判断存在内部碎片还是外部碎片。
这些题型都不是靠背定义能拿分的,需要动手画内存图、填分区表。我的经验是拿一张草稿纸,把内存画成一条从低地址到高地址的横线,每次分配和回收都动手标一遍,比任何复习资料都管用。
如果时间充裕,我还建议把课程里的例题全部按“画内存状态图”的方式重做一遍,不要只看答案觉得懂了。因为考试时这类题的丢分点往往不是算法理解,而是回收相邻空闲区时漏合并、分配大分区时忘记切分剩余空间这些细节。
5. 学习过程中常见的疑点和坑
5.1 内部碎片和外部碎片,我总是分不清
这是这门课被问得最多的问题。我的记忆方法很简单:看碎片“属于谁”。内部碎片是已经划给某个作业的分区里,剩下来没人用的部分,这个分区有主人,但主人用不上里面的小尾巴。外部碎片是所有空闲区之外、因为分散而无法利用的空间,它们没有主人,是内存里的“公共资源”,却因为不连续而集体失效。
拿宿舍来比喻:内部碎片是分给你的一整个柜子,你只装了一半,剩下的一半别人不能动;外部碎片是四个人分别占了四个箱子,但每个箱子里都有一小块空位,把这些空位加起来可能够放一张桌子,但因为没有一个大柜子,桌子就是放不进去。
进一步说,内部碎片通常出现在固定分区和分页/段页式这类固定粒度的分配方案中,外部碎片则主要出现在动态分区这类按需切分的方案中。判断题目给的是哪种碎片,就看它的空闲空间有没有被“圈进”某个已分配的区域内。
5.2 紧凑到底是在“挪数据”还是在“改地址”
紧凑的全过程,网课一句话能讲完:把已分配的分区移到内存一端。但有人会问,程序在内存中移动后,指令里的地址不就用错了吗?
答案在于,能执行紧凑的前提是系统采用了动态重定位。所有程序在运行过程中,指令里携带的都是逻辑地址,CPU取地址时靠重定位寄存器换算成物理地址。紧凑之后,系统只需要更新每个进程中与物理位置相关的重定位寄存器值,程序里的逻辑地址完全不变。换句话说,紧凑搬的是数据和起始地址记录,而不是修改程序本身。
这里也解释了为什么单一连续分配和固定分区不能做紧凑:它们要么是静态重定位,要么没有重定位寄存器,程序的位置一旦固定就不能动。动态重定位+重定位寄存器,是紧凑技术的硬件基础。
5.3 作业题里最常见的几类题型
如果你正在刷题,多留意下面这几类。
第一类是动态分区分配模拟题。题目会给你空闲分区表和作业请求序列,让你分别用首次适应、最佳适应等算法写出分配结果。这种题的关键是顺序不能乱,尤其回收时要记得合并相邻空闲区。
第二类是地址转换题。给出逻辑地址和重定位寄存器的值,让你算物理地址。其实就是一个加法,但题目常见陷阱是:逻辑地址超过了分区长度,需要判断越界。比如分区长度是200KB,重定位寄存器值是100KB,逻辑地址是180KB,物理地址是280KB没问题;但如果逻辑地址是250KB,它已经超出分区范围,直接判越界。
第三类是概念辨析题。比如比较固定分区和动态分区的碎片情况,或者判断以下哪些技术可以消除外部碎片。紧凑能消除外部碎片,但它是暂时的,新作业还会产生新碎片;分页则从机制上消除外部碎片,可它把碎片问题转化成了页表开销和内部碎片(页内碎片)。
掌握了这三类题,期末这部分基本稳了。
最后说点个人体会。这个网课系列里,我反而觉得“简单存储管理”这一节最值得反复听两遍。第一次听的时候觉得全是概念,无非是把内存分来分去;第二次听,才品出味道来——固定分区、动态分区、四种分配算法,这些看起来零散的知识点其实全都在围着“碎片”打转,而碎片问题的根源,就是那个“连续”二字。所以如果要给学弟学妹们一个建议,我会说:学这块内容时,不要急着背算法流程,先花十分钟想清楚“程序为什么要连续存放”,后面所有章节都能顺着这条线理解得轻松许多。
