当前位置:首页>Linux>Linux 内存管理全景:虚拟内存、MMU、页表、TLB 与 huge page 实战

Linux 内存管理全景:虚拟内存、MMU、页表、TLB 与 huge page 实战

  • 2026-09-29 20:28:36
Linux 内存管理全景:虚拟内存、MMU、页表、TLB 与 huge page 实战

前言

你写了一个 malloc(1GB),内核真的立即给了你 1GB 物理内存吗?不,它只是画了一张"地图"。真正分配物理页是当你访问那些地址的时候。

Linux 内存管理是操作系统最复杂的子系统之一。从虚拟地址到物理地址、从 4KB 小页到 1GB 大页、从 TLB 命中到缺页中断——每一层设计都在平衡灵活性与性能。


PART 01:虚拟内存 —— 每个进程的"幻觉"

每个进程都以为自己独占了整个地址空间:

32位进程 64位进程0x00000000 ┌─────────┐ 0x0000000000000000 ┌──────────────┐ │ 代码+数据 │ │ 代码+数据 │ ├─────────┤ ├──────────────┤ │ 堆 │ │ 堆 │ ├─────────┤ ├──────────────┤ │ ↓ │ │ ↓ │ │ 空闲区域 │ │ 空闲区域 │ │ ↑ │ │ ↑ │ ├─────────┤ ├──────────────┤ │ 栈 │ │ 栈 │0xFFFFFFFF └─────────┘ 0x7FFFFFFFFFFF └──────────────┘

虚拟内存的好处

  1. **隔离**:进程 A 不能访问进程 B 的内存
  2. **简化**:每个进程看到连续的地址空间,不需要关心物理碎片
  3. **共享**:共享库的物理内存只有一份,映射到多个进程的虚拟地址
  4. **惰性分配**:分配虚拟地址不消耗物理内存,用到才给

    # 查看进程虚拟内存布局cat /proc//mapscat /proc//smaps# 更详细(含各段实际物理内存使用)

    PART 02:MMU 与页表

    地址转换

    虚拟地址 0x7F123456 ↓CPU → MMU(内存管理单元) ↓查页表(Page Table) ↓物理地址 0x3A000456

    四级页表(4-Level Paging)

    虚拟地址 (64位)┌──────┬──────┬──────┬──────┬──────────┐│ PGD │ PUD │ PMD │ PTE │ 偏移量 ││ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │└──┬───┴──┬───┴──┬───┴──┬───┴──────────┘ ↓ ↓ ↓ ↓ PGD → PUD → PMD → PTE → 物理页 (4KB)

    每个页表项(PTE)映射 4KB 物理页。一个 64 位进程的完整页表,光是页表本身就可能占用几百 MB 内存。

    5-Level Paging(内核 5.0+,Intel Ice Lake+):再多一级,支持 512PB 地址空间:

    PGD → P4D → PUD → PMD → PTE

    PART 03:TLB —— 地址转换的缓存

    为什么需要 TLB

    每次内存访问都需要 MMU 查页表,而查页表本身需要多次内存访问(4 级页表 = 4 次内存访问)。TLB 缓存最近使用的虚拟→物理地址映射。

    没有 TLB: 每次内存访问 → 4 次页表查询 → 400ns有 TLB(命中): 每次内存访问 → 1 次 TLB 查询 → ~1ns

    TLB Miss 的成本

    场景
    延迟
    相对比较
    L1 TLB Hit
    ~1ns
    1x
    L2 TLB Hit
    ~7ns
    7x
    TLB Miss(页表遍历)
    ~80ns
    80x
    TLB Miss + 缺页中断
    ~10us
    10000x

    大页(Huge Page)降低 TLB Miss

    4KB 小页: 1GB 数据 → 262144 个页 → TLB 需要 262144 个条目 TLB 只有 ~50 个条目 → 疯狂 Miss2MB 大页: 1GB 数据 → 512 个大页 → TLB 只占 512 个条目 Miss 大幅减少1GB 大页: 1GB 数据 → 1 个巨页 → TLB 只需要 1 个条目

    PART 04:Huge Page 实战

    透明大页(THP)

    内核自动将 4KB 页合并为 2MB 大页。但 THP 有两大问题:

    1. **khugepaged 线程**:扫描内存并合并,消耗 CPU
    2. **内存碎片**:分配不到连续 2MB 时回退到 4KB,导致延迟抖动

    内核自动将 4KB 页合并为 2MB 大页。但 THP 有两大问题:

      # 查看 THP 状态cat /sys/kernel/mm/transparent_hugepage/enabled# always [madvise] never# always:内核自动合并所有区域(默认)# madvise:只在 madvise(MADV_HUGEPAGE) 区域使用# never:禁用# 数据库场景建议 neverecho never > /sys/kernel/mm/transparent_hugepage/enabled# 或 madvise(只在显式指定的区域使用)echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

      显式大页(HugeTLB)

      手动预留物理内存作为大页,应用通过 mmap 或 shmget 使用:

      # 预留 1024 个 2MB 大页echo 1024 > /proc/sys/vm/nr_hugepages# 查看预留情况cat /proc/meminfo | grep -i hugeHugePages_Total: 1024HugePages_Free: 768HugePages_Rsvd: 256HugePages_Surp: 0# 挂载 hugetlbfsmount -t hugetlbfs none /mnt/huge

      MySQL/PostgreSQL 使用大页

      # MySQL 8.0# 确保已预留大页echo 4096 > /proc/sys/vm/nr_hugepages# MySQL 配置large-pages=ON# MySQL 会在启动时锁定大页# PostgreSQL# PG 13+ 支持大页huge_pages = on# PG 会自动通过 mmap 使用大页

      使用大页的性能提升

      使用大页的性能提升

      场景
      4KB 页
      2MB 页
      提升
      Redis GET 吞吐
      120K QPS
      145K QPS
      +21%
      MySQL OLTP 延迟
      2.1ms
      1.5ms
      -29%
      PG 只读查询
      8500 TPS
      10200 TPS
      +20%


      PART 05:缺页中断

      惰性分配的全过程

      ptr = malloc(1GB); // 只分配了虚拟地址,没有物理页ptr[0] = 'a'; // 访问这个地址 → CPU 触发缺页中断 // 内核分配物理页 → 建立页表映射 // 返回用户态 → 重新执行访问指令
      # 查看缺页统计cat /proc//stat | awk '{print $10, $12}'# majflt minflt# majflt:主缺页(需要磁盘 IO,慢!)# minflt:次缺页(不需要磁盘 IO,快)

      主缺页 vs 次缺页

      主缺页 vs 次缺页

      类型
      含义
      延迟
      优化
      **次缺页**
      页在内存但未建立映射
      ~1us
      正常
      **主缺页**
      页不在内存,需要从磁盘加载
      ~10ms
      预读、mlock

      # 锁定进程的内存页面防止被换出mlockall(MCL_CURRENT | MCL_FUTURE);# 配置 MySQLmemlock# my.cnf 中设置,防止被 swap

      PART 06:Page Fault 排查

      谁在触发大量缺页

      # 按进程查看缺页次数ps -eo pid,minflt,majflt,cmd --sort=-majflt | head# 实时监控缺页perf stat -e page-faults,major-faults,minor-faults -p

      预读(read-ahead)

      内核检测到顺序读模式时会提前加载更多页:

      # 查看预读参数cat /sys/block/nvme0n1/queue/read_ahead_kb# 默认 128KB# 大文件顺序读场景调高echo 2048 > /sys/block/nvme0n1/queue/read_ahead_kb

      PART 07:内存性能排查工具箱

      PART 07:内存性能排查工具箱

      工具
      作用
      示例
      `/proc/meminfo`
      全局内存使用
      `grep -E "MemTotal
      MemFree
      Cached
      Huge" /proc/meminfo`
      `/proc/<PID>/smaps`
      进程内存详细分布
      `cat /proc/<PID>/smaps \
      grep Pss`
      `numastat`
      NUMA 内存分配统计
      `numastat -p <PID>`
      `perf stat -e TLB-misses`
      TLB Miss 率
      `perf stat -e dTLB-load-misses,iTLB-load-misses -a sleep 5`
      `vmstat 1`
      缺页和 swap 统计
      `vmstat 1`(si/so 列有值说明在 swap)
      `slabtop`
      内核 slab 缓存
      `slabtop -o`


      总结

      内存管理是”层层缓存”的设计:CPU 寄存器 ↓ (~1ns)L1/L2/L3 Cache ↓ (~5-40ns)TLB(地址转换缓存) ↓ (~1ns 命中 / 80ns Miss)页表(4 级 / 5 级) ↓物理内存 (DRAM) ↓ (~100ns)Swap / 磁盘 ↓ (~10ms)
      1. **大页**是降低 TLB Miss 最有效的武器
      2. **THP** 有 CPU 开销,数据库场景建议禁用
      3. **HugeTLB** 显式大页适合可预测的内存分配
      4. **缺页**不可避免,但可以减少主缺页

      预告:《OOM Killer 深度剖析:oom_score、oom_adj、cgroup 内存限制与内存超卖》当内存耗尽,内核如何决定杀谁?

      最新文章

      随机文章