Skip to content

第七十六章:内存压缩

元婴后期

涉及内核源码:

林小源站在一片广阔的内存平原上,脚下的地面像棋盘一样被分成了无数格子。有的格子上站着页面——发光的方块,有的格子空着。

但他很快发现了一个问题。

空闲的格子到处都是,但它们不连续。每隔一两个格子就有一个空的,像被人故意打散了一样。

"碎片化。"一个声音从远处传来。

林小源循声望去,看到一个穿着灰色长袍的老者推着一辆独轮车,车上装满了发光的页面方块。老者把车停在一块碎片化的区域前,开始搬运页面——把已分配的页面一个一个搬到区域的一端,空闲页面自然聚集到另一端。

"你在干什么?"

"压缩。"老者头也不回地说,"Compaction。空闲页面分散在各处,大页分配找不到连续的空闲块。我把已分配的页面挪到一边,空闲的自然就聚在一起了。"

林小源看着老者搬运页面。每搬一个,老者就要更新对应的页表项——把旧地址换成新地址。有些页面搬不动——它们被钉住了, 标记或者正在被 DMA 使用。

"搬不动的怎么办?"

"跳过。"老者说,"压缩不是强制的。搬不动的就留在原地,我搬能搬的。最终能不能形成足够大的连续块,看运气。"

林小源注意到,在平原的远处,有一个影子在不停地移动。它不像老者那样手动搬运,而是自动地、持续地扫描着整个区域。

"那是 。"老者说,"内核的压缩守护进程。它在后台运行,定期检查碎片化程度。当碎片化严重到影响大页分配时,它就自动启动压缩。"

林小源看着那个影子。它走得很慢,但从未停止。

"为什么不一直压缩?"

"代价。"老者说,"每一次压缩都要复制页面、更新页表、刷新 TLB。如果系统很忙,压缩会和正常工作争抢 CPU。所以 kcompactd 只在需要的时候才工作。"

林小源坐在一块空闲的格子上,思考着。

"回收和压缩有什么区别?"他问。

老者停下手中的活,擦了擦汗。"回收是丢弃——把不活跃的页面写到 swap,释放物理内存。压缩是整理——移动活跃的页面,让空闲的聚在一起。回收增加空闲内存的总量,压缩让现有的空闲内存更连续。"

"一个增加数量,一个改善质量。"

"对。"老者说,"碎片化严重时,光回收没用——你回收了 100 个页面,但它们分散在各处,还是凑不出一个 2MB 的大页。这时候需要压缩。反过来,如果内存真的不够用,压缩也帮不了你——你需要回收。"

林小源站起来,看着老者继续搬运页面。那是一项枯燥的工作,但每一搬动一次,棋盘上的空闲区域就变得更连续一点。

他忽然明白过来——"整理"和"丢弃"是两回事。


c
struct page {
    int allocated;
    int movable;
};

printf("=== 内存压缩 — 消除碎片 ===\n\n");

struct page memory[16];
int pattern[] = {1,0,1,0,1,1,0,1,0,1,0,0,1,0,1,0};

for (int i = 0; i < 16; i++)
    memory[i].allocated = pattern[i];

printf("压缩前(碎片化):\n");
printf("  1=已分配 0=空闲\n  ");
for (int i = 0; i < 16; i++)
    printf("%d ", memory[i].allocated);
printf("\n");

int free_count = 0;
for (int i = 0; i < 16; i++)
    if (!memory[i].allocated) free_count++;
printf("  空闲页面: %d\n", free_count);
printf("  最大连续空闲: 2 页\n\n");

printf("执行压缩...\n\n");

struct page compressed[16];
int alloc_idx = 0;
int free_idx = 16 - free_count;

for (int i = 0; i < 16; i++) {
    if (memory[i].allocated) {
        compressed[alloc_idx++].allocated = 1;
    } else {
        compressed[free_idx++].allocated = 0;
    }
}

printf("压缩后:\n");
printf("  ");
for (int i = 0; i < 16; i++)
    printf("%d ", compressed[i].allocated);
printf("\n");
printf("  空闲页面: %d\n", free_count);
printf("  最大连续空闲: %d\n\n", free_count);

printf("--- 压缩的触发 ---\n");
printf("1. 大页分配失败\n");
printf("2. 外部碎片严重\n");
printf("3. kcompactd 守护进程\n\n");

printf("--- 压缩的代价 ---\n");
printf("1. 复制页面: 需要 CPU 时间\n");
printf("2. 更新页表: 需要 TLB 刷新\n");
printf("3. 可能延迟: 影响响应时间\n");

道藏笔记

内核启示

内存压缩通过移动页面消除碎片。

压缩的工作原理:

  1. 扫描内存区域
  2. 移动已分配页面到一端
  3. 空闲页面聚集到另一端
  4. 形成连续空闲区域

触发条件:

  • 大页分配失败
  • 外部碎片严重
  • 守护进程

压缩 vs 回收:

  • 回收 — 释放不活跃页面,增加空闲内存
  • 压缩 — 移动活跃页面,整理空闲内存

压缩是"整理",回收是"丢弃"——两者配合使用。


破关试炼

内存压缩之试

内存碎片严重、影响大页分配时,本章提到会在后台启动压缩的守护进程叫什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道