深蓝黑背景封面。左侧上下两块面板对比两种内存碎片。上面板标题「内部碎片:分出去的块用不满」:一条长条代表一个 8 字节分配块,左段青色填充标注「实际数据 5 字节」,右段暗灰填充标注「闲置 3 字节」,下方小字「程序申请 8 字节、只写 5 字节,剩下的浪费在块内部」。下面板标题「外部碎片:空闲总量够,凑不出连续大块」:一条内存条带按「已用 140 / 空 50 / 已用 160 / 空 40 / 已用 90 / 空 60」交替排列,青色为已用、暗灰为空闲,下方小字「空闲 50 + 40 + 60 = 150,但申请 100 连续空间仍失败,碎片散在块与块之间」。右侧大标题「内存的内部碎片」「和外部碎片是什么」分两行,副标题「一个浪费在块内,一个浪费在块间」,小注「分页、伙伴系统、slab,各治一种碎片」。左上标注「面试经典 · 操作系统」,右上圆角胶囊「Day 07 · 12-04」。 图 1:本文封面,两种碎片的位置对比——一个在块内,一个在块间。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「讲讲内部碎片、外部碎片,如何解决」是操作系统面试的常驻题,真正考的不是两个名词背诵,而是你能不能说清各自的成因,以及分页、伙伴系统、slab 这些机制分别在对付哪一种。

一句话结论:内部碎片是分出去的块里用不满的部分,外部碎片是块与块之间凑不出连续大块的空洞;前者看块内利用率,后者看最大连续空闲。

内部碎片:分配粒度不小于需求。

只要分配器按固定粒度给空间,粒度减需求的部分就浪费在块内部。典型来源三个:分页按整页分配,进程最后一页用不满;slab 这类定长对象缓存,对象比槽位小;还有内存对齐的填充字节。教科书上的经典结论是:分页系统中每个进程平均浪费半页(最后一页),页越小内部碎片越少,但页表越大——这是个权衡。

外部碎片:变长分配与释放交错,把空闲区打散。

空闲内存的总量足够,但被已用块隔成一个个小洞,没有任何一个洞装得下新的连续分配。它只在「需要连续物理空间」的分配方式里存在——分段、变长分区、堆上按字节分配都是高发区。

两种碎片不是嘴上说说的,用一个模拟器直接演示(Clang 17,g++ -std=c++17 frag.cpp && ./a.out)。外部碎片部分用首次适配算法管理一段 540 单元的条带:先分配 6 块占满,再释放第 2、4、6 块制造三个互不相邻的空洞;内部碎片部分按 4 KB 整页给请求分配,统计页内闲置:

// 内存碎片演示:外部碎片用首次适配模拟器,内部碎片用分页统计
#include <cstdio>
#include <vector>
using namespace std;
struct Block { int start, size; bool used; };
vector<Block> mem = {{0, 540, false}};
int firstFit(int size) { // 首次适配:找第一个装得下的空闲块
    for (size_t i = 0; i < mem.size(); i++) {
        if (!mem[i].used && mem[i].size >= size) {
            Block b = mem[i];
            mem[i] = {b.start, size, true};
            if (b.size > size) mem.insert(mem.begin() + i + 1, {b.start + size, b.size - size, false});
            return b.start;
        }
    }
    return -1; // 没有任何连续空闲块装得下
}
void freeAt(int start) {
    for (auto &b : mem) if (b.start == start) b.used = false;
    for (size_t i = 0; i + 1 < mem.size(); ) { // 合并相邻空闲块
        if (!mem[i].used && !mem[i+1].used) {
            mem[i].size += mem[i+1].size;
            mem.erase(mem.begin() + i + 1);
        } else i++;
    }
}
int main() {
    int a = firstFit(140), b = firstFit(50), c = firstFit(160);
    int d = firstFit(40),  e = firstFit(90), f = firstFit(60); // 六块把 540 单元条带占满
    printf("[外部碎片] 六块分配起点: %d %d %d %d %d %d(条带占满)\n", a, b, c, d, e, f);
    freeAt(b); freeAt(d); freeAt(f); // 释放第 2、4、6 块:三个互不相邻的空洞
    int freeTotal = 0, largest = 0;
    for (auto &blk : mem) if (!blk.used) { freeTotal += blk.size; largest = max(largest, blk.size); }
    printf("[外部碎片] 空闲总量 %d 单元,最大连续空闲 %d 单元\n", freeTotal, largest);
    printf("[外部碎片] 申请 100 连续单元: %s\n",
           firstFit(100) >= 0 ? "成功" : "失败(总量够,但凑不出连续大块)");
    // 内部碎片:请求按 4 KB 整页向上取整,累加 页数*4096 - 请求大小
    const int PAGE = 4096;
    vector<int> reqs = {3000, 5000, 8200, 100, 4096, 7000};
    long waste = 0, alloc = 0;
    for (int r : reqs) {
        int pages = (r + PAGE - 1) / PAGE;
        waste += pages * PAGE - r; alloc += pages * PAGE;
    }
    printf("[内部碎片] 6 个请求按整页分配,共占 %ld 字节,页内闲置 %ld 字节(%.1f%%)\n",
           alloc, waste, 100.0 * waste / alloc);
    long wsum = 0; unsigned seed = 42; // 10 万次随机请求,验证「末页平均浪费约半页」
    for (int i = 0; i < 100000; i++) {
        seed = seed * 1103515245 + 12345;
        wsum += (PAGE - (int)(seed % PAGE)) % PAGE;
    }
    printf("[内部碎片] 10 万次随机请求末页平均闲置 %.0f 字节(页大小 %d,约半页)\n",
           (double)wsum / 100000, PAGE);
}

实测输出:

[外部碎片] 六块分配起点: 0 140 190 350 390 480(条带占满)
[外部碎片] 空闲总量 150 单元,最大连续空闲 60 单元
[外部碎片] 申请 100 连续单元: 失败(总量够,但凑不出连续大块)
[内部碎片] 6 个请求按整页分配,共占 40960 字节,页内闲置 13564 字节(33.1%)
[内部碎片] 10 万次随机请求末页平均闲置 2048 字节(页大小 4096,约半页)

外部碎片的反直觉处看得很清楚:空闲 150、只申请 100,照样失败,因为三个洞 50、40、60 互不相邻。内部碎片部分,10 万次随机请求的末页平均闲置落在 2048 字节——恰好半页,教科书结论被直接复现。

机制对照:每个方案治一种碎片。

分页治外部碎片。虚拟地址离散映射到物理页框,进程看到的连续地址不再需要连续的物理内存,外部碎片直接消灭;代价是把碎片挪进每个进程的最后一页,从外部变成内部。伙伴系统治外部碎片。空闲块按 2 的 k 次幂管理,分配时分裂、释放时找「伙伴」合并,小洞有机会重新拼成大块,Linux 内核的页级分配就用它。slab 治内部碎片。内核里频繁分配释放的对象(如进程描述符)大小固定,为同型对象建定长缓存,对象恰好填满槽位,块内浪费趋近于零,还顺带省了重复初始化的开销。紧凑拼接(compaction)也能治外部碎片——搬运已用块把洞合并,但要能移动数据且代价高,一般作为兜底手段。分段和分页的对照是经典考点:分段是变长分配所以有外部碎片,分页是定长分配所以只剩内部碎片。

深蓝黑背景概念图。顶部一行小字「概念图 · 内存碎片」,主标题「内部碎片是『分大了』,外部碎片是『凑不齐』」。左上大面板讲内部碎片:一条 4 KB 页框条带,前 3 KB 青色标注「数据 3 KB」,后 1 KB 暗灰标注「页内闲置 1 KB」;成因是分配粒度不小于需求(页帧、slab、对齐填充),典型后果是分页最后一页平均浪费半页。右上大面板讲外部碎片:一条「已用 / 空 / 已用 / 空 / 已用 / 空」交替的内存条带,成因是变长分配与释放交错把空闲区打散,典型后果是空闲总量充足却分配失败。中部一排四张机制对照卡:「分页 → 治外部:离散页框映射,无需连续物理内存」「伙伴系统 → 治外部:2 的 k 次幂块分裂与合并」「slab → 治内部:同型对象定长缓存」「紧凑拼接 → 治外部:搬运已用块合并空洞,代价高」。底部总结条:内部碎片看「块内利用率」,外部碎片看「最大连续空闲」,先判断哪种再选机制;最底一行小字补充分页用离散映射消灭外部碎片,代价是把碎片挪进最后一页。 图 2:两种碎片的成因对照与四种机制的治理分工。

判据与答题结构。

判断手头的问题是哪种碎片,看两个指标:块内利用率低是内部碎片,最大连续空闲不足是外部碎片。面试答题按「两个定义加成因 → 一个演示或例子 → 四个机制各治哪种 → 分段分页对照收尾」推进,答到「分页消灭外部碎片的代价是把碎片挪进最后一页」这层,就说明你理解的是设计权衡而不是名词。你在实际项目里遇到过碎片问题吗?是堆分配器的,还是页缓存层面的?欢迎聊聊。