想象一座运营多年的露天停车场:车进车出,车位渐渐变得零散——东空三格、西空两格,总数虽不少,可当一支 12 辆的大巴团队想停进来时,却怎么也找不到连续的空位。车位就是内存的“页框”,车辆就是“页面数据”,大巴则是操作系统里那些需要大块连续内存的“高阶分配请求”。为了治好这种“空位虽多、却不顶用”的毛病,停车场管理员请来了挪车大师:把能动的车集中挪到一侧,腾出大片连续空位,这套功夫就是“内存规整”(compaction)。挪车时要分清哪些车能挪、哪些“承重墙”碰不得,还要两位工人从两头相向扫描,用一张“碎片指数”仪表盘判断何时动手。真到车位全满时,挪车大师还得和“拖车大队”(kswapd)搭档。这篇文章就用停车场的日常,带你完整看懂操作系统内存碎片整理的门道。

岗位关联

对 AI 从业者而言,掌握内存碎片整理意味着你能理解大模型加载为何失败、推理服务为何越跑越慢,并学会通过配置内核参数和监控碎片指数,让模型服务长期稳定运行。

目录
  1. 可视化演示
  2. 碎片类型与成因
  3. 伙伴系统与分配瓶颈
  4. 内存规整核心机制
  5. 双扫描器协作
  6. 碎片指数评估
  7. 规整触发时机
  8. 与 kswapd 协同
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示内存碎片化可视化、Compaction迁移扫描器协同工作过程、碎片指数仪表与compaction/kswapd双路径协同策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

内存碎片类型可视化 Free:空闲页框分散分布 Used:已分配不可迁移页框 Movable:可迁移页面 external_frag = 1 - (max_free_order / total_free) 外部碎片导致高阶(order>0)连续分配失败,伙伴系统无法满足。

碎片类型与成因

观察一座运营三年的露天停车场,你会发现两种让人头疼的浪费。第一种:一辆小轿车大摇大摆地占着两个标准车位,因为管理员只按“标准位”出租,车子只好多占一个——这叫内部浪费。第二种:车位东空一格、西空三格,加起来明明有三十个空位,一支大巴团却因为找不到 12 个连续车位只能走人——这叫外部浪费。这两种浪费,正是内存世界里“内部碎片”与“外部碎片”的翻版。

操作系统把物理内存切成固定大小的页框(通常 4KB),按页分配给进程。进程申请 13KB 内存时,系统分配 4 个页框(16KB),多出的 3KB 再也无法使用,这就是内部碎片。而进程反复加载、退出、扩容后,空闲页框像停车场的空位一样被“稀释”到各处,这就是外部碎片。外部碎片的本质是:空闲内存总量充足,但最大的连续空闲块太小,无法满足高阶分配。

我们可以用一个公式量化碎片化程度:

想象停车场明明还有 30 个空位,最长一排却只有 3 个连位,大巴队就是进不来。内部碎片是“车位太宽、车太瘦”的浪费,外部碎片则是散沙般的空位——它们共同构成了内存长期运行的“慢性病”,而治病的良方,就是后面登场的挪车大师。

Rfrag=1−LmaxFtotal\displaystyle R_{frag} = 1 - \frac{L_{max}}{F_{total}}
这个公式告诉我们:当最大连续空闲块占空闲总量的比例越小,碎片化程度就越严重。
  1. 统计总空闲页数 F_total遍历所有页框,统计处于空闲状态的页框总数,相当于停车场里所有空位的数量。
    Ftotal=∑i=1n[pagei=free]F_{total} = \sum_{i=1}^{n} [page_i = \text{free}]
  2. 求最大连续空闲块 L_max扫描页框序列,找出最长连续空闲段的长度,相当于停车场里最长的一排连续空位。
    Lmax=max⁡{run1,run2,…,runk}L_{max} = \max\{ \text{run}_1, \text{run}_2, \dots, \text{run}_k \}
  3. 计算碎片化程度用 1 减去连续系数。比值越接近 1,说明空位越零散;越接近 0,说明空位越集中。
    Rfrag=1−LmaxFtotalR_{frag} = 1 - \frac{L_{max}}{F_{total}}

伙伴系统与分配瓶颈

停车场管理员老练地定下一套铁规矩:所有区域只按 1、2、4、8、16 这样的“翻倍”方式划分。你要 3 个车位?给你 4 个;要 6 个?给你 8 个。这套翻倍分配法,就是操作系统里的“伙伴系统”(Buddy System)。规则简单高效,也让管理员的账本一目了然。

Linux 内核的伙伴系统把空闲页框按 order(阶数)挂进不同链表:order 0 是 1 个页框,order n 是 \(2^n\) 个连续页框的块。分配 \(2^n\) 页时,若对应链表为空,就向更高阶拆一半下来;释放时,若相邻的“伙伴”块也空闲,则合并回更高阶。这套机制保证了分配速度,却埋下了隐患:长期运行后,大量小块被分配又释放,大块被拆散很难再合并回来,高阶链表逐渐枯竭。

伙伴系统的翻倍划线法简单高效,却挡不住时间的侵蚀。当一个 8 辆连排的大巴位被反复拆成单人位后,管理员再想划一个 16 位大区就难了。好在,他还有第二件法宝——挪车。

S(n)=2n×PAGE_SIZE\displaystyle S(n) = 2^n \times PAGE\_SIZE
伙伴系统的一个块大小等于 2 的阶数次幂乘以页大小,阶数越高,能服务的“大巴”就越大,但也越稀缺。
  1. 阶数 n 的范围伙伴系统用阶数表示块大小级别,Linux 中 order 从 0 到 MAX_ORDER-1 共 11 级。
    n∈{0,1,2,…,10}n \in \{0, 1, 2, \dots, 10\}
  2. 页大小 PAGE_SIZE系统一页通常 4KB,是内存分配的最小粒度,相当于一个标准车位。
    PAGE_SIZE=4 KBPAGE\_SIZE = 4\ \text{KB}
  3. 块大小 S(n)order n 的空闲块包含 \(2^n\) 个连续页框,这是伙伴系统分配与合并的基础。
    S(n)=2n×4 KBS(n) = 2^n \times 4\ \text{KB}

内存规整核心机制

大巴队又要来了,管理员决定启动“挪车方案”:把散落在角落的小轿车一辆辆挪到指定区域集中停放,腾出另一端大片连续空位。挪车前先给每辆车贴标签:哪些车能挪(私家车),哪些是不能碰的配电房围挡。这套贴标签、集中挪车的流程,就是 Linux 的内存规整(compaction)机制。

内核将物理内存按 pageblock(通常 128 个页框)划分迁移类型:MIGRATE_MOVABLE 用于用户态可移动页(匿名页、文件缓存),MIGRATE_UNMOVABLE 用于内核代码、页表等不可移动页,MIGRATE_RECLAIMABLE 用于可回收但不宜移动的内核页。执行规整时,内核选好迁移源(可移动页所在的 pageblock)和迁移目标(空闲页所在的 pageblock),逐个把源页内容拷贝到目标页,并改写页表映射和反向映射。页面迁移不只是复制数据,还要更新所有引用它的页表项,成本不低。

挪车方案的精髓在于“分清主次”:焊死的围挡绝不动,能动的车全归拢。迁移成本摆在那里,所以管理员从不轻易出手,而是等碎片指数报警,或者大巴真到门口时才动手。

Emigrate=Nfree_blockNmigrated\displaystyle E_{migrate} = \frac{N_{free\_block}}{N_{migrated}}
迁移效率等于腾出的连续块大小除以迁移页面数量,比值越高,这次挪车越划算。
  1. 被迁移页数 N_migrated迁移过程中挪动了多少个可移动页面,这是挪车付出的“人力成本”。
    Nmigrated=被移动的页框数N_{migrated} = \text{被移动的页框数}
  2. 腾出连续块 N_free_block整理后能形成多大的连续空闲块,这是挪车的“收益”。
    Nfree_block=规整后的最大连续空闲块N_{free\_block} = \text{规整后的最大连续空闲块}
  3. 计算迁移效率收益除以成本。比值越大,说明这次挪车越值;若效率太低,宁可不挪。
    Emigrate=Nfree_blockNmigratedE_{migrate} = \frac{N_{free\_block}}{N_{migrated}}

双扫描器协作

挪车可不是乱搬一气。两位工人从停车场两端出发:A 师傅从东门往西走,专门找贴了“可挪”标签的小轿车;B 师傅从西门往东走,专门找空车位。二人边走边配合——A 找到一辆可挪的车,就把它塞进 B 刚发现的空位。两人越走越近,直到中间碰头,整理结束。

Linux 的 compaction 正是如此。它有两个扫描器:正向扫描器(migration scanner)从区域头部向尾部推进,寻找可迁移页面;反向扫描器(free scanner)从区域尾部向头部推进,寻找空闲页框。每找到一对“源页 + 空闲页”,就把源页迁到空闲位置。两个扫描器各自维护 pfn(页框号)索引:cc->migrate_pfn 和 cc->free_pfn。每轮扫描后把 pfn 缓存在 zone 的 compact_cached_migrate_pfn 里,下次直接从这里继续,避免重复扫描。

两位师傅碰头的那条线,就是停车场新的“秩序线”——一侧是齐整的车阵,另一侧是干净的空地。系统每次扫描都从缓存的 pfn 继续,绝不倒车,把挪车效率拉满。

Pmigrate+Pfree=Lzone\displaystyle P_{migrate} + P_{free} = L_{zone}
当正向扫描器位置与反向扫描器位置相遇时,所有可移动页都已归拢到一侧,整理自动停止。
  1. 正向扫描器位置 P_migrate迁移扫描器沿 pfn 增大的方向推进,指向下一个待检查页框。
    Pmigrate=migrate_pfnP_{migrate} = \mathrm{migrate\_pfn}
  2. 反向扫描器位置 P_free空闲扫描器沿 pfn 减小的方向推进,指向下一个待检查页框。
    Pfree=free_pfnP_{free} = \mathrm{free\_pfn}
  3. 相遇停止条件当正向位置大于等于反向位置时,两端扫描区域重叠,说明中间已无“可移页 + 空闲页”配对了。
    migrate_pfn≥free_pfn⇒停止扫描\mathrm{migrate\_pfn} \geq \mathrm{free\_pfn} \Rightarrow \text{停止扫描}

碎片指数评估

管理员每天上班第一件事,就是看墙上的“停车焦虑指数”仪表盘:指数 0 代表所有空位连成一片,岁月静好;指数 1 代表空位数虽多,但全是碎渣空位,大巴来了只能干瞪眼。这个仪表盘,就是碎片指数(fragmentation index)。

碎片指数衡量的是当前空闲内存中,有多大比例真正能满足分配请求。它由内核在分配路径上动态计算,用来判断此刻执行 compaction 是否值得。计算思路很简单:统计总空闲页数,再统计其中“可组成目标阶数连续块”的页数。若大部分空闲页分散在无法组成大块的位置,指数就接近 1。

碎片指数为系统提供了“动手的底气”:指数高到一定程度,管理员才舍得派挪车小队;指数低时,随便来辆车都能找到位,何必折腾?指数就是一把尺子,量出碎片病到了几分。

Ifrag=1−PusablePfree\displaystyle I_{frag} = 1 - \frac{P_{usable}}{P_{free}}
碎片指数是系统决定“要不要挪车”的温度计:接近 1 表示碎片严重,接近 0 表示内存健康。
  1. 总空闲页数 P_free区域内所有空闲页框的总数,相当于停车场空位总数。
    Pfree=total free pagesP_{free} = \text{total free pages}
  2. 可用空闲页数 P_usable这些空闲页中有多少能被当前目标阶数实际使用,相当于能组成连排空位的数量。
    Pusable=free pages usable for target orderP_{usable} = \text{free pages usable for target order}
  3. 指数判定指数接近 1,说明绝大多数空位是分散的,碎片严重;接近 0,说明空位集中,直接分配即可满足。
    Ifrag→1:严重碎片;Ifrag→0:内存健康I_{frag} \to 1: \text{严重碎片};\quad I_{frag} \to 0: \text{内存健康}

规整触发时机

挪车小队不能天天上街——每次折腾都有成本。那什么时候动手?管理员定下两条规矩:一是大巴已经堵在门口,死活找不到连位,只能紧急挪车;二是停车场快满员了,提前派拖车队清理僵尸车,顺手把车位理一遍,防患于未然。

内核同样有两条触发路径。直接规整(direct compaction):分配进程在 alloc_pages 里寻求高阶连续块失败后,同步执行 compaction,进程阻塞等待整理结果。异步规整:kswapd 发现内存水位紧张时,唤醒后台线程 kcompactd 执行规整,进程不阻塞。直接规整发生在分配请求带 GFP_COMPACT 且 order 大于 0 时,快速路径找块失败后进入慢速路径,调用 __alloc_pages_direct_compact 救火。

紧急挪车和提前整理,一个治标一个治本。内核的分寸感在于:平时让 kcompactd 在后台静静巡检,危机时刻直接规整当场救火,分工明确,绝不浪费。

orderrequest>0 ∧ free_block=∅⇒direct compaction\displaystyle order_{request} > 0 \ \wedge \ free\_block = \varnothing \Rightarrow \text{direct compaction}
只有“高阶请求 + 无可用连续块”两个条件同时满足,进程才会在分配路径上同步触发内存规整。
  1. 高阶请求 order > 0只有需要 2 页及以上连续页框的分配,才值得做规整,单页分配不用凑热闹。
    orderrequest∈{1,2,3,… }order_{request} \in \{1, 2, 3, \dots\}
  2. 无空闲块伙伴系统当前找不到满足请求阶数的空闲块,相当于停车场找不到连排车位。
    free_block=∅free\_block = \varnothing
  3. 触发直接规整两个条件同时满足,分配进程进入同步整理流程,整理完成后重新尝试分配。
    direct compaction started\text{direct compaction started}

与 kswapd 协同

如果停车场已经满到连站的地方都没有,光靠挪车就回天乏术了——这时候得请“拖车大队”出场。拖车大队(kswapd)专门把停了好几个月没动的僵尸车拖走,腾出空位;挪车小队(kcompactd)则负责把剩下的车排整齐。两人一前一后,停车场才真正起死回生。

kswapd 是 Linux 内核的页面回收守护线程,当内存水位低于 low 阈值时被唤醒,扫描 LRU 列表回收不活跃页。compaction 与回收有微妙的分工:回收产生的是“可用内存的数量”,而规整解决的是“连续内存的质量”。数量不足时靠回收,连续性不足时靠规整。

在内核的慢速分配路径中,两者协同作战:kswapd 负责把内存压到 low 水位以上,kcompactd 负责把剩余空闲页做成大块。特别地,当无法迁移足够的可移动页面时,内核还会尝试回收不可移动页面之间的可回收页,把“硬骨头”也啃下来。

拖车队和挪车队从不内讧:回收负责“腾数量”,规整负责“造质量”。当停车场终于迎来一辆大巴时,正是这两位师傅的默契配合,才让老旧满员的停车场依然容得下大块头。

free_pages<low_watermark⇒wakeup kswapd and kcompactd\displaystyle free\_pages < low\_watermark \Rightarrow \text{wakeup } kswapd \text{ and } kcompactd
空闲内存跌破低水位时,回收与规整会同时被唤醒:一个负责腾数量,一个负责造质量。
  1. 低水位阈值每个内存 zone 维护 low/high 水位线,空闲页低于 low 意味着内存随时可能耗尽。
    low_watermark=zone_low_wm\text{low\_watermark} = \mathrm{zone\_low\_wm}
  2. 唤醒 kswapd回收线程被唤醒,开始扫描 LRU 列表,把冷页面写出到磁盘或直接丢弃。
    kswapd wakes and reclaims pages\text{kswapd wakes and reclaims pages}
  3. 同步唤醒 kcompactd回收的同时启动后台规整,把释放出的页框合并成大块连续区。
    kcompactd compacts concurrently\text{kcompactd compacts concurrently}

AI 应用场景

云上 AI 推理平台的模型加载优化

阶段:模型部署与服务(推理服务生命周期管理)

解决的问题:大模型(如 Llama 3 70B)需要 GB 级大页连续物理内存。业务 Pod 频繁创建销毁会导致物理内存碎片化,模型加载时 HugeTLB 分配失败、启动变慢甚至 OOM。内存规整能迁移可移动页,为模型腾出连续大块内存,就像挪车大师为大巴清出连排车位。

具体案例:某 MLOps 平台在 GPU 服务器上运行 24 小时推理服务,每 30 分钟滚动发布一个新版本模型,版本替换反复申请和释放近 10GB 内存。几周后模型加载时间从 2 分钟劣化到 15 分钟,并频繁报"cannot allocate hugepage"。运维人员通过监控碎片指数并配置内核在业务低峰期主动触发 compaction 后,加载时间恢复稳定,错误清零。

大数据框架的 Executor 长时稳定运行

阶段:数据预处理与特征工程(分布式计算框架的堆外内存管理)

解决的问题:Spark/Flink 的 executor 堆外内存频繁分配大块 Direct Buffer,JVM 与操作系统之间的物理内存碎片会导致 Direct Memory 分配失败。理解碎片整理机制,可帮助正确设置 JVM 参数和内核 vm.compact_memory 策略。

具体案例:某数据团队在 200GB 内存节点上运行 Spark 批量特征工程,频繁出现 "Direct buffer memory" 异常。排查发现系统伙伴系统 order-3 以上分配成功率持续走低,碎片指数长驻 0.8。通过修改内核参数、在业务低峰期主动触发内存规整,任务成功率恢复稳定,Batch 任务耗时下降约 18%。

Python 演示

python64 行
"""模拟内存碎片化与规整(compaction)过程"""import random class Memory:    def __init__(self, size=64, page_size=4):        self.size = size  # 总页框数        self.pages = [None] * size  # None=空闲, 'mov'=可移动, 'fix'=不可移动        self.page_size = page_size     def alloc(self, cnt, movable=True):        """尝试分配 cnt 个连续页框, 成功返回起始索引"""        typ = 'mov' if movable else 'fix'        for start in range(self.size - cnt + 1):            if all(p is None for p in self.pages[start:start + cnt]):                for i in range(start, start + cnt):                    self.pages[i] = typ                return start        return -1     def free(self, start, cnt):        for i in range(start, start + cnt):            self.pages[i] = None     def max_free_run(self):        best = run = 0        for p in self.pages:            run = run + 1 if p is None else 0            best = max(best, run)        return best     def frag_index(self, need):        """碎片指数: 1 - 能满足 need 的空闲比例"""        free_total = sum(1 for p in self.pages if p is None)        if free_total == 0:            return 0.0        usable = self.max_free_run() >= need        return 1.0 - (need / free_total if usable else 0.0)     def compact(self):        """规整: 将所有可移动页往前搬, 腾出尾部连续区"""        write = 0        for read in range(self.size):            if self.pages[read] == 'mov':                if write != read:                    self.pages[write] = self.pages[read]                    self.pages[read] = None                write += 1 def simulate():    mem = Memory(size=64)    random.seed(42)    for _ in range(30):        cnt = random.randint(1, 8)        start = mem.alloc(cnt)        if start != -1 and random.random() < 0.5:            mem.free(start, cnt)    print("碎片化后: 最大连续块 =", mem.max_free_run())    print("碎片指数 (need=16) =", round(mem.frag_index(16), 2))    mem.compact()    print("规整后: 最大连续块 =", mem.max_free_run())    print("碎片指数 (need=16) =", round(mem.frag_index(16), 2)) if __name__ == "__main__":    simulate()
目的:用一段 60 多行的模拟器演示“随机分配释放导致碎片化,再通过 compact 规整恢复连续空间”的完整过程。
  1. self.pages = [None] * size用数组模拟物理内存页框,None 表示空闲车位,'mov' 和 'fix' 对应可移动与不可移动页面。
  2. if all(p is None for p in self.pages[start:start + cnt])检查从 start 开始是否有 cnt 个连续空闲页框,模拟伙伴系统的连续分配约束。
  3. return 1.0 - (need / free_total if usable else 0.0)计算碎片指数:最大连续块能满足 need 时按比例折算,否则视为完全碎片化。
  4. if self.pages[read] == 'mov': ... write += 1双指针原地规整:把所有可移动页搬到数组前部,腾出尾部连续区,等价于 compaction 的页面迁移。
  5. random.seed(42)固定随机种子,保证每次运行结果可复现,方便对比规整前后指标。

常见误区

误区进程看到的是虚拟地址连续的空间,物理内存有没有碎片无所谓吧?反正应用层根本感知不到物理地址。

正解虚拟地址连续确实给进程提供了“连续假象”,但物理页框不连续时,单页分配没问题,一旦遇到大页(HugeTLB)、DMA 缓冲、设备驱动等需要物理连续内存的场景就会分配失败。好比外卖地址写得再漂亮,车还是得停在真实、连续的物理车位上。

为何常见:现代 CPU 的 MMU 把物理地址隐藏得太好,应用开发者几乎感受不到物理内存布局,容易误以为碎片无足轻重。

误区快没内存了就回收一下、整理一下,不都是一回事吗?反正都是让系统有更多可用内存。

正解回收(kswapd)是把不再使用的页面释放掉,减少已用空间;而规整(compaction)是移动页面位置,不减少已用空间,只改变空闲空间的布局。前者多“腾数量”,后者多“造质量”。移植到停车场语境:回收是拖走僵尸车,规整是给剩下的车重新排位。

为何常见:两者常在同一条分配慢路径中先后执行,都能缓解内存压力,表面上难以区分,实则目标不同。

误区碎片指数都 0.9 了,说明碎片很严重,赶紧把整块内存都规整一遍!

正解碎片指数只衡量碎片严重程度,不代表规整收益。若可移动页极少,或者空闲页集中在低阶块,迁移成本可能远大于收益。内核会综合迁移成本、pageblock 迁移类型和空闲块分布,再决定是否值得整理。指数是温度计,不是处方——温度高不等于一定要做手术。

为何常见:把“度量指标”和“决策指标”混为一谈,是新手常犯的直觉错误。

误区整理内存嘛,把页面全部挪到一边排整齐不就好了?数据搬过去再改一下地址就行。

正解内核代码段、页表、中断描述符、某些驱动分配的 DMA 缓冲区都不可移动。这些页面一旦移动,物理地址变化会直接导致内核崩溃。所以规整只迁移 MIGRATE_MOVABLE 页面,还要小心处理页表映射与反向映射。好比挪车时配电房是绝对碰不得的——那是整座停车场的命脉。

为何常见:从应用层看所有内存都一样,但内核中有大量与物理地址强绑定的页面,普通开发者很难接触到这一层细节。

面试题

Q1. 关于外部碎片,下列说法正确的是?

单选题 · easy

  • A.是分配粒度向上取整造成的空间浪费
  • B.是空闲页框分散导致无法分配连续大块← 正确答案
  • C.只发生在虚拟内存中
  • D.可以通过增大页框尺寸彻底消除

答案:B。外部碎片是空闲内存总量充足但分散成小段,无法满足连续大块分配。A 描述的是内部碎片,C 错在外部碎片发生在物理内存,D 错在增大页框只会加剧内部碎片、无法消除外部碎片。

Q2. 伙伴系统按什么粒度管理空闲页框?

单选题 · easy

  • A.按字节管理
  • B.按2的幂次页框块← 正确答案
  • C.按固定128KB块
  • D.按段管理

答案:B。伙伴系统把空闲页框按 order 分组,order n 对应 \(2^n\) 个连续页框。A 是分配器层面的粒度,C 不对应伙伴系统约定,D 是分段机制的做法。

Q3. 内存规整(compaction)的核心动作是什么?

单选题 · mid

  • A.将不活跃页面换出到磁盘
  • B.将可移动页面迁移到另一侧腾出连续空闲区← 正确答案
  • C.合并两个相邻的空闲块
  • D.把进程虚拟地址映射到新页

答案:B。compaction 是把可移动页面集中迁移,改变空闲页布局而非减少已用内存。A 是回收,C 是伙伴系统释放时的合并,D 只是迁移过程中的一个环节而非核心目标。

Q4. 以下哪些页面在 compaction 中通常不可移动?

多选题 · mid

  • A.用户态匿名页
  • B.内核代码段← 正确答案
  • C.页表页← 正确答案
  • D.文件缓存页

答案:BC。内核代码段和页表页都与物理地址强绑定,移动会导致内核崩溃,属于 MIGRATE_UNMOVABLE。用户态匿名页和文件缓存页属于可移动页面,正是 compaction 的主要迁移对象。

Q5. 关于双扫描器,下列说法正确的有?

多选题 · hard

  • A.正向扫描器从区域头部向尾部寻找可迁移页面← 正确答案
  • B.反向扫描器从区域尾部向头部寻找空闲页框← 正确答案
  • C.扫描器相遇时 compaction 停止← 正确答案
  • D.只有正向扫描器需要更新缓存

答案:ABC。正向扫描器找可迁移页、反向扫描器找空闲页,二者相遇即停止,对应 A、B、C。D 错误:两个扫描器的 pfn 都会缓存在 zone 的 compact_cached 字段中,下次扫描从缓存继续。

Q6. 碎片指数接近 1 意味着什么?

单选题 · mid

  • A.空闲内存充足且连续
  • B.空闲内存总量很少
  • C.空闲页分散难以满足连续分配← 正确答案
  • D.系统即将宕机

答案:C。碎片指数 \(I_{frag}=1-P_{usable}/P_{free}\) 接近 1 说明绝大多数空闲页无法被目标阶数使用,即空位零散。B 是内存总量不足,A 是指数接近 0 的情形,D 过于绝对。

Q7. kswapd 的主要职责是什么?

单选题 · easy

  • A.执行页面迁移
  • B.在内存低于水位时回收页面← 正确答案
  • C.维护伙伴系统链表
  • D.分配页框给进程

答案:B。kswapd 是内核回收守护线程,空闲内存低于 low 水位时被唤醒,扫描 LRU 列表回收不活跃页。A 是 kcompactd 或 compaction 的职责,C 是伙伴系统管理函数,D 是页面分配器的职责。

Q8. 直接规整与异步规整的主要区别是什么?

单选题 · mid

  • A.直接规整不需要迁移页面
  • B.直接规整在分配路径中同步阻塞,异步规整由后台线程执行← 正确答案
  • C.异步规整不会更新页表
  • D.两者没有区别

答案:B。直接规整发生在分配失败后,进程阻塞等待迁移完成;异步规整由 kcompactd 后台执行,不阻塞任何进程。两者都要迁移页面和更新页表,A、C、D 均错误。

Q9. 内存碎片化可能导致哪些后果?

多选题 · hard

  • A.大页(HugeTLB)分配失败← 正确答案
  • B.DMA缓冲区分配失败← 正确答案
  • C.单页分配数量减少
  • D.高阶伙伴块请求失败率上升← 正确答案

答案:ABD。碎片化直接影响需要物理连续内存的场景,如大页、DMA,并推高高阶伙伴块请求失败率。但单页分配只要有任意空闲页就能满足,数量不受碎片影响,故 C 错误。

Q10. 某系统碎片指数高达 0.9,但执行 compaction 后收益仍然很小,最可能的原因是什么?

单选题 · extreme

  • A.空闲页过多
  • B.可移动页面占比太低,迁移成本高于收益← 正确答案
  • C.伙伴系统阶数太高
  • D.扫描器方向错误

答案:B。碎片指数只衡量碎片严重程度,不衡量规整收益。若可移动页占比很低,则能迁移的页极少,无论怎么挪都腾不出大块连续区,迁移成本反而白费。A 不直接导致收益小,C 与规整收益无关,D 在内核实现中不会发生。