Linux 内核内存管理是一个庞大而精密的子系统,而 SLUB 分配器作为内核对象的"资产管理者",在高性能场景下扮演着至关重要的角色。本文将深入剖析 SLUB 分配器的内部机制、与 Buddy System 的协作关系、OOM Killer 的决策逻辑,以及实战调优手段。
一、内核内存管理全景图
Linux 内核采用分层架构管理物理内存:
Buddy System(伙伴系统)作为最底层的物理页框分配器,以 page 为单位(通常 4KB)管理所有连续物理内存。它通过阶(order)的概念组织 2^n 个连续页框,支持快速拆分与合并,有效减少了外部碎片。
Slab 分配器家族(SLAB/SLUB/SLOB)构建在 Buddy System 之上,专门用于内核中小于一页的频繁分配/释放对象(如 task_struct、inode、dentry 等)。SLUB(Unqueued Slab)自 2.6.23 内核起成为默认分配器,因其简化了每 CPU/每节点的队列管理,在多核场景下展现出更优的可扩展性。
二、SLUB 分配器核心架构
2.1 三级缓存层次
SLUB 采用三级结构管理内核对象缓存:
kmem_cache:每个内核数据结构对应一个 kmem_cache。它定义了对象大小、对齐方式、构造函数等元信息。例如 task_struct 对应 task_struct_cachep,dentry 对应 dentry_cache。
kmem_cache_cpu(per-CPU 缓存):每个 CPU 维护一个快速分配路径,包含 freelist 链表和当前正在使用的 page。这是无锁分配路径,大多数 kmalloc 调用由此完成。
kmem_cache_node(NUMA 节点缓存):每个 NUMA 节点维护 partial 链表(部分空闲的 slab 页)。当 CPU 本地缓存不足时,从节点的 partial 链表补充;当节点的 partial 也不足时,向 Buddy System 申请新的页面。
2.2 快速分配路径
kmalloc 的分配流程如下(理想情况无锁):
- 通过 size → kmem_cache 的静态映射找到目标缓存
- 从 kmem_cache_cpu->freelist 取出第一个空闲对象
- 如果 freelist 为空但有 page(即 page->freelist 非空),将 page 的 freelist 迁移到 cpu freelist
- 如果 cpu 层面完全无可用对象,进入慢路径:从 node partial 或 Buddy System 获取新 slab
2.3 SLUB 的 Freelist 保护机制
相比 SLAB 分配器使用外部 freelist 管理,SLUB 将 freelist 直接嵌入 slab 页面本身。空闲对象的内存空间被复用来存储指向下一个空闲对象的指针,释放时只需将对象插入链表头部。这种设计在释放操作上是 O(1) 的常数时间开销。
自 5.15 内核起引入了 CONFIG_SLAB_FREELIST_HARDENED,将 freelist 指针与随机 cookie 异或编码,有效缓解了堆溢出利用 freelist 指针的攻击。6.x 内核更进一步引入 RANDOM_KMALLOC_CACHES,让 kmalloc 在多个同尺寸缓存中随机选择,大幅提升了堆喷攻击的成本。
三、Buddy System 与 SLUB 的协作边界
3.1 页面分配阶的选择
SLUB 向 Buddy System 申请页面时通过 order 参数指定需要的连续页数。默认情况下 SLUB 会根据对象大小和每 slab 可容纳的对象数量动态选择 order。对于大量小对象,order=0(1 页)即可起步;对于较大或数量较多的缓存,会自动提升 order。
关键权衡:order 越大,分配越容易因无法满足连续页面要求而失败(外部碎片),但 slab 页内对象连续性越好,TLB 命中率越高。
3.2 水线机制与直接回收
Buddy System 维护 min/low/high 三条水线。当空闲页面低于 min 时触发直接内存回收(direct reclaim),同步阻塞调用进程执行页面回收(脏页回写、匿名页换出等);在 min 与 low 之间触发 kswapd 后台回收;高于 high 则内存充足无需回收。
SLUB 分配触发 fast path 失败并进入 Buddy System 时,如果当前处于 direct reclaim 路径,会进一步触发shrink_slab() 回调,迫使 SLUB 收缩部分 partial slab 页归还给 Buddy System。
四、OOM Killer 深度解析
当所有内存回收手段(kswapd、direct compaction、direct reclaim)均无法释放足够内存时,OOM Killer 作为最后手段介入。这个过程在 out_of_memory() 函数中实现。
4.1 oom_badness() 评分机制
每个进程的 OOM 评分计算公式为:
points = (total_vm * 1000 / total_ram) + oom_score_adj * total_ram / 1000
其中 total_vm 是进程占用的总虚拟内存页数,oom_score_adj 可在 /proc/pid/oom_score_adj 中调整(范围 -1000 到 +1000)。特殊规则:
- init 进程(pid=1)和内核线程得分为 0,永远不会被选中
- oom_score_adj = -1000 时进程免疫 OOM killer
- 根进程默认获得少量减免
4.2 6.x 内核的 OOM Group 改进
引入 memory cgroup 后,OOM 决策增加了 cgroup 维度的考量。当某个 cgroup 内存超限时,仅在该 cgroup 内选择 victim,实现了更精细的隔离。同时引入了 oom_kill.grouping 参数支持按用户/服务分组杀进程。
五、实战调优与问题诊断
5.1 SLUB 调优参数
/proc/slabinfo:查看所有 slab 缓存的详细统计,包括活跃对象数、每 slab 对象数、页数等。
/sys/kernel/slab/<cache>/:每个缓存的独立调优入口:
- min_partial:节点 partial 链表最少保留的 slab 数,减少频繁向 Buddy System 申请
- cpu_partial:每 CPU 缓存的最大空闲对象数,超过后归还给节点
- slab_order / min_order:控制向 Buddy System 申请页面的阶数
- alignment / object_size:查看缓存对齐方式和对象真实大小
5.2 vm.min_free_kbytes 设置
该参数决定 Buddy System min 水线的绝对值。过小会导致系统进入直接回收的频率过高;过大则浪费可用内存。经验值为总内存的 1%~2%,高内存消耗(如数据库)场景可适当调高至 4%。
5.3 vm.swappiness 与内存回收策略
swappiness(0-200)控制匿名页与文件页回收的倾向性:
- 0:几乎不换出匿名页,优先回收 page cache
- 60:默认值,平衡两者
- 100+:积极换出匿名页,适合大内存场景保留更多 page cache
注意:swappiness=0 在 6.x 内核中并不意味着完全禁止 swap,在内存紧张时仍会触发最小限度的换出。
5.4 内存碎片化诊断
cat /proc/buddyinfo 直接查看各阶连续页面分布。如果高阶连续页面常年为 0,说明系统严重碎片化。
cat /proc/pagetypeinfo 可进一步按页面迁移类型(Unmovable/Movable/Reclaimable)分析碎片成因。大量 Unmovable 页面(如内核分配的 page table、kernel module)是碎片化的主要原因,也是 compaction 无法解决的痛点。
5.5 vm.overcommit_memory 策略
三种模式:
0(启发式):默认模式,小分配总是成功,超大分配会被拒绝。存在 OOM 风险。
1(总是允许):所有 malloc/mmap 都返回成功,适合科学计算等场景,OOM Killer 兜底。
2(严格模式):分配总量不能超过 swap + RAM * overcommit_ratio%,适合需要严格资源预算的场景。
六、性能观测工具链
slabtop:实时展示 slab 缓存排序,类似于 top 但聚焦内存分配。可发现异常增长的 kmem_cache。
perf 内存分析:perf record -e kmem:kmalloc -ag -- sleep 30 追踪 kmalloc 热点路径。
bpftrace 一键探针:
kretprobe:kmem_cache_alloc
{
@bytes = hist(retval ? args->size : 0);
@slab_count[arg0->name] = count();
}
ftrace 跟踪:通过 /sys/kernel/debug/tracing/events/kmem/ 下的 tracepoint 追踪所有 slab 事件。
七、6.x 内核新特性
tlbshootdown coalescing:优化了多核场景 TLB shootdown 的批量处理,减少了 MMU 通知的开销。
Multi-Gen LRU:自 5.18 引入并逐步完善,用代际(generation)标记替代传统的 active/inactive 链表,扫描效率提升显著,尤其在混合负载下减少了 page cache 误回收。
MGLRU fast mode / slow mode:fast mode 追求最低扫描开销,适合交互式桌面;slow mode 做更精确的访问频率检测,适合服务器长期运行负载。可通过 sysctl 切换。
Damon (Data Access Monitor):提供精确的工作集大小(WSS)分析和自动 NUMA 平衡,可与 Multi-Gen LRU 配合使用实现自适应内存回收。
八、总结
SLUB 分配器通过与 Buddy System 的精密配合,构建了高效的内核对象分配体系。理解其 CPU-Node 两层次缓存架构、freelist 嵌入编码机制、以及在水线压力下的回收路径,是诊断内核内存问题的基础。结合 Multi-Gen LRU、Damon、SLUB hardened 等新特性,现代 Linux 内核在内存管理的性能和安全方面都达到了新的高度。
运行时调优需要根据负载特征灵活组合参数:数据库场景侧重 page cache 保护和最小化 swap,容器场景侧重 cgroup 内存限制和 OOM 隔离,HPC 场景则可以利用 overcommit + 大页来最大化内存利用率。掌握 /proc/slabinfo、slabtop、perf kmem 等工具,能够快速定位内存泄漏、碎片化、分配热点等问题。

发表评论 取消回复