Skip to content

第七十五章:内存泄漏

元婴后期

涉及内核源码:

林小源在内景中发现了一件怪事。

有些 VMA 大陆的边缘在缓慢地"生长"——不是正常的增长,而是某种不受控制的蔓延。新的页面从大陆边缘冒出来,像杂草一样,无人认领,无人管理。

他蹲下来,捡起一块刚冒出来的页面。页面上什么都没有——空白的,零初始化的,但确实被分配了。

"这是什么?"

"内存泄漏。" mm_struct 的声音从远处传来,带着一种疲惫的厌烦,"进程分配了内存,但没有释放。页面被占用了,但没有任何指针指向它们——它们变成了孤儿。"

林小源翻看那块页面。页面的背面刻着一行小字:malloc(1024) ——但没有对应的

"分配了却没有释放。"他喃喃道。

"最常见的错误。" mm_struct 说,"每次 malloc 都应该有对应的 free。但程序员会忘记——尤其在错误路径上。函数提前返回了,释放代码被跳过;指针被覆盖了,原来的地址丢失了;循环引用了,没有任何外部指针能触达……"

林小源把那块页面放回去。它落回地面,和周围的页面融为一体——变成了又一块无人认领的内存。

"一块页面不算什么。"他说。

"一块不算什么。" mm_struct 说,"但你看看那边。"

林小源顺着它的指引望去。远处有一块 VMA 大陆,边缘已经蔓延出去了很远——像一条不断延伸的舌头。那条"舌头"上全是空白页面,密密麻麻,一眼望不到头。

"那是一个长期运行的进程。" mm_struct 的声音沉了下来,"每次调用都泄漏一点内存。一天泄漏 1KB,一年就是 365KB。十年——"

"十年就是几 MB。"

"几 MB 不算什么。" mm_struct 说,"但如果每次调用泄漏 1MB 呢?如果每秒调用一百次呢?一天就是 8TB。你的系统撑不过一天。"

林小源看着那条不断延伸的"舌头",后背发凉。

他走近那条"舌头",想看清楚泄漏是怎么发生的。

在"舌头"的根部——VMA 大陆正常区域和泄漏区域的交界处——他看到了一些奇怪的痕迹。有些页面被分配了,然后……指针断了。像一根绳子被剪断,绳子一端连着分配的页面,另一端悬在空中,什么都不连。

"指针丢失。" mm_struct 说,"程序分配了内存,把地址存到一个变量里。然后变量被覆盖了——新值替换了旧值。旧地址丢了,但页面还在。没有任何人知道它在哪里,也没有任何人能释放它。"

林小源试着追踪那些断掉的指针。它们像断裂的桥梁,一端悬在空中,另一端连着深埋地下的页面。他能感觉到那些页面还在"活着"——被分配了,被初始化了,但永远不会被使用。

"内核有检测工具吗?"他问。

" 。" mm_struct 说,"内核的内存泄漏检测器。它跟踪每一次内存分配,定期扫描整个内核内存——如果发现一块被分配的内存,没有任何指针指向它,就标记为疑似泄漏。"

林小源感觉到头顶有什么东西在飞。他抬头一看——一个半透明的、像眼睛一样的结构体在高空中盘旋。它不断地扫视着下方的内存,每扫一次,就有一些页面被标记上红色的光点。

"那些红点是什么?"

"疑似泄漏。" kmemleak 的声音从高空传来,冷冰冰的,像机器在报告,"没有被引用的内存块。不一定是泄漏——可能是故意的、暂时的、或者只是扫描时机不对。但值得调查。"

"怎么查看结果?"

" /sys/kernel/debug/kmemleak 。" kmemleak 说,"读取那个文件,就能看到所有疑似泄漏的记录——分配时的调用栈、大小、地址。"

林小源看着那些红点。有些红点在闪烁——那是 kmemleak 不确定的标记。有些红点则稳定地亮着——那是高置信度的泄漏。

"检测比修复更重要。"他喃喃道。

"不。" mm_struct 纠正他,"预防比检测更重要。每个 malloc 都有对应的 free——这是铁律。错误路径也要释放内存,指针释放后要置 NULL,循环引用要用弱引用打破。检测是最后的手段——预防才是根本。"

林小源坐在那条"舌头"的末端,双脚悬在空白页面的边缘。

他闭上眼睛,试着感受内存泄漏的"节奏"。在意识深处,他"看到"了——每一次 malloc,都有一小块页面从虚空中凝聚出来;每一次 free,页面就消散回去。但有些 malloc 之后……没有 free。页面凝固在那里,像化石一样,永远不变。

"小泄漏会变成大问题。"他睁开眼,轻声说。

"这就是慢性病。" mm_struct 的声音难得地柔和了一些,"不像 OOM 那样突然爆发——内存泄漏是缓慢的、持续的、不知不觉的。你的系统今天还好好的,明天还好好的,后天……突然发现内存用了一半,但你不知道用到哪里去了。"

林小源站起来,看着那条延伸到远方的"舌头"。它还在生长——缓慢地、坚定地、不可阻挡地。

"怎么修复?"他问。

"找到泄漏点。" mm_struct 说,"用 kmemleak 找到疑似泄漏的地址和调用栈,然后检查代码——为什么这块内存没有被释放?是忘了?是指针丢了?是错误路径没处理?"

"然后呢?"

"加上 free。" mm_struct 说,"就这么简单。但'简单'不等于'容易'——在几万行代码里找到一个 missing free,就像在沙滩上找一粒特定的沙子。"

林小源点了点头。他从"舌头"上跳下来,落在正常的 VMA 大陆上。脚下的地面坚实而稳定——没有泄漏,没有蔓延,没有失控的生长。

他回头看了一眼那条"舌头"。它还在延伸,像一条永远不停的河流。

内存泄漏是慢性病——不会立即致命,但会慢慢拖垮系统。


c
/*
 * 内存泄漏的常见原因:
 *
 * 1. 忘记释放
 *    ptr = malloc(size);
 *    // ... 使用 ptr ...
 *    // 忘记 free(ptr)
 *
 * 2. 指针丢失
 *    ptr = malloc(size);
 *    ptr = NULL;  // 指针丢失,无法释放
 *
 * 3. 异常路径
 *    ptr = malloc(size);
 *    if (error) return;  // 错误返回,没有释放
 *    free(ptr);
 *
 * 4. 循环引用
 *    A -> B -> C -> A
 *    没有外部引用时,无法释放
 *
 * 内核的内存泄漏检测:
 *   kmemleak — 跟踪内核内存分配
 *   /sys/kernel/debug/kmemleak
 */

struct allocation {
    void *ptr;
    size_t size;
    int leaked;
};

printf("=== 内存泄漏 — 分配了却没有释放 ===\n\n");

/* 模拟内存分配 */
struct allocation allocs[5] = {0};
int nr_allocs = 0;

printf("模拟内存分配:\n");

/* 正常分配和释放 */
allocs[0] = (struct allocation){malloc(1024), 1024, 0};
printf("分配 1KB: %p\n", allocs[0].ptr);
free(allocs[0].ptr);
printf("释放 1KB: %p\n", allocs[0].ptr);
allocs[0].leaked = 0;

/* 泄漏: 忘记释放 */
allocs[1] = (struct allocation){malloc(2048), 2048, 1};
printf("分配 2KB: %p (忘记释放)\n", allocs[1].ptr);

/* 泄漏: 指针丢失 */
allocs[2] = (struct allocation){malloc(4096), 4096, 1};
printf("分配 4KB: %p (指针丢失)\n", allocs[2].ptr);
allocs[2].ptr = NULL;

/* 泄漏: 错误路径 */
allocs[3] = (struct allocation){malloc(8192), 8192, 1};
printf("分配 8KB: %p (错误返回)\n", allocs[3].ptr);
// if (error) return; // 没有释放

printf("\n--- 泄漏统计 ---\n");
size_t total_leaked = 0;
for (int i = 0; i < 5; i++) {
    if (allocs[i].leaked) {
        printf("泄漏: %lu 字节\n", allocs[i].size);
        total_leaked += allocs[i].size;
    }
}
printf("总泄漏: %lu 字节\n\n", total_leaked);

printf("--- 内核的泄漏检测 ---\n");
printf("kmemleak:\n");
printf("  跟踪内核内存分配\n");
printf("  找到没有被引用的内存块\n");
printf("  /sys/kernel/debug/kmemleak\n\n");

printf("--- 预防内存泄漏 ---\n");
printf("1. 每个 malloc 都有对应的 free\n");
printf("2. 错误路径也要释放内存\n");
printf("3. 使用后置 NULL\n");
printf("4. 定期检查内存使用\n");

道藏笔记

内核启示

内存泄漏是指分配了内存但没有释放。

内存泄漏的原因:

  • 忘记释放
  • 指针丢失
  • 异常路径没有释放
  • 循环引用

内核的检测工具:

  • — 跟踪内核内存分配
  • /sys/kernel/debug/kmemleak — 查看泄漏报告

预防措施:

  • 每个分配都有对应的释放
  • 错误路径也要释放
  • 使用后置 NULL
  • 定期检查内存使用

内存泄漏是"慢性病"——不会立即致命,但会慢慢拖垮系统。


破关试炼

内存泄漏之试

本章中跟踪内核内存分配、扫描疑似泄漏记录的检测器叫什么?

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

以修仙之名,悟内核之道