Linux内核物理内存管理:从伙伴系统到SLUB分配器深度解析
第一次接触 Linux 内核内存管理时,很多人会陷入一个误区:认为理解了虚拟内存、分页机制这些概念就等于掌握了内核内存管理的精髓。但真正深入内核源码后才发现,物理内存的管理远比想象中复杂——它不仅要处理从硬件启动时的内存布局探测,到伙伴系统、SLUB 分配器的层层抽象,还要应对 NUMA 架构、内存回收、碎片整理等工程难题。
更关键的是,内核内存管理不是一个孤立模块,它贯穿了从系统启动到进程调度、从驱动分配到网络缓存的每个环节。如果你只停留在理论层面,当遇到实际性能问题或内存泄漏时,往往会无从下手。这篇文章将带你从物理内存的硬件基础开始,逐步拆解内核如何组织、分配、回收物理内存,并给出可实操的排查方法和优化思路。
1. 从硬件到内核:物理内存的探测与初始化
理解内核内存管理,首先要回答一个基本问题:内核如何知道系统有多少物理内存,以及它们的布局是怎样的?这个问题在 x86 架构上尤其重要,因为 BIOS 提供的 e820 内存映射表描述了系统中可用的、保留的、甚至是存在缺陷的内存区域。
1.1 启动阶段的内存布局探测
在 x86 系统启动时,BIOS 会通过 e820 调用提供内存映射信息。内核在
arch/x86/kernel/e820.c
中处理这些信息,将其转换为
struct e820_entry
数组。这个过程发生在实模式到保护模式的转换期间,此时内核还没有建立完整的页表映射。
// 简化版 e820 条目结构
struct e820_entry {
u64 addr; // 内存区域起始地址
u64 size; // 区域大小
u32 type; // 类型:E820_RAM、E820_RESERVED 等
};
关键的是,e820 表显示的内存布局通常不是连续的。可能存在多个可用的 RAM 区域,中间被保留区域(如 BIOS 代码、硬件映射等)隔开。这就引出了内核需要解决的第一个问题:如何管理这些非连续的内存区域?
1.2 物理内存的抽象:从 e820 到 memblock
在完全建立页表之前,内核使用 memblock 来管理物理内存。memblock 是一个早期内存分配器,它将 e820 信息转换为两个核心数组:
-
memory:记录所有可用的物理内存区域 -
reserved:记录已分配或保留的内存区域
你可以通过
/sys/kernel/debug/memblock/memory
查看系统内存区域信息:
# 查看内存区域布局
cat /sys/kernel/debug/memblock/memory
memblock 的初始化在
init/main.c
的
setup_arch()
阶段完成。这个阶段内核还没有启用 slab 分配器,所以 memblock 使用最简单的位图管理方式。
注意:在排查内存问题时,如果怀疑是早期内存分配异常,可以检查 memblock 的调试信息。但生产环境通常不开启 memblock debug,需要在编译内核时配置
CONFIG_DEBUG_FS。
1.3 内核页表的建立与内存映射
内核启动过程中最关键的步骤之一是从实模式切换到保护模式,再启用分页机制。这个过程涉及多级页表的建立:
-
临时页表
:在
arch/x86/kernel/head_64.S中建立最初的 4 级页表,映射内核代码和数据区域 -
最终页表
:在
arch/x86/mm/init_64.c中建立完整的直接映射区域(direct mapping)
x86_64 架构下,内核空间的虚拟地址与物理地址存在固定的偏移关系(通常是 0xffff888000000000)。这种直接映射简化了内核访问物理内存的过程,但也意味着内核虚拟地址空间的大小受限于物理内存容量。
// 物理地址到内核虚拟地址的转换
#define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
#define __pa(x) ((unsigned long)(x) - PAGE_OFFSET)
这种设计带来的一个实际影响是:在内存大于 64TB 的系统上,可能需要调整内核配置来支持更大的直接映射区域。
2. 伙伴系统:大块物理内存的管理核心
当内核完成基本初始化后,memblock 会将内存控制权交给伙伴系统(Buddy System)。伙伴系统是物理内存管理的基石,它负责以页为单位(通常是 4KB)分配和释放连续物理内存。
2.1 伙伴系统的工作原理与数据结构
伙伴系统的核心思想是将物理内存划分为多个阶(order)的链表,每个阶对应 2^order 个连续页面。当请求分配内存时,系统会从最适合的阶开始查找,如果该阶没有空闲块,就向更高阶分裂。
关键数据结构在
include/linux/mmzone.h
中定义:
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
// ... 其他字段
struct free_area free_area[MAX_ORDER];
// ...
};
MAX_ORDER
通常定义为 11,意味着伙伴系统最多可以分配 2^10 = 1024 个页面(4MB)的连续内存。这也是为什么需要其他机制(如 CMA、vmalloc)来分配更大块内存的原因。
2.2 内存迁移类型与反碎片机制
Linux 2.6.24 引入了迁移类型来减少内存碎片。系统将页面分为以下几种类型:
-
MIGRATE_UNMOVABLE:不可移动页面(内核数据结构等) -
MIGRATE_MOVABLE:可移动页面(用户进程数据) -
MIGRATE_RECLAIMABLE:可回收页面(文件缓存等) -
MIGRATE_CMA:连续内存分配器专用
你可以通过
/proc/buddyinfo
查看各迁移类型的空闲页面分布:
# 查看伙伴系统状态
cat /proc/buddyinfo
输出示例:
Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3
Node 0, zone DMA32 1043 5278 1283 892 467 228 118 48 30 15 20
这个信息对于诊断内存碎片问题非常有用。如果高阶(右侧)的空闲块很少,说明系统可能存在碎片。
2.3 实际应用:诊断内存碎片问题
当驱动程序申请大块连续内存失败时,伙伴系统往往是问题的根源。排查步骤应该是:
-
检查当前碎片状态 :
cat /proc/buddyinfo cat /proc/pagetypeinfo -
确认内存申请大小 :计算需要的阶数,
order = ilog2(size/PAGE_SIZE) -
分析申请路径 :使用
ftrace或perf跟踪__alloc_pages调用链 -
考虑解决方案 :
-
调整迁移类型配置(通过
/proc/sys/vm/相关参数) - 使用 CMA 预留连续内存
- 修改驱动使用多段映射而非单段连续内存
-
调整迁移类型配置(通过
经验:在嵌入式或需要大量连续内存的场景中,应该在系统启动早期通过内核参数预留内存,而不是依赖运行时的伙伴系统。
3. SLUB 分配器:小块内存的高效管理
伙伴系统以页为单位管理内存,但对于几十到几百字节的小块内存,直接使用伙伴系统会产生巨大浪费。SLUB 是当前内核默认的小块内存分配器,它构建在伙伴系统之上,为内核对象提供高效的内存分配。
3.1 SLUB 的三层架构设计
SLUB 分配器采用巧妙的三层设计:
- kmem_cache :每个对象类型对应一个缓存(如 task_struct、inode_cache)
- SLUB 页面 :每个 CPU 有本地的 slab 页面,用于快速分配
- 伙伴系统 :当 slab 不足时,向伙伴系统申请整页内存
这种设计的优势在于:
- per-CPU 缓存减少锁竞争
- 对象复用减少初始化开销
- 自然缓存对齐提高访问效率
查看系统所有 kmem_cache 的状态:
# 查看 SLUB 分配器统计
cat /proc/slabinfo
3.2 实际性能优化:调整 SLUB 参数
在生产环境中,SLUB 分配器的配置对性能有显著影响。特别是网络、文件系统等高频分配路径:
# 调整特定 kmem_cache 的批量分配数量
echo "16" > /sys/kernel/slab/kmalloc-128/cpu_partial
# 限制每个 slab 的最大对象数,减少内存浪费
echo "16" > /sys/kernel/slab/kmalloc-512/objects
调整原则应该是:
- 对于高频小对象,增加 per-CPU 缓存大小
- 对于大对象或低频对象,减少 slab 页面数量以节省内存
-
根据实际负载使用
slabinfo工具分析热点
3.3 内存泄漏检测与调试
SLUB 提供了强大的调试功能,可以在编译内核时开启:
-
CONFIG_SLUB_DEBUG=y:启用基本调试功能 -
CONFIG_SLUB_DEBUG_ON=y:默认开启所有 slab 的调试
调试功能开启后,可以检测:
- 使用未初始化内存
- 释放后使用(use-after-free)
- 越界访问
实际操作中,更常用的方法是使用
kmemleak
:
# 启用 kmemleak
echo scan > /sys/kernel/debug/kmemleak
# 获取检测报告
cat /sys/kernel/debug/kmemleak
4. NUMA 架构下的内存管理挑战
在多处理器系统中,NUMA(Non-Uniform Memory Access)架构带来了新的内存管理复杂度。每个 CPU 节点有本地内存,访问远程节点内存会有额外延迟。
4.1 NUMA 拓扑识别与内存分配策略
内核在启动时通过 ACPI SRAT 表或硬件特定方法识别 NUMA 拓扑。查看系统 NUMA 信息:
# 查看 NUMA 节点布局
numactl --hardware
# 查看进程的 NUMA 内存分配
cat /proc/<pid>/numa_maps
内核提供了多种 NUMA 分配策略:
-
MPOL_DEFAULT:使用进程启动节点的策略 -
MPOL_BIND:绑定到特定节点 -
MPOL_PREFERRED:优先使用指定节点 -
MPOL_INTERLEAVE:在多个节点间交错分配
4.2 实际性能调优:NUMA 平衡与绑定
对于性能敏感的应用,错误的 NUMA 策略可能导致性能下降 30% 以上。优化建议:
-
分析当前状态 :
# 查看每个节点的内存使用 numastat # 查看进程的内存节点分布 numastat -p <pid> -
绑定内存分配 :
# 将进程绑定到节点 0 运行,内存也从节点 0 分配 numactl --membind=0 --cpunodebind=0 <command> -
监控 NUMA 平衡器效果 :
# 查看自动平衡统计 cat /proc/vmstat | grep numa
注意:NUMA 平衡器(自动迁移页面到访问节点)会带来一定开销。在已知工作负载特性的情况下,手动绑定通常能获得更好性能。
4.3 跨节点分配的内存回收挑战
NUMA 环境下的内存回收比 UMA 系统复杂得多。当某个节点内存不足时,回收器需要决定:
- 回收本地节点的内存(可能较少)
- 回收远程节点的内存(访问成本高)
- 迁移页面到访问节点(平衡长期效益与短期开销)
内核的
zone_reclaim_mode
参数控制这一行为:
# 启用积极的本地回收
echo 1 > /proc/sys/vm/zone_reclaim_mode
在实际调优中,需要根据应用的内存访问模式选择合适的策略。数据库等连续访问大量数据的工作负载适合绑定策略,而多线程科学计算可能更适合交错分配。
5. 内存回收机制:从 LRU 到页面交换
物理内存是有限资源,当内存紧张时,内核需要回收部分页面来满足新的分配请求。理解回收机制对于诊断系统卡顿、OOM 杀进程等问题至关重要。
5.1 LRU 链表与页面分类
内核维护多个 LRU(Least Recently Used)链表来跟踪页面使用情况:
- 活跃匿名页面 :进程堆栈、堆数据
- 非活跃匿名页面 :待换出的匿名内存
- 活跃文件页面 :正在使用的文件缓存
- 非活跃文件页面 :可回收的文件缓存
查看当前 LRU 状态:
# 查看各 LRU 链表大小
cat /proc/meminfo | grep -i active
cat /proc/meminfo | grep -i inactive
5.2 内存回收触发条件与流程
内存回收主要通过以下条件触发:
- 直接回收 :分配内存时发现空闲页面不足
- kswapd 后台回收 :当空闲页面低于水位线时启动
- 内存压力通知 :cgroup 内存压力达到阈值
回收流程的核心在
mm/vmscan.c
的
shrink_node
函数中,基本步骤:
- 计算需要回收的页面数量
- 先尝试回收文件页面(无代价,只需丢弃缓存)
- 如果文件页面不足,开始回收匿名页面(需要交换到磁盘)
- 根据 LRU 算法选择最近最少使用的页面
- 处理脏页回写和交换输出
5.3 交换机制与交换分区优化
当匿名页面需要被回收时,内核将其内容写入交换设备。交换策略对系统性能影响巨大:
交换分区配置建议 :
- 对于桌面系统:交换空间 = 物理内存的 1.5-2 倍
- 对于服务器:根据工作负载特性决定,可能完全禁用交换
- 使用 SSD 作为交换设备能显著提高交换性能
监控交换活动 :
# 查看交换统计
vmstat 1
# 查看进程交换情况
cat /proc/<pid>/status | grep VmSwap
优化交换行为 :
# 调整交换倾向性(0-100,值越大越积极交换)
echo 10 > /proc/sys/vm/swappiness
# 控制脏页回写阈值
echo 20 > /proc/sys/vm/dirty_ratio
5.4 OOM Killer 机制与调优
当内存回收无法满足分配请求时,内核会触发 OOM Killer 选择进程终止。理解选择算法有助于预防重要进程被误杀:
查看 OOM 评分 :
# 查看进程的 OOM 评分
cat /proc/<pid>/oom_score
# 调整 OOM 调整值(-1000 到 1000)
echo -1000 > /proc/<pid>/oom_score_adj
OOM Killer 调优策略 :
-
为关键进程设置
oom_score_adj = -1000 - 使用 cgroup 内存限制隔离不同服务
-
监控
/var/log/messages中的 OOM 日志
6. 高级主题与性能优化实战
掌握了基本机制后,我们需要关注实际生产环境中的高级主题和优化技巧。
6.1 透明大页(THP)的性能影响
透明大页(2MB 或 1GB 页面)能减少 TLB 缺失,提高内存访问效率,但也可能增加内存碎片:
# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 控制 THP 行为
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
使用建议:
- 内存密集型应用(数据库、科学计算)通常受益于 THP
- 内存受限或碎片敏感的系统应该谨慎使用
-
使用
madvise模式,让应用显式请求大页
6.2 内存压缩与 KSM
内核页面合并(KSM)通过合并相同内容的页面节省内存,特别适合虚拟化环境:
# 查看 KSM 统计
cat /sys/kernel/mm/ksm/pages_shared
cat /sys/kernel/mm/ksm/pages_sharing
内存压缩(zswap、zram)在内存紧张时压缩页面内容,减少交换I/O:
# 启用 zswap
echo 1 > /sys/module/zswap/parameters/enabled
6.3 内存性能监控工具箱
建立完整的内存监控体系:
基础监控 :
# 实时内存状态
watch -n 1 'cat /proc/meminfo'
# 内存压力监控
cat /proc/pressure/memory
高级分析 :
# 使用 perf 分析内存访问模式
perf record -e cache-misses -ag -- sleep 5
# 使用 valgrind 进行用户空间内存分析
valgrind --tool=memcheck <program>
生产环境检查清单 :
-
定期检查
/proc/buddyinfo预防内存碎片 -
监控
/proc/vmstat中的页面回收统计 - 设置 cgroup 内存限制防止单个服务耗尽内存
- 配置合理的交换空间和 swappiness 参数
Linux 内核的物理内存管理是一个极其复杂的系统,从硬件抽象到高级优化策略,每个层面都有其设计哲学和实际考量。真正掌握内存管理不在于记住所有细节,而在于理解各个组件如何协同工作,以及当问题出现时如何系统性地排查和解决。
最有效的学习路径是从实际需求出发:先理解自己工作场景中的内存使用模式,然后有针对性地深入研究相关机制。无论是优化数据库性能、调试内核驱动内存泄漏,还是简单地为应用选择合适的内存配置,扎实的内存管理知识都是不可或缺的基础。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)