3. 内存泄漏定义

内核内存泄漏(Kernel Memory Leak)是指内核在运行过程中,动态分配的内存(通过 kmalloc、vmalloc、alloc_pages 等接口)在不再使用时,没有被正确释放,导致这部分内存永久丢失,无法被系统重新利用。与用户空间内存泄漏不同,内核内存泄漏往往更隐蔽、危害更大,因为内核管理着整个系统的物理内存资源。

核心定义: 内核内存泄漏 = 动态分配的内存对象丢失了所有引用指针,且未调用对应的释放函数(如 kfree、vfree、free_pages),导致该内存块永久脱离内核内存管理器的追踪。

3.1 泄漏的典型症状

内核内存泄漏不会像用户态程序那样立即崩溃,而是通过一系列系统级症状逐步显现。下表总结了最常见的泄漏症状及其排查线索:

症状类别 具体表现 排查线索
系统可用内存持续减少 /proc/meminfo 中 MemFree 和 MemAvailable 随时间线性下降,即使没有新进程启动 使用 free -h 或 cat /proc/meminfo 定期采样
Slab 缓存异常增长 /proc/slabinfo 中特定 slab 缓存(如 kmalloc-*、dentry)持续增长,不回落 slabtop -s c 按缓存大小排序观察
OOM(Out-Of-Memory)触发 系统日志(dmesg)出现 Out of memory: Killed process 信息,OOM Killer 被迫终止进程 dmesg | grep -i "oom" 查看被杀进程
内核线程/进程 RSS 异常 /proc/<pid>/status 中 VmRSS 持续增长,尤其是内核工作线程 ps aux --sort=-%mem 或 top -p <pid>
系统响应变慢、卡顿 内存压力导致频繁回收(reclaim)、swap 换入换出,CPU iowait 升高 vmstat 1 观察 si/so 和 wa 列
经验法则: 如果系统在无业务负载变化的情况下,MemFree 每小时下降超过 5%,且无法通过 drop_caches 回收,基本可以判定存在内核内存泄漏。

3.2 泄漏对系统稳定性的影响

内核内存泄漏对系统稳定性的破坏是渐进式且多层次的。下面通过一张流程图展示泄漏从发生到系统崩溃的完整演化路径:

内核内存泄漏对系统稳定性的影响演化路径 阶段1:泄漏发生 kmalloc 未 kfree 阶段2:可用内存减少 MemFree 持续下降 阶段3:内存碎片化 大块连续内存稀缺 阶段4:内存回收压力 kswapd 频繁唤醒 阶段5:性能严重下降 CPU iowait 飙升 阶段6:OOM 触发 OOM Killer 杀进程 阶段7:系统崩溃 / 关键服务被杀死 内核 panic 或关键进程被 OOM Killer 误杀 ⚠ 关键观察: • 泄漏初期(阶段1-2)几乎无感知,但不可逆。 • 到达阶段5-6时,系统已处于高危状态,OOM Killer 可能误杀关键进程(如 sshd、dockerd)。

3.3 泄漏对系统各组件的具体影响

内核内存泄漏不仅影响内存子系统,还会波及整个系统的运行稳定性。下表从不同维度分析了泄漏带来的连锁反应:

影响维度 具体表现 严重程度
内存子系统 Buddy 系统可用页面减少,NR_FREE_PAGES 趋近于 0;Slab 缓存膨胀,active_anon 异常升高 ★★★★★
进程调度 内存压力导致 direct reclaim 路径被频繁触发,进程在分配内存时阻塞,调度延迟增加 ★★★★☆
I/O 子系统 kswapd 大量换出匿名页,导致磁盘 I/O 压力骤增,iowait 飙升,影响存储性能 ★★★★☆
网络子系统 sk_buff 泄漏导致网络吞吐量下降,连接超时,甚至网卡 OOM ★★★☆☆
文件系统 dentry/inode 缓存泄漏导致目录遍历变慢,文件打开失败,No space left on device 误报 ★★★★☆
设备驱动 DMA 缓冲区泄漏导致设备无法提交新 I/O 请求,硬件超时,驱动报错 ★★★★★

3.4 泄漏的隐蔽性与危害

内核内存泄漏之所以是驱动开发中的“头号杀手”,在于其三个核心特性:

  • 累积性: 单次泄漏量可能很小(几十字节),但经过数小时甚至数天的累积,会耗尽整个系统的内存。
  • 不可回收性: 与用户态不同,内核泄漏的内存无法通过 kill 进程来回收,只能重启系统。
  • 连带破坏性: OOM Killer 在杀死进程时,可能误杀关键系统服务(如 systemd、sshd),导致系统完全不可用。
实战警示: 某生产环境中的 NVMe 驱动因 bio 结构体泄漏,每处理 100 万次 I/O 泄漏 4KB。系统运行 72 小时后,MemFree 从 32GB 降至 128MB,最终 OOM Killer 杀死了 MySQL 进程,导致 30 分钟业务中断。事后排查发现,驱动在 I/O 完成回调中遗漏了 bio_put() 调用。

3.5 泄漏的常见根因分类

根据内核驱动开发中的实际案例,内存泄漏的根因可以归纳为以下几类:

根因类型 典型场景 示例代码(错误写法)
遗漏释放 在错误路径或提前返回时忘记调用 kfree
buf = kmalloc(size, GFP_KERNEL);
if (error) {
    return -EINVAL;  // 遗漏 kfree(buf)
}
kfree(buf);
引用丢失 将指针覆盖或赋值为 NULL,原内存无法访问
ptr = kmalloc(size, GFP_KERNEL);
ptr = another_ptr;  // 原内存地址丢失
循环累积 在循环或定时器中不断分配但不释放
while (1) {
    buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
    // 没有对应的 kfree
}
引用计数失衡 kref、module_put 等引用计数只增不减
kobject_get(&obj->kobj);
// 缺少对应的 kobject_put
数据结构未清理 链表、哈希表中的节点在移除时未释放内存
list_del(&entry->list);
// 遗漏 kfree(entry)
排查起点: 当怀疑驱动存在内存泄漏时,首先检查 /proc/slabinfo 中驱动使用的 slab 缓存是否持续增长。如果 kmalloc-128 或自定义 slab 的 active_objs 随时间线性上升,基本可以锁定泄漏源。