资讯动态

分区存储管理:从固定分区到可变分区的内存分配原理与碎片问题

发布时间:2026/10/15 6:42:13 来源:尧图企业网站定制
1. 从一段“内存告急”的调试经历说起前阵子帮一个刚入行的朋友看他写的一个简易内存分配模拟器代码逻辑看着没什么大毛病但跑起来就是各种诡异现象有时候申请两块内存第二块的起始地址居然和第一块重叠了有时候释放了中间一块再申请一块大的却死活分配不出来明明总空闲空间是够的。他一脸茫然地问我“这不就是内存管理吗怎么这么难”我一看他的实现问题其实很典型——他脑子里没有“分区”这个概念只是简单地用一个数组模拟内存申请就往后挪指针释放就把标记清零。这种做法在真实操作系统里根本行不通因为内存不是一条无限长的传送带它是一块有限的、需要被反复切割和回收的资源。分区存储管理就是操作系统用来解决“怎么把有限的内存合理分给多个进程”这个问题的第一代成熟方案也是理解后续分页、分段、虚拟内存绕不开的地基。这篇文章我想聊的不是教科书上那种干巴巴的定义而是把分区存储管理这件事拆开揉碎讲清楚它到底在解决什么问题、几种经典分区方式各自的取舍逻辑、实际写代码模拟时最容易踩的坑以及为什么今天你几乎看不到纯粹的分区管理了但它的思想却无处不在。不管你是正在学操作系统课程的学生还是想补一补底层知识的开发者或者只是好奇“我点开一个程序它的内存到底是怎么被安排的”这篇内容应该都能给你一些实在的收获。我下面会从最朴素的需求出发一步步推到具体实现中间会穿插我自己写模拟器时踩过的坑和总结出来的经验尽量让你看完之后不仅能理解概念还能自己动手写一个能跑的分区管理模拟程序。2. 分区管理到底在管什么把“一块地”切成能用的“房间”2.1 内存管理的核心矛盾有限资源与无限需求要理解分区存储管理得先回到它诞生的那个场景。计算机的物理内存是有限的比如早期机器可能只有 64KB 甚至更少而用户想同时运行多个程序每个程序都需要一块自己的内存空间。这就产生了一个根本矛盾内存总量固定但进程数量和每个进程的需求是动态变化的。操作系统作为“大管家”必须回答几个问题新来的进程能不能放进来放在哪里放进来之后怎么保证它不会踩到别的进程的内存进程结束后它占的那块地怎么回收再利用分区存储管理就是针对这些问题给出的第一套系统化答案。它的核心思路非常直白——把内存划分成若干个分区每个分区装一个进程分区之间互不干扰。你可以把物理内存想象成一栋大楼分区就是里面的一个个房间。早期的做法是每个房间大小固定后来发现有的程序“块头大”住不下有的程序“个子小”却占了一整间太浪费于是又演化出了大小可变的房间。这个从固定到可变的过程就是分区管理思想演进的主线。2.2 分区管理与后续内存管理方式的本质区别很多人学到这里会有一个疑问分区管理和后面的分页、分段到底差在哪我当初也卡过这个点。后来想明白了关键区别在于**“连续性”**。分区存储管理要求分配给一个进程的内存必须是一整块连续的区域。进程有多大就得给它切一块多大的连续空间。这带来一个直接后果内存用着用着就会碎成很多不连续的小块也就是所谓的外部碎片。而分页和分段打破了连续性约束把进程拆成小单元分散存放从而缓解了碎片问题。所以分区管理是“整块分配”思路的代表理解它的局限性才能真正理解为什么后来要发明更复杂的分页机制。换句话说分区管理不是被淘汰的废物而是理解内存管理演进逻辑的起点。你只有先搞懂“整块分配”会遇到什么麻烦才能体会到“拆散分配”的精妙之处。2.3 一个生活化类比停车场与车位分配我用停车场来类比一下分区管理的几种方式会好理解很多。固定分区就像停车场里画好了一个个固定大小的车位小车停小车位大车停大车位。问题是如果来的全是小车大车位就空着浪费如果来了一辆超大车所有车位都停不下只能拒绝。可变分区则像是一个没有画线的空地管理员根据来的车的大小现场划一块刚好够用的区域给它停。听起来很灵活对吧但问题来了车进进出出之后空地被切得七零八落虽然总空面积够停一辆大车但没有一块连续的空地能容纳它这就是外部碎片。这个停车场类比几乎能解释分区管理所有核心问题后面讲具体算法时我会反复回到这个场景。3. 固定分区最简单也最“浪费”的方案3.1 固定分区的划分方式与运行逻辑固定分区是分区管理里最朴素的一种。系统在启动时就把内存划分成若干个大小固定、数量固定的分区这些分区一旦划好运行期间就不再改变。每个分区可以装一个进程操作系统通常维护一张分区说明表记录每个分区的起始地址、大小和当前是否被占用。当一个新进程需要内存时系统就查这张表找一个足够大的空闲分区分配给它。如果进程所需内存比所有空闲分区都大那就只能拒绝哪怕这些空闲分区的总和大得多也没用因为分区不能拆分也不能合并。这种方式的实现极其简单分配和回收都只是改一下标志位不需要复杂的数据结构。在早期批处理系统中这种简单性是有价值的因为那时候内存小、进程少、需求相对可预测。3.2 固定分区的大小设计等分还是不等分固定分区有两种常见设计分区大小全部相等和分区大小不等。全等分区实现最简单但灵活性最差。如果所有分区都是 64KB那么一个只需要 10KB 的进程也会占掉 64KB浪费 54KB而一个需要 100KB 的进程则完全放不进来。这种浪费发生在分区内部所以叫内部碎片。不等分分区则把内存划成几种不同大小的分区比如几个 8KB 的、几个 32KB 的、几个 128KB 的。这样小进程用小分区大进程用大分区内部碎片相对少一些。但问题依然存在如果某一类分区全被占满而另一类分区大量空闲新来的进程还是进不去。这种设计需要对进程大小分布有较好的预判否则分区比例配错了浪费照样严重。我当初写模拟器时试过全等分区结果发现模拟出来的内存利用率低得吓人一个 4KB 的进程占着 64KB 的分区利用率只有 6%。后来改成不等分利用率上去了但代码复杂度也上去了需要维护多个空闲分区队列。3.3 固定分区的内部碎片问题与实测数据内部碎片是固定分区绕不开的痛点。我做过一个简单的模拟实验假设内存 1MB划分成 16 个 64KB 的分区然后随机生成 100 个大小在 1KB 到 60KB 之间的进程请求。跑下来平均内存利用率大概只有 55% 左右也就是说将近一半的内存被浪费在了分区内部的空隙里。这个数据说明什么说明固定分区在进程大小差异大的场景下非常不划算。但它的优点也很明显分配速度快实现简单不会产生外部碎片。所以在一些对实时性要求高、进程大小可预测的嵌入式场景里固定分区思想依然有它的位置。提示如果你在写固定分区模拟器记得把分区说明表设计成数组或链表都行但一定要记录“已用/空闲”状态和分区大小否则分配时无法判断哪个分区能用。4. 可变分区灵活背后的碎片噩梦4.1 可变分区的动态切割与回收机制可变分区是对固定分区的直接改进。系统启动时内存是一整块大的空闲区不预先划分。每当有进程请求内存就从当前的空闲区里切一块刚好满足需求或按某种策略选一块的区域分配出去剩下的部分继续作为空闲区保留。进程结束时它占用的区域被回收重新变成空闲区。这种方式的好处显而易见分区大小按需定制不会像固定分区那样出现大量内部碎片。一个需要 10KB 的进程就给它 10KB不会多占。内存利用率在理想情况下可以很高。但代价是管理复杂度上升。系统需要维护一张空闲分区表或空闲分区链记录每一块空闲区的起始地址和大小。分配时要查找合适的空闲区回收时要判断能否与相邻的空闲区合并。这些操作如果设计不好性能会很难看。4.2 分配算法三兄弟首次适应、最佳适应、最坏适应可变分区最核心的设计决策就是当有多个空闲区都能满足请求时选哪一个这就引出了三种经典分配算法。首次适应算法从空闲区链的头部开始扫描找到第一个大小足够的空闲区就分配。它的优点是实现简单、速度快不需要遍历整个链表。缺点是低地址部分会被反复切割留下很多小碎片而高地址部分的大空闲区长期得不到使用。最佳适应算法遍历整个空闲区链找到能满足请求且最小的那个空闲区来分配。直觉上这最节省空间但实际效果往往适得其反。因为它总是优先切割那些刚好够用的空闲区结果留下大量极小的、几乎无法再利用的碎片。我实测过最佳适应在长时间运行后空闲区数量会急剧膨胀很多碎片小到连 1KB 的请求都满足不了。最坏适应算法反过来总是选最大的空闲区来分配。这样切割后剩下的空闲区仍然比较大还能继续用。听起来不错但它会把大空闲区迅速消耗掉当真正需要大块连续内存的进程到来时反而没有足够的空间了。算法选择策略优点缺点首次适应第一个够用的速度快实现简单低地址碎片多最佳适应最小的够用的单次分配最省空间产生大量微小碎片最坏适应最大的剩余区较大可复用大块内存被快速耗尽4.3 碎片问题的本质为什么“总空间够”却“分配不出来”外部碎片是可变分区的核心难题。它的本质是空闲内存被已分配区域分割成了许多不连续的小块虽然总空闲量足够但没有一块连续的、足够大的区域能满足新请求。我朋友那个模拟器遇到的问题就是这个。他释放了中间一块内存后前后两块空闲区没有合并导致空闲区链里出现了三个相邻的小空闲块而新请求需要的大小比任何单独一块都大于是分配失败。解决办法其实很简单在回收分区时检查它是否与前后的空闲区相邻如果相邻就合并成一个大的空闲区。这个操作叫空闲区合并是可变分区管理必须实现的功能。合并的时机有两种一种是回收时立即合并另一种是分配时再合并。立即合并实现简单但每次回收都要检查前后邻居延迟合并可以减少回收时的开销但空闲区链会变长分配时扫描更慢。实际系统中两种都有用取决于负载特征。注意合并操作必须同时检查前邻和后邻只检查一边是新手最常犯的错误。我当初就只检查了后邻结果前邻的空闲区一直孤零零地挂着碎片问题依然存在。5. 动手写一个可变分区模拟器从数据结构到合并逻辑5.1 空闲分区链的设计与节点结构要真正理解可变分区光看概念不够得动手写。我用 Python 写过一个简化版模拟器核心数据结构是一条按地址排序的空闲分区双向链表。每个节点记录起始地址、大小以及前后指针。为什么按地址排序因为合并操作需要判断相邻关系按地址排序后只需要检查当前节点的前驱和后继是否与它地址连续即可。如果不排序每次合并都得遍历整条链去找邻居效率太低。节点结构大概是这样class FreeBlock: def __init__(self, start, size): self.start start # 起始地址 self.size size # 大小 self.prev None # 前驱指针 self.next None # 后继指针已分配区域我单独用一个字典记录键是进程 ID值是起始地址和大小方便回收时查找。5.2 分配逻辑的实现与边界处理分配逻辑我采用的是首次适应算法因为实现最简单也最接近真实系统的常见选择。核心步骤是遍历空闲链找到第一个 size 大于等于请求大小的节点然后切割。切割时有两种情况如果空闲块大小刚好等于请求大小直接整块分配从链表中摘除如果大于请求大小就从头部切出请求大小剩余部分留在原节点更新起始地址和大小。这里有个容易忽略的边界切割后剩余大小为 0 的情况。如果空闲块大小和请求大小完全相等剩余为 0这时候不能留下一个大小为 0 的节点必须把整个节点删掉。我当初就是没处理这个结果链表里出现了一堆 size 为 0 的僵尸节点遍历时死循环。def allocate(self, pid, size): cur self.head while cur: if cur.size size: if cur.size size: # 整块分配摘除节点 self._remove_node(cur) else: # 切割剩余部分留在原节点 cur.start size cur.size - size self.allocated[pid] (cur.start - size if cur.size 0 else cur.start, size) return True cur cur.next return False # 无合适空闲区上面这段代码里记录已分配地址时我用了cur.start - size因为切割后cur.start已经后移了。这个细节如果搞错回收时就会释放错误的地址范围导致内存状态混乱。5.3 回收与合并前邻后邻都要查回收逻辑是可变分区模拟器的灵魂。步骤是先从已分配字典里取出该进程的起始地址和大小然后把它作为一个新的空闲节点插入到空闲链的正确位置按地址排序最后检查它是否能与前驱和后继合并。合并前驱的条件是前驱的start size 当前节点.start。合并后当前节点的起始地址变成前驱的起始地址大小变成两者之和然后把前驱从链表中删掉。合并后继的条件是当前节点的start size 后继.start。合并后当前节点的大小加上后继的大小然后把后继删掉。def free(self, pid): start, size self.allocated.pop(pid) node FreeBlock(start, size) self._insert_sorted(node) # 合并前驱 if node.prev and node.prev.start node.prev.size node.start: node.prev.size node.size self._remove_node(node) node node.prev # 合并后继 if node.next and node.start node.size node.next.start: node.size node.next.size self._remove_node(node.next)这段代码我调试了很久问题出在合并前驱后node的指向变了必须把node更新为前驱节点否则后续合并后继时会用错地址。这种指针操作的坑不实际写一遍是很难体会到的。5.4 实测中的碎片演化与调参经验我用这个模拟器跑了一组随机请求观察碎片演化。结果很有意思前 50 次分配回收内存利用率还能维持在 80% 以上到 200 次左右空闲区数量从最初的 1 个涨到了 30 多个利用率掉到 60% 以下到 500 次碎片已经非常严重很多请求明明总空闲够却分配失败。这说明可变分区在长时间运行后碎片问题会累积到不可忽视的程度。缓解手段主要有两个一是紧凑技术把所有已分配区域挪到一起空闲区合并成一大块但这需要移动进程的内存内容开销很大而且需要硬件支持地址重定位二是限制进程大小或定期重启工程上不太优雅但有时很实用。我在模拟器里加了一个简单的紧凑功能把所有已分配块按地址排序后依次前移空闲区全部挤到尾部。跑下来碎片问题确实解决了但每次紧凑都要遍历所有已分配块并更新地址开销不小。这也解释了为什么真实系统更倾向于用分页而不是靠紧凑来解决问题。6. 分区管理的局限与它在今天的影子6.1 为什么纯分区管理退出了历史舞台分区管理最大的硬伤就是连续性约束。它要求每个进程占一整块连续内存这直接导致了外部碎片和分配效率的矛盾。固定分区浪费内部空间可变分区产生外部碎片紧凑又太贵。随着进程数量和内存需求增长这套方案越来越力不从心。分页机制的出现彻底改变了局面。它把内存和进程都切成固定大小的小块进程的块可以分散存放在不连续的内存块里通过页表做地址映射。这样一来外部碎片消失了内存利用率大幅提升。分段则在逻辑上更贴近程序结构但同样打破了连续性约束。分区管理作为“整块分配”的代表自然就被取代了。但我要强调的是被取代不等于没价值。分区管理提出的问题——如何跟踪空闲内存、如何选择分配区域、如何回收合并——在分页和分段里依然存在只是解决方式变了。理解分区管理是理解后续所有内存管理机制的前提。6.2 分区思想在现代系统中的残留虽然纯分区管理不用了但它的思想到处都有影子。比如很多嵌入式实时操作系统因为进程数量固定、大小可预测仍然采用类似固定分区的静态内存池。又比如一些内存分配器如某些实时分配器会预划分不同大小的内存池本质上就是不等分固定分区的现代版。再比如操作系统加载内核模块时往往需要一块连续的物理内存这时候分配器就得在物理内存里找一块足够大的连续区域这跟可变分区的首次适应算法思路是一样的。还有理解分区管理里的碎片概念对排查实际程序中的内存碎片问题非常有帮助——你可能遇到过服务跑久了内存占用越来越高、重启就好这背后往往就是碎片在作祟。6.3 学完分区管理后应该建立的几个认知第一内存管理没有银弹。固定分区简单但浪费可变分区灵活但碎片多分页高效但需要硬件支持。每种方案都是特定约束下的权衡理解权衡比记住结论更重要。第二碎片是内存管理的永恒敌人。内部碎片和外部碎片是两种不同性质的浪费解决一种往往会引入另一种。分页解决了外部碎片但产生了页内碎片分段解决了内部碎片但又有外部碎片。这个矛盾贯穿整个内存管理领域。第三数据结构和算法在系统里是实打实的。空闲链表的排序、合并时的指针操作、分配算法的选择这些不是纸上谈兵写错一行代码程序就崩。我建议每个学操作系统的人都亲手写一遍分区管理模拟器比看十遍书都管用。最后分享一个我自己的小习惯每次学完一个内存管理机制我都会画一张图标出内存被划分成什么单位、分配时怎么找、回收时怎么合、碎片在哪里产生。画完这张图这个机制基本就吃透了。分区管理的图很简单就是一条线被切成若干段但正是这张简单的图撑起了后面所有复杂机制的底层逻辑。

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

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

免费获取报价 →
↑