2. 内核内存管理基础:伙伴系统、slab分配器、vmalloc区域、内存回收机制

在深入使用 SystemTap 调试内核内存泄漏之前,必须透彻理解 Linux 内核的内存管理架构。内存泄漏的本质是“分配后未释放”,而泄漏可能发生在不同的内存管理层级。本章将拆解内核内存管理的四大核心子系统:伙伴系统(Buddy System)、slab 分配器、vmalloc 区域以及内存回收机制,并指出每一层可能出现的泄漏模式。

调试视角: SystemTap 探测点通常挂载在内存分配与释放的路径上。理解底层数据结构(如 struct page、kmem_cache、vm_struct)是编写精准探测脚本的前提。

2.1 伙伴系统(Buddy System)

伙伴系统是内核最底层的物理页面管理器,负责以 2 的幂次阶(order)为单位分配连续的物理页框。其核心数据结构是 free_area 数组,每个元素维护一个空闲链表。

核心数据结构

  • struct page:描述每个物理页框的元数据,包含 _refcount(引用计数)、flags、mapping 等字段。
  • free_area[MAX_ORDER]:每个阶(0~10)对应一个链表,存储该阶的空闲连续页块。
  • zone:将物理内存划分为 DMA、Normal、HighMem 等区域,每个 zone 拥有独立的伙伴系统。
泄漏模式: 如果驱动调用 alloc_pages(gfp_mask, order) 后未调用 __free_pages(page, order),会导致该阶的连续页块永久丢失。SystemTap 可通过探测 mm_page_alloc 和 mm_page_free tracepoint 来追踪未配对的分配。
伙伴系统:分配与合并流程 order=2 空闲块 order=2 空闲块 order=2 已分配 order=2 空闲块 请求 order=0 order=1 order=1 0 1 释放后合并 order=2 重新空闲 分配时分裂大块,释放时合并伙伴块以缓解碎片化

2.2 slab 分配器

slab 分配器构建在伙伴系统之上,用于管理内核中频繁分配/释放的小对象(如 task_struct、inode、dentry)。它通过缓存(kmem_cache)维护预先分配的对象池,避免直接操作伙伴系统带来的性能开销。

关键概念对比

概念 说明 SystemTap 探测点
kmem_cache 管理特定类型对象的缓存,包含多个 slab kernel.trace("kmem_cache_alloc")
slab 一个或多个连续物理页框,划分为若干对象 kernel.trace("kmem_cache_free")
object 实际分配的内核对象实例 通过 $ptr 追踪地址
着色(coloring) 通过偏移调整缓存行对齐,减少伪共享 通常不直接探测
常见泄漏场景: 内核模块反复调用 kmem_cache_alloc() 但遗漏 kmem_cache_free(),导致特定 kmem_cache 的 slab_objects 持续增长。使用 SystemTap 统计 kmem_cache_alloc 与 kmem_cache_free 的差值即可定位泄漏的缓存。
slab 分配器结构(kmem_cache) kmem_cache (如 dentry_cache) slab 0 [部分空闲] slab 1 [全部空闲] slab 2 [全部使用] obj obj 空闲 空闲 slab 内部对象布局(含空闲链表)

2.3 vmalloc 区域

vmalloc 用于分配虚拟地址连续但物理地址不连续的内存区域,适用于大块内存分配(如内核模块加载、动态缓冲区)。其虚拟地址范围通常位于 VMALLOC_START 到 VMALLOC_END 之间,通过页表建立非连续物理页的映射。

vmalloc 与 kmalloc 对比

特性 kmalloc vmalloc
物理连续性 连续 不连续
分配大小 通常 < 4MB(受限于伙伴系统) 可分配大块(可达数百 MB)
性能 快速(slab 缓存) 较慢(需建立页表)
内存碎片影响 易受外部碎片影响 不受外部碎片影响
典型用途 内核数据结构、网络缓冲区 动态模块、大块 I/O 缓冲区
泄漏模式: 调用 vmalloc(size) 后未调用 vfree(addr),会导致虚拟地址空间泄漏以及物理页引用计数无法归零。SystemTap 可通过探测 vmalloc 和 vfree 函数入口/返回来追踪泄漏。
vmalloc 虚拟地址空间映射 虚拟地址空间(连续) vmalloc 块 A vmalloc 块 B vmalloc 块 C 页 页 页 页 页 页 物理页框(不连续,通过页表映射)

2.4 内存回收机制

当系统内存压力增大时,内核启动内存回收以释放可回收页面。主要回收路径包括:

  • 页面回收(Page Reclaim):通过 LRU 链表扫描匿名页和文件页,将脏页写回、干净页直接回收。
  • slab 收缩(Slab Shrinker):调用注册的 shrinker 回调,回收 dentry、inode 等可回收 slab 对象。
  • OOM Killer:当回收无法满足需求时,选择并杀死占用内存最多的进程。
泄漏与回收的混淆: 内存泄漏会导致回收机制持续触发,但回收无法释放“已泄漏”的内存(因为泄漏的内存没有合法引用,无法被扫描到)。SystemTap 可以通过监测 kswapd 唤醒频率和 shrink_slab 调用次数来间接判断是否存在泄漏。
内核内存回收流程 内存压力触发(kswapd / direct reclaim) 页面回收 扫描 LRU 链表 slab 收缩 调用 shrinker 回调 OOM Killer 杀死进程 释放内存 / 缓解压力 泄漏的内存无法被任何回收路径扫描到,导致 kswapd 持续运行

2.5 各层级泄漏检测要点总结

层级 分配函数 释放函数 SystemTap 关键探测点 泄漏特征
伙伴系统 alloc_pages() __free_pages() mm_page_alloc, mm_page_free 特定 order 的 free_area 持续减少
slab 分配器 kmem_cache_alloc() kmem_cache_free() kmem_cache_alloc, kmem_cache_free 特定缓存 slab_objects 只增不减
vmalloc vmalloc() vfree() vmalloc 函数入口/返回 /proc/vmallocinfo 条目持续增长
内存回收 — — mm_vmscan_kswapd_wake, mm_shrink_slab 回收频率异常高但无法释放内存
实战建议: 在编写 SystemTap 脚本前,先通过 /proc/buddyinfo、/proc/slabinfo、/proc/vmallocinfo 获取基线数据。这些文件直接反映了各层级的内存使用状态,是判断泄漏的第一手证据。