Linux内核内存管理深度实战:伙伴系统、Slab分配器与OOM治理
一、内核内存管理架构总览
Linux内核的内存管理子系统(MM)是操作系统最核心的组件之一,它负责物理内存的分配与回收、虚拟地址空间的映射与管理、以及内存压力的感知与响应。理解内核内存管理,是每一个系统级开发者、运维工程师和架构师的必备技能。
内核内存管理子系统主要由以下层次构成:
- 物理内存模型:FLATMEM、SPARSEMEM、SPARSEMEM_VMEMMAP 三种模型,适应不同规模的物理内存
- 伙伴系统(Buddy System):以页(Page,通常4KB)为最小单位的物理内存分配器
- Slab分配器:在伙伴系统之上的对象缓存层,用于内核频繁分配释放的小对象
- vmalloc区域:提供虚拟连续但物理不一定连续的内存映射
- 内存控制组(memcg):cgroup v1/v2 的内存资源隔离与限制机制
- OOM Killer:内存耗尽时的进程淘汰策略
- KSM(Kernel Samepage Merging):内存去重技术,提升内存利用率
| 层次 | 粒度 | 典型用途 |
|---|---|---|
| 伙伴系统 | 4KB ~ 1GB(2^0 ~ 2^18 页) | 页级DMA/常规/高端内存分配 |
| Slab/Slob/Slub | 数十字节到数页 | task_struct、inode、dentry等内核对象 |
| vmalloc | 任意大小(页对齐) | 模块加载、ioremap、大缓冲区 |
| per-cpu分配器 | 对象级(通过slab) | 避免CPU缓存行伪共享 |
二、伙伴系统(Buddy System)深度解析
2.1 核心原理
伙伴系统是Linux内核物理内存分配的基础算法,由Knowlton于1965年提出。其核心思想是将空闲内存按2的幂次分组(order 0 = 1页,order 1 = 2页,以此类推至order 10或更高),当需要分配N页时,先从对应order的空闲链表中取,若没有则从更高order的链表中取一个块,将其分裂为两个"伙伴"(buddy),一半分配给请求者,另一半留在低order链表中。
伙伴系统的"伙伴"判断条件(对于第n个页框,order为k的伙伴):
// 伙伴页框号计算(经典公式)
buddy_pfn = pfn ^ (1 << order);
// 即:将页号的第order位取反即得伙伴的页号
以order=1(2页)为例,页框0和页框1是一对伙伴,页框2和页框3是一对伙伴。判断依据是:页框0和1的二进制第1位不同,其余低位相同。
2.2 Zone-Based 分配策略
Linux将物理内存按硬件限制划分为多个Zone:
- ZONE_DMA:0~16MB,ISA DMA设备使用
- ZONE_DMA32:16MB~4GB,32位DMA设备
- ZONE_NORMAL:直接线性映射到内核虚拟地址空间(x86: 896MB)
- ZONE_HIGHMEM:超过直接映射区的高端内存(仅32位系统)
- ZONE_MOVABLE:可移动内存区域(热插拔、碎片整理用)
每个Zone内部都维护独立的一套伙伴系统free_area数组。分配请求优先从ZONE_NORMAL开始,无法满足时按fallback顺序向低级别Zone查找。
2.3 页分配器 API 实战
内核模块或驱动中常用的页级分配接口:
// 分配 2^order 个连续物理页,返回 page 结构体指针
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
// 返回虚拟地址(仅适用于 ZONE_NORMAL 线性映射区)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
// GFP 标志(Get Free Pages)决定分配行为
GFP_KERNEL // 标准分配,可能睡眠,适用于进程上下文
GFP_ATOMIC // 原子分配,不睡眠,适用于中断/持锁场景
GFP_DMA // 必须从 ZONE_DMA 分配
GFP_ZERO // 分配后清零
__GFP_HIGHMEM // 允许从高端内存分配
__GFP_NOWARN // 分配失败不打印警告
__GFP_RETRY_MAYFAIL // 允许失败,不建议重试
__GFP_NORETRY // 不重试,直接失败
2.4 碎片问题与页面迁移
长期运行的系统会面临物理内存碎片化问题——空闲页面总数足够,但不连续,无法满足高阶分配请求。Linux提出了多种解决方案:
- 反碎片(Anti-Fragmentation):将页面按可移动性分组(UNMOVABLE、RECLAIMABLE、MOVABLE),从/proc/buddyinfo可观察
- 页面迁移(Page Migration):CONFIG_MIGRATION,将已分配页面搬移以腾出连续空间
- 内存规整(Memory Compaction):CONFIG_COMPACTION,由kcompactd内核线程或显式触发
- 透明大页(THP):CONFIG_TRANSPARENT_HUGEPAGE,减少TLB miss,但可能增加内存浪费
生产环境中,可以通过以下命令监控碎片状况:
# 查看伙伴系统状态(每个order的空闲页数)
cat /proc/buddyinfo
# 查看内存碎片指数(0-100,越高越碎片化)
cat /proc/sys/vm/extfrag_threshold
# 触发主动内存规整
echo 1 > /proc/sys/vm/compact_memory
# 透明大页配置(建议数据库场景设为madvise)
cat /sys/kernel/mm/transparent_hugepage/enabled
三、Slab分配器架构与实战
3.1 为什么需要Slab?
伙伴系统的分配粒度是页(4KB),而内核频繁创建销毁的对象如task_struct(~8KB)、inode(~600B)、dentry(~200B)等,远小于一页。如果直接用伙伴系统分配,内部碎片极大。Slab分配器(1994年Bonwick为Solaris发明,Linux引入并优化)正是解决这一问题。
Slab的核心思想是对象缓存:为每种频繁分配的内核对象创建一个缓存(kmem_cache),缓存由若干"Slab"组成,每个Slab占用一页或多页,内部等分为N个slot,每个slot存放一个对象实例。对象的构造和析构只执行一次(constructor/destructor),后续分配只是从空闲slot中取用。
3.2 Slab的三种实现
Linux内核现在提供三种Slab分配器实现,可通过内核配置选项选择:
| 分配器 | 特点 | 适用场景 |
|---|---|---|
| Slab | 原始实现,硬件缓存友好 | 大多数场景(x86) |
| Slub(默认) | 简化版,元数据少,调试支持好 | 绝大多数发行版默认 |
| Slob | 极简实现,代码量极小 | 嵌入式/无MMU系统 |
Slub作为当前默认分配器,其设计哲学是"元数据尽量存放在页结构体(struct page)中",避免了Slab分配器需要大量旁路元数据的问题。每个Slab的管理信息可以通过page结构体的slab_cache和freelist字段追踪。
3.3 Slab API 与内核接口
// 1. 创建专用缓存
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
unsigned int align,
unsigned long flags,
void (*ctor)(void *));
// 2. 从缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cache, gfp_t flags);
// 分配并清零
void *kmem_cache_zalloc(struct kmem_cache *cache, gfp_t flags);
// 3. 释放对象
void kmem_cache_free(struct kmem_cache *cache, void *obj);
// 4. 销毁缓存
void kmem_cache_destroy(struct kmem_cache *cache);
// 通用分配器(不指定缓存,按大小选择最近的kmem_cache)
void *kmalloc(size_t size, gfp_t flags); // 可做线性映射的内存(通常不超过128KB)
void *kzalloc(size_t size, gfp_t flags); // 分配并清零
void kfree(const void *obj);
// 虚拟地址连续的内存
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);
void vfree(const void *addr);
// sizeof 信息(调试用)
size_t ksize(const void *obj);
3.4 kmalloc vs vmalloc vs kmem_cache_alloc
| 接口 | 物理连续 | 大小限制 | 性能 | 典型用途 |
|---|---|---|---|---|
| kmalloc | 是 | 通常<4MB | 快(线性映射) | 驱动缓冲区、数据结构 |
| vmalloc | 否 | 几乎无限 | 慢(修改页表) | 模块加载、大缓冲区 |
| kmem_cache_alloc | 是 | 取决于缓存 | 最快(构造复用) | 高频小对象 |
实测经验:kmalloc的底层实现依赖19个大小级(size-8到size-8192)的kmem_cache加上伙伴系统,这些预分配缓存使得小分配基本O(1)完成。
3.5 Slab调试与监控
Slub分配器提供了丰富的调试信息,用于排查内存泄漏和越界问题:
# 查看所有slab缓存
cat /proc/slabinfo
# Slabtop 实时监视(类似 top)
slabtop -o
# 启用Slub调试(越界检测+毒化)
echo 1 > /sys/kernel/slab/kmalloc-256/poison // 释放时填充0x5a
echo 1 > /sys/kernel/slab/kmalloc-256/red_zone // 添加前后红区
# 通过SysRq触发Slab信息dump
echo m > /proc/sysrq-trigger
dmesg | grep -i slab
// ftrace 追踪内存分配
echo 1 > /sys/kernel/debug/tracing/events/kmem/enable
cat /sys/kernel/debug/tracing/trace_pipe
四、OOM Killer 机制与治理策略
4.1 OOM 的触发条件
OOM(Out of Memory)是指系统无法满足任何内存分配请求的状态。内核的OOM Killer会在以下条件同时满足时触发:
- 伙伴系统中所有Zone的空闲页面低于最低水位线(min)
- 回收机制(kswapd后台回收+直接回收)无法释放足够页面
- GFP分配标志允许OOM(GFP_FS等)
- 无可忽略的分配请求(GFP_ATOMIC的处理不同)
4.2 OOM评分算法(oom_badness)
OOM Killer的核心问题是"杀谁"。Linux内核使用oom_badness()函数为每个进程打分,分数最高的进程将被终止。评分算法综合考虑以下因素:
// 伪代码(基于Linux 6.x逻辑)
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages) {
long points;
// 1. 占用的内存百分比(RSS + Swap + Page Table + mmap)
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
points = points * 1000 / totalpages; // 归一化到0-1000
// 2. oom_score_adj 调整(-1000 ~ 1000,-1000禁止选中)
points += (long)p->signal->oom_score_adj * totalpoints / OOM_SCORE_ADJ_MAX;
// 3. 特殊进程免杀
if (is_global_init(p) || p->flags & PF_KTHREAD) return 0;
return max(0, points);
}
oom_score_adj的值可以在用户空间通过/proc/[pid]/oom_score_adj调整,范围-1000到1000。设为-1000的进程永远不会被OOM Killer选中。
4.3 容器/Namespace场景的OOM
在cgroup v1中,内存控制组(memory cgroup)可以实现组级OOM隔离:当一个cgroup的内存使用量超过限制时,只杀死该cgroup内的进程,而不是全局扫描。
# 设置cgroup内存限制
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes // 512MB
# 查看cgroup OOM事件
cat /sys/fs/cgroup/memory/myapp/memory.oom_control
// cgroup v2 内存限制
echo "512M" > /sys/fs/cgroup/myapp/memory.max
echo "480M" > /sys/fs/cgroup/myapp/memory.high // 软限制,触发回收而非OOM
从Linux 5.19开始,cgroup v2的PSI(Pressure Stall Information)接口可以提供更细粒度的内存压力指标,提前预警OOM风险。
4.4 OOM 治理最佳实践
对于生产环境的服务部署,以下措施可以显著降低OOM带来的影响:
- 设置关键进程的oom_score_adj:数据库、消息队列、监控代理等设为-1000或负值
- 限制非核心服务的内存:通过cgroup v2 memory.max限制
- 启用earlyoom:用户态OOM守护进程,比内核OOM更快响应
- 配置panic_on_oom:集群节点设为1,让节点快速重启并触发故障转移
- 监控PSI指标:avg10 > 10表示有显著内存压力
- 预留足够min_free_kbytes:确保系统有基本可用内存供内核运行
# earlyoom 安装与配置
apt install earlyoom
# /etc/default/earlyoom 默认在可用内存<10%且交换<10%时启动杀进程
# 系统级OOM策略(集群节点推荐panic_on_oom=1)
sysctl vm.panic_on_oom=1
sysctl vm.oom_kill_allocating_task=1 // 杀触发者而非扫描
sysctl vm.overcommit_memory=2 // 严格不超分,配合overcommit_ratio
// PSI监控
cat /proc/pressure/memory
// 输出格式:some avg10=0.00 avg60=0.00 avg300=0.00 total=123456
五、内存调试与故障排查工具链
内核内存问题的调试需要借助专业的工具链。以下是从问题定位到根因分析的全流程方法:
| 工具 | 功能 | 适用场景 |
|---|---|---|
| dmesg | OOM日志、slab错误 | 所有内存事件 |
| slabtop/proc/slabinfo | Slab缓存使用统计 | Slab内存泄漏 |
| kmemleak | 内核内存泄漏检测 | C/C++内核模块 |
| page_owner | 追踪每个分配页的调用栈 | 定位页分配调用者 |
| memory cgroup | 容器级内存统计 | 容器化环境 |
| /proc/pid/smaps | 进程级内存详细统计 | 用户态内存分析 |
| /proc/meminfo | 全局内存统计 | 整体状况评估 |
| atop/sar -r | 历史内存趋势 | 规划容量 |
kmemleak是定位内核内存泄漏的利器,其原理类似于用户态的ASan/Valgrind:通过拦截kmalloc等分配函数记录每个分配的地址和调用栈,周期性扫描内存,扫描不到的被标记为leak。
# 启用并触发kmemleak扫描
mount debugfs /sys/kernel/debug -t debugfs
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak // 查看泄漏报告
// page_owner 追踪页分配
echo 1 > /proc/sys/vm/page_owner
// 分配/释放完读取
cat /proc/page_owner_sort
echo 0 > /proc/sys/vm/page_owner
六、生产环境内存参数调优
以下是经过大规模生产实践验证的内核内存参数调优建议:
# /etc/sysctl.conf
// 1. 最小空闲内存(确保内核有足够内存执行关键操作)
vm.min_free_kbytes = 262144 // 1GB RAM 机器
vm.min_free_kbytes = 1048576 // 4GB RAM 机器
// 2. 脏页回写策略
vm.dirty_ratio = 10 // 脏页占总内存10%时开始阻塞写入
vm.dirty_background_ratio = 5 // 后台回写阈值
vm.dirty_expire_centisecs = 1500 // 脏页过期时间(15秒)
vm.dirty_writeback_centisecs = 300 // 回写线程唤醒周期(3秒)
// 3. swap倾向度(0-200,越低越不愿swap)
vm.swappiness = 10 // 数据库/缓存服务推荐1-10
vm.swappiness = 60 // 默认值,通用负载
// 4. overcommit策略
vm.overcommit_memory = 0 // 启发式(默认)
vm.overcommit_memory = 1 // 永远允许(适合malloc+从来不用)
vm.overcommit_memory = 2 // 严格限制(适合数据库/金融)
// 5. NUMA策略
kernel.numa_balancing = 0 // 关闭自动NUMA均衡(某些场景有性能抖动)
// 6. Hugepage 配置
vm.nr_hugepages = 1024 // 静态大页,适合MySQL/Redis等
vm.nr_overcommit_hugepages = 0 // 额外可分配的大页
vm.hugetlb_shm_group = 1002 // 允许使用Hugepage的组ID
对于数据库专用服务器,建议关闭swap(swappiness=0)并配置足够的Hugepage,同时设置oom_score_adj=-1000保护数据库进程。对于K8s节点,则应该启用严格的cgroup内存限制和earlyoom。
七、总结与展望
Linux内核内存管理是一个历经数十年演进的精密系统。从伙伴系统的物理页管理,到Slab分配器的对象缓存优化,再到OOM Killer的极端情况处理,每一层都有其独特的设计哲学和实战调优空间。
在云原生时代,内存管理面临着新的挑战:容器化环境下的内存隔离与QPS平衡、超大页数(GB级别Hugepage)的使用、持久内存(PMEM/DIMM)与传统DRAM的混合管理、以及CXL(Compute Express Link)带来的内存池化架构。Linux内核社区也在持续迭代——从SLUB的不断优化,到MGLRU(Multi-Generational LRU)页面回收算法的引入,再到CXL.mem支持的逐步完善。
掌握这些底层原理,不仅能帮助我们更好地调试生产环境中的内存问题,也为构建高性能、高稳定的系统服务打下坚实基础。
本文涵盖:伙伴系统原理与Zone策略、Slab/Slub分配器架构、kmalloc vs vmalloc对比、OOM Killer评分算法与治理、PSI内存压力监控、kmemleak/page_owner调试工具、生产环境sysctl调优参数。适用于Linux内核开发者、系统运维工程师、SRE和性能优化工程师。

发表评论 取消回复