Linux 内核的内存管理子系统是整个操作系统最核心、最复杂的组件之一。理解其原理和机制,对于系统调优、性能分析和内核开发都至关重要。

一、物理内存管理架构

Linux 内核的物理内存管理采用 buddy system(伙伴系统)与 slab/slub/slob 分配器协作的经典架构。物理内存的基本单位是页框(page frame),大小通常为 4KB。内核使用 struct page 结构体来跟踪系统中的每一个物理页面。

1.1 节点(node)与 zone

NUMA 架构下,内存被划分为多个节点,每个节点包含若干 zone。典型的 zone 分为:

Zone 类型说明x86_64 典型大小
ZONE_DMAISA 设备 DMA 用0-16MB
ZONE_DMA3232-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 pathcpu_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

三种内核内存分配方式对比:

特性kmallocvmallockmap
连续性物理连续虚拟连续-
限制≤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
numastatNUMA 分布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 内存子系统全景式的认知框架。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }