Skip to content

内存碎片化:为什么空闲够却拿不出一大块

假设你手上有一整块连续的内存,反复地分一块出去、用完再收回来。分着分着,你会发现一件怪事:明明空闲的总量还够,真去申请一块稍大的连续空间,却怎么也拿不出来。这种"看着有、用不上"的窘境,就是咱们这一篇要讲的东西——内存碎片化。它是后面伙伴分配器、slab 这些设计要对付的头号麻烦,不把它讲透,后面那些分配器"为什么非设计成那样"就全是无源之水。

先垫一块底:内存是怎么被分出去的

聊碎片之前,咱们得先把"分配内存"这件事说清楚,不然碎片无从谈起。

内核手里管的是一大片连续的物理内存。它不是"你要几个字节就给你几个字节"那样零敲碎打地给——那样记账成本太高,每字节都得记归谁。实际做法是把内存切成一个个固定大小的(咱们这套系统里一页 4 KiB),分配的最小颗粒就是页:你要内存,内核给你整数个页;你用完还回来,内核把这些页重新标记成空闲。

这就有了"一个页的池子":一部分页正被用着(已分配),一部分页空着(空闲,随时能给出去)。分配是从空闲里挑出来标成已用,释放是把已用的标回空闲。听起来很简单,麻烦就藏在这个"挑出来"和"标回去"里。

为什么"连续"这件事很重要

你可能要问:既然手里有 100 个空闲页,你要 8 个连续页,我从 100 里随便挑 8 个给你不就行了,凭什么非要"连续"?

原因是很多场景拿这页内存,要的不是一个数字上的"8 个页",而是"8 个页拼成的一整块连续地址"。比如设备做 DMA 传输,认的是物理上连续的一块缓冲,它不会帮你把散在各处的页自己拼起来;内核自己要开一个大数组、一张页表,这些数据结构本身就得连续存放;一段连续的虚拟地址想映射到连续的物理页,不然就得逐页单独建映射,开销大。所以"给 8 个页"和"给 8 个连续页"是两回事,后者苛刻得多——它要求这 8 个页在物理上挨在一起。

碎片化:空闲够,但凑不出连续的大块

有了上面两块垫底,碎片化就好讲了。

外部碎片,就是开头说的那种窘境:池子里空闲页的总数够,但这些空闲页散在各处,中间夹着正在被用的页,导致你挑不出一块连续的、够大的空闲区。打个比方:一个停车场本来能停一辆大卡车(连续的大块),后来被划成小车位一辆辆租出去,租客还车时还回来的小车位散落在停车场各个角落;现在虽然空车位的总数还够停一辆大卡车,但没有一处是连续的、容得下大卡车的——大卡车就停不进来了。空闲总量没少,但"连续性"被打碎了。

内部碎片是另一副面孔:分配器手里只有几种固定大小的块(比如只有 8 页、16 页这种规格),你要 5 页,它给不了你精确的 5 页,只能给 8 页(向上凑到下一档),多出来的 3 页你用不上、也归你管不着,白白浪费在"给的比要的大"里头。这种浪费发生在每个分配出去的块内部,所以叫内部碎片。

这一篇重点关心的是外部碎片——它是"看着有、用不上"的那种,也是后面伙伴分配器要正面治的。

碎片是怎么一点一点攒出来的

知道了外部碎片是什么,下一个问题是:它怎么产生的?把病因看清,才看得出后面的药(伙伴分配器)对症在哪。

想象一个干净的池子,全是连续的空闲页。现在开始反复分配、释放,而且每次要的页数不一样(一会儿要 2 页、一会儿要 7 页、一会儿要 1 页):

先分出去好几块大小不一的页,它们占在池子各处,把原来的连续大块切割成一段一段;用完之后这些块被释放、标回空闲,可它们不是按分配的顺序释放的,有的早分配却晚还、有的反过来;于是释放回来的空闲块,就夹在还正在用的块之间,东一块西一块。本来两块相邻的空闲可以拼回一大块,但没人去拼——分配器释放时只是把这块标成空闲就收工,并不主动看一眼"我邻居是不是也空着、能不能合回去"。

这最后一点是关键:一个"分了就分了、释放了标记完事"的分配器,天然会攒碎片,因为每一次释放都是一个"本可以合并却没合并"的机会。用得越久、分配释放越杂,碎片就越细越散,直到某天你申请一块中等大小的连续区,发现池子里明明空着 200 页,却凑不出你要的那 20 个连续页。

这一篇留下了什么

把碎片化讲透,是为了给后面的分配器铺一个"为什么要那样设计"的地基。一个分配器好不好,很大程度就看它治理外部碎片治得怎样——是放任碎片攒下去,还是在分配尤其是释放的时候,主动把能拼回去的相邻空闲块拼回去。

具体怎么"主动拼回去"、靠什么机制又快又准地找到该拼的邻居,就是下一篇《伙伴分配器》要讲的:它有一套特别巧的设计,能让释放时的合并几乎是自动的、结构性的,根本不用满池子去搜邻居——咱们下一篇见。

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04