Linux 内核的内存管理子系统是整个操作系统最核心、最复杂的组件之一。理解其原理和机制,对于系统调优、性能分析和内核开发都至关重要。
一、物理内存管理架构
Linux 内核的物理内存管理采用 buddy system(伙伴系统)与 slab/slub/slob 分配器协作的经典架构。物理内存的基本单位是页框(page frame),大小通常为 4KB。内核使用 struct page 结构体来跟踪系统中的每一个物理页面。
1.1 节点(node)与 zone
NUMA 架构下,内存被划分为多个节点,每个节点包含若干 zone。典型的 zone 分为:
| Zone 类型 | 说明 | x86_64 典型大小 |
|---|---|---|
| ZONE_DMA | ISA 设备 DMA 用 | 0-16MB |
| ZONE_DMA32 | 32-bit 设备 DMA 用 | 16MB-4GB |
| ZONE_NORMAL | 内核可直接映射 | 4GB-896MB(内核空间) |
| ZONE_HIGHMEM | 用户态大内存 | (x86_64 无此 zone) |
| ZONE_MOVABLE | 可迁移页面(内存热插拔) | 动态 |
ZONE_MOVABLE 是现代内核为支持内存热插拔和页面碎片整理而引入的虚拟 zone,不对应物理内存范围。
1.2 伙伴系统(Buddy System)
伙伴系统是内核管理连续物理页面的核心算法,它将页面按 2 的幂次划分为 11 个连续页面块链表(order 0~10),每个 order 包含 2n 个连续页面。分配时从匹配的 order 链表取出,不足时向更大的 order 分裂;释放时检查 buddy(相邻同 order 页面块)是否空闲,若空闲则合并为更大的 order 块。
C 代码层面,核心函数位于 mm/page_alloc.c:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
void __free_pages(struct page *page, unsigned int order);
// GFP 标志决定分配行为
GFP_KERNEL // 普通内核分配,可能睡眠
GFP_ATOMIC // 原子分配,不睡眠(中断上下文)
GFP_USER // 用户态分配
GFP_NOWAIT // 不等待,不触发回收
GFP_NOFS // 不触发文件系统操作
二、虚拟内存与进程地址空间
2.1 页表层次结构
x86_64 架构采用 4 级页表:PGD -> P4D -> PUD -> PMD -> PTE。从 Linux 5.11 内核开始支持 5 级页表(增加 P4D 级别),可将虚拟地址空间从 256TB 扩展到 128PB。每个进程的页表由 struct mm_struct 管理,包含 pgd 基址等关键信息。页表条目中除了物理地址外,还包含权限位:R/W(读写)、U/S(用户/内核)、PWT/PCD(缓存策略)、NX(不可执行,自 Linux 2.6.8 起支持)。
2.2 Lazy Allocation 机制
现代 OS 中,mmap/malloc 分配内存时并不会立即分配物理页面。内核仅创建 VMA(Virtual Memory Area)记录映射关系,直到实际访问时触发 page fault,再由缺页中断分配物理页面。这种惰性分配(Lazy Allocation)策略在 do_anonymous_page() 和 do_fault() 中实现:第一次写触发 minor fault,文件映射未缓存时触发 major fault。优化建议:madvise(MADV_HUGEPAGE) 指示分配大页,madvise(MADV_DONTNEED) 释放物理页面。
三、SLAB/SLUB/SLOB 分配器详解
3.1 SLAB 分配器工作原理
SLAB 最初由 Jeff Bonwick 为 Solaris 设计,Linux 自 2.2 内核引入。核心思想:预分配对象缓存,避免频繁的构造/析构开销和内存碎片。每个 kmem_cache 管理一种大小对象的缓存。每个 slab 是连续的 1~N 个页面,内部被划分为等大小 slot。空闲对象通过 freelist 链表管理,或嵌入式 free 数组跟踪状态。CPU per-cpu 缓存(cpu_cache)确保每个 CPU 有自己的本地缓存,避免锁争用和缓存行 bouncing。
3.2 SLUB:现代默认分配器
SLUB 自 Linux 2.6.23 起成为默认分配器,是 SLAB 的简化版本。关键特性:freelist 用第一个空闲 slot 的指针链式连接(无额外元数据);每个 CPU 缓存只需维护一个 page 指针和一个 freelist 指针(极轻量);NUMA 本地分配优先从 Node-local slab 分配。
SLUB 的核心数据结构:
// mm/slub.c 中的核心结构
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // Per-CPU 滑动指针
struct kmem_cache_node **node; // NUMA 节点数组
unsigned int size; // 对象总大小
unsigned int object_size; // 用户请求大小
struct kmem_cache_order_objects oo; // min/low/high order
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表
unsigned long tid; // 事务ID(防竞态)
struct page *page; // 当前活跃页
struct page *partial; // CPU-local 部分满页
};
3.3 SLUB 分配性能分析
| 场景 | 路径 | 时间 |
|---|---|---|
| fast path | cpu_slab->freelist 非空 | ~10ns |
| slow path | 从伙伴系统申请新 slab | ~1μs |
| 最坏情况 | 跨 NUMA 节点分配 | ~5μs |
3.4 SLUB 调试与监控
SLUB 提供丰富的调试和监控接口:slabinfo 查看缓存统计;debug 选项通过 red zone、poison、tracking 等检测内存越界和 UAF;sysfs 调整参数;/proc/slabinfo 查看每个缓存的活跃对象数和空闲对象数。
四、vmalloc vs kmalloc vs kmap
三种内核内存分配方式对比:
| 特性 | kmalloc | vmalloc | kmap |
|---|---|---|---|
| 连续性 | 物理连续 | 虚拟连续 | - |
| 限制 | ≤128KB | 可达 | 单个大页 |
| 睡眠 | GFP 决定 | 可能睡眠 | 不睡眠 |
| TLB 效率 | 高 | 低(刷新) | - |
| 性能 | 快 | 慢 | 中 |
使用场景:kmalloc 用于 DMA 缓冲区、小结构体分配、中断上下文(GFP_ATOMIC);vmalloc 用于大内存分配(>128KB)、模块加载、ioremap;kmap 用于访问 HIGHMEM 页面(x86_64 不常用)。
五、内存回收与 OOM Killer
5.1 页面回收算法
Linux 采用 LRU(Least Recently Used)列表来管理页面回收。自内核 2.6 起,采用 Active/Inactive 双列表机制,页面首次在 Inactive,被访问后晋升到 Active,未被访问则降级回 Inactive,最终被回收。kswapd 守护进程负责后台回收,当空闲页面低于 high watermark 时唤醒回收到 low watermark。
5.2 Cgroup v2 内存控制器
cgroup v2 提供精细的 memory 控制:memory.min(最小保护,不会被回收)、memory.low(尽力保护,仅在其他 cgroup 也紧张时回收)、memory.max(硬限制,超限触发 OOM)、memory.high(软限制,短暂超限以节流)、memory.swap.max(交换限制)。例如,echo 2G > /sys/fs/cgroup/myapp/memory.max 设置 2GB 硬限制。
5.3 OOM Killer 评分算法
OOM Killer 根据 oom_badness 评分选择牺牲进程,公式为:进程内存占用(RSS+swap+页表)× 10oom_score_adj/1000。root 进程有 3% bonus;长时间运行进程分数降低(badness *= t_running/3600+1);可通过 echo -1000 > /proc/[pid]/oom_score_adj 保护 systemd 等关键服务。
实践中越来越多地用 cgroup v2 的 memory.max 限制替代全局 OOM Killer,避免系统级不稳定。还可以在 /proc/sys/vm/oom_kill_allocating_task=1 直接杀死触发进程。
六、Huge Pages 与透明大页
6.1 静态大页(Static Huge Pages)
静态大页在启动时通过 hugetlbfs 分配,预分配确定数量的大页。x86_64 支持 2MB 和 1GB 两种大页。配置方法:grub 参数 default_hugepagesz=2M hugepagesz=2M hugepages=256 预分配 256 个 2MB 大页。使用 mmap + MAP_HUGETLB 或 hugetlbfs 挂载。适用于 DPDK 数据包处理、KVM 虚拟机、HPC 计算、数据库(PostgreSQL shared_buffers)。
6.2 透明大页(Transparent Huge Pages)
THP 由 khugepaged 守护进程自动将连续的 4KB 页面合并为 2MB 大页,应用无需修改代码。配置选项:always(全局启用)、madvise(仅对 madvise(MADV_HUGEPAGE) 区间生效)、never(完全关闭)。
重要提示:许多数据库(如 MongoDB、Oracle)推荐关闭 THP。启动延迟大,合并延迟可达 10s;合并后失去细粒度控制;内存碎片可能造成分配失败。最佳实践:数据库/大数据 workload 关闭 THP 改用静态大页;应用代码中使用 madvise(MADV_HUGEPAGE) 精确控制。
七、性能分析工具与实战案例
7.1 常用工具
| 工具 | 用途 | 示例 |
|---|---|---|
| free/vmstat | 内存概览 | vmstat 1 |
| sar -R | 页面回收速率 | sar -R 1 5 |
| slabtop | 实时 slab 缓存 | slabtop -s c |
| numastat | NUMA 分布 | numastat -p [pid] |
| perf | 内存访问分析 | perf mem record/report |
| /boot/config | 内核配置 | 查看 CONFIG_HUGETLB_PAGE 等 |
7.2 生产环境实战
场景一:MySQL 大内存 + 混合负载
mysqld 使用 64GB 大内存,开启 8192 个静态大页(2MB 页),设置 NUMA 交错、vm.zone_reclaim_mode=0、vm.dirty_ratio=20、THP 关闭,memory cgroup 设置 80GB 上限。
场景二:Java 堆外内存失控
Direct ByteBuffer 分配导致 RSS 远大于堆 + 页缓存。使用 NativeMemoryTracking(-XX:NativeMemoryTracking=detail -XX:+PrintNMTStatistics),确认是 Direct ByteBuffer 还是 Thread Stack,使用 jcmd <pid> VM.native_memory scale=MB 定位 root cause。可能原因:Direct ByteBuffer 池泄漏、大量线程栈(-Xss1M 默认)、NIO Selector 未释放。
场景三:容器 OOM 频繁
Pod memory limit = 2GB,应用 RSS 仅 1.2GB 但被 OOM Kill。root cause:shm(共享内存页计入 RSS)、页表内存(4GB 地址空间理论最大 4MB PTE)、slab 缓存(dentry/inode cache 可回收但仍会计入 cgroup 统计)。
八、前沿趋势
1. MGLRU(Multi-Gen LRU)
Linux 6.1 引入,取代 Active/Inactive 的年龄模拟方法。通过 per-generation 访问频率模拟更准确的页面热度识别,显著降低缓存命中率损失,对于大内存节点效果尤佳。
2. DAMON(Data Access MONitor)
Linux 5.15+ 支持,不需要修改应用,通过采样模式监测每个内存区域的访问频率。可用于主动回收冷内存、NUMA 数据迁移、主动 THP 合并。目标是 0% overhead 的粗粒度监控 + 应用级 hint 互补。
3. Rust 重写内存管理模块
Google 正用 Rust 为 Android 开发新的 Low Memory Killer,利用 Ownership 模型减少 UAF 和 double-free 类型漏洞。Linux 内核 RISC-V 架构已引入 Rust Binder IPC 驱动。长期看,内存管理的安全性会从语言层面得到改进。
4. PTE 折叠与内存去重
AMD Zen 4 和 Intel Sapphire Rapids 上启用 PTE 折叠(将多个连续 PTE 合并为一个),减少 TLB miss。KSM(Kernel Samepage Merging)已用于 KVM 场景,但开销大。新一代方案如 TMO(Tiered Memory Optimization)结合温度监控和 CXL 扩展内存,自动冷热页面分层。
总结
Linux 内存管理是操作系统的"心脏"。从伙伴系统到 SLUB 分配器,从 LRU 页面回收到 MGLRU,从静态大页到 THP,每一层都有其设计哲学和适用场景。性能调优的首要原则永远是:先测量,再分析,最后优化。希望本文能帮助读者建立起对 Linux 内存子系统全景式的认知框架。

发表评论 取消回复