第七十五章:内存泄漏
元婴后期涉及内核源码:
一
林小源在内景中发现了一件怪事。
有些 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 大陆上。脚下的地面坚实而稳定——没有泄漏,没有蔓延,没有失控的生长。
他回头看了一眼那条"舌头"。它还在延伸,像一条永远不停的河流。
内存泄漏是慢性病——不会立即致命,但会慢慢拖垮系统。
/*
* 内存泄漏的常见原因:
*
* 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");#include <stdio.h>
#include <stdlib.h>
/*
* 内存泄漏的常见原因:
*
* 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;
};
int main() {
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");
return 0;
}道藏笔记
内核启示
内存泄漏是指分配了内存但没有释放。
内存泄漏的原因:
- 忘记释放
- 指针丢失
- 异常路径没有释放
- 循环引用
内核的检测工具:
- — 跟踪内核内存分配
/sys/kernel/debug/kmemleak— 查看泄漏报告
预防措施:
- 每个分配都有对应的释放
- 错误路径也要释放
- 使用后置 NULL
- 定期检查内存使用
内存泄漏是"慢性病"——不会立即致命,但会慢慢拖垮系统。
内存泄漏之试
本章中跟踪内核内存分配、扫描疑似泄漏记录的检测器叫什么?