---
title: 虚拟内存
url: https://doc.liz6.com/linux-kernel/02-memory-management/01-virtual-memory
locale: zh
area: linux-kernel
tags:
- linux-kernel
- 内存管理
date: 2026-06-30
modified: 2026-07-11
description: '覆盖: 页表遍历 (4-level/5-level) → PTE 格式 → TLB 机制 → HugeTLB/THP → KPTI 隔离 内核版本: 2.6 ~ 6.x，重点标注 5-level paging 和 mTHP 演进'
---

# 虚拟内存

> 覆盖: 页表遍历 (4-level/5-level) → PTE 格式 → TLB 机制 → HugeTLB/THP → KPTI 隔离
> 内核版本: 2.6 ~ 6.x，重点标注 5-level paging 和 mTHP 演进

## 概述

虚拟内存是 Linux 内存管理的基石。它将每个进程隔离在自己的地址空间中，同时通过页表实现延迟分配 (demand paging)、写时复制 (COW)、内存共享 (mmap MAP_SHARED) 和 swap。

理解虚拟内存的关键在于理解**页表遍历**——CPU 如何把一个虚拟地址转换成物理地址。这个过程发生在每次内存访问中（由 MMU 硬件完成），速度至关重要。

---

## 页表结构

### x86-64 4-level paging (48-bit)

```mermaid
flowchart LR
    VA["虚拟地址 (48-bit)"]

    VA --> PGD["PGD/PML4 索引<br/>[47:39] 9 bit<br/>512 entries"]
    PGD -->|"CR3 →"| PUD["PUD 索引<br/>[38:30] 9 bit<br/>512 entries"]
    PUD --> PMD["PMD 索引<br/>[29:21] 9 bit<br/>512 entries"]
    PMD --> PTE["PTE 索引<br/>[20:12] 9 bit<br/>512 entries"]
    PTE --> PAGE["4KB 物理页<br/>offset [11:0] 12 bit"]

    classDef va fill:#e3f2fd,stroke:#1565c0
    classDef level fill:#f3e5f5,stroke:#7b1fa2
    classDef phys fill:#e8f5e9,stroke:#2e7d32
    class VA va
    class PGD,PUD,PMD,PTE level
    class PAGE phys
```

为什么每级正好 9 bit (512 entries)? 因为每个 PTE 是 8 bytes，512 × 8 = 4KB = 正好一个物理页。这种自相似的设计使得页表本身可以用页面分配器管理。

### 5-level paging (57-bit, kernel 4.14+)

```mermaid
flowchart LR
    VA5["虚拟地址 (57-bit)"]
    VA5 --> P5E["PML5 索引<br/>[56:48] 9 bit<br/>512 entries"]
    P5E --> P4D["P4D 索引<br/>[47:39] 9 bit<br/>512 entries"]
    P4D --> PUD5["PUD 索引<br/>[38:30] 9 bit"]
    PUD5 --> PMD5["PMD 索引<br/>[29:21] 9 bit"]
    PMD5 --> PTE5["PTE 索引<br/>[20:12] 9 bit"]
    PTE5 --> PAGE5["4KB 物理页<br/>offset [11:0]"]

    classDef va fill:#e3f2fd,stroke:#1565c0
    classDef level fill:#f3e5f5,stroke:#7b1fa2
    classDef phys fill:#e8f5e9,stroke:#2e7d32
    class VA5 va
    class P5E,P4D,PUD5,PMD5,PTE5 level
    class PAGE5 phys
```

需要 CPU 支持 LA57 feature (Ice Lake+)
检查: `grep la57 /proc/cpuinfo`

5-level 将虚拟地址空间从 256TB (128T user + 128T kernel) 扩展到 128PB。不是为今天的应用准备的——是为未来的 CXL 共享内存池和 PB 级 NVDIMM。

### 大页如何减少层级

```mermaid
flowchart LR
    subgraph HUGE["大页遍历层级"]
        direction LR
        A["PGD"] --> B["PUD"]
        B -->|"2MB: PS=1"| C["PMD → 2MB 大页 ✅"]
        B -->|"1GB: PS=1"| D["PUD → 1GB 大页 ✅"]
    end

    subgraph NORMAL["4KB 页遍历"]
        direction LR
        E["PGD"] --> F["PUD"] --> G["PMD"] --> H["PTE → 4KB"]
    end

    classDef huge fill:#e8f5e9,stroke:#2e7d32
    classDef normal fill:#e3f2fd,stroke:#1565c0
    class HUGE huge
    class NORMAL normal
```

在 PMD/PUD entry 中: bit 7 (PS bit) = 1 → 不再往下走，这一级就是最终物理地址

### 页表项 (PTE) 格式

```
x86-64 PTE (简化, 64 bit):
  bit 0:   Present       → 页在物理内存中 (1) 还是被换出/未映射 (0)
  bit 1:   R/W           → 可写 (1) 还是只读 (0) — COW 的关键!
  bit 2:   U/S           → User 可访问 (1) 还是 kernel only (0)
  bit 3:   PWT           → Page Write-Through caching
  bit 4:   PCD           → Page Cache Disable
  bit 5:   Accessed      → 硬件自动设 1 (页面被访问过)
                          → 回收的关键: 没访问过的页面是 swap 首选
  bit 6:   Dirty         → 硬件自动设 1 (页面被写过)
                          → 脏页 swap 时需写回，干净页直接丢弃
  bit 7:   PAT / PS      → PTE 级: PAT (Page Attribute Table)
                          → PMD/PUD 级: PS (Page Size, 指示大页)
  bit 8:   Global        → CR3 切换时不刷 (用于内核页表)
  bit 11:  NX bit 63     → No-Execute (XD), 不可执行
  bit 63:  NX            → 同 bit 11 的别名

物理地址: bits [51:12] → 指向 4KB 对齐的物理页
  其余 bits [11:0] 是 offset，不参与地址转换
```

Accessed 和 Dirty 位是内存回收的硬件加速器。没有它们，内核需要用 page fault 来模拟"是否被用过"，成本高数百倍。

---

## TLB (Translation Lookaside Buffer)

### 基本概念

```
TLB = MMU 内部的页表 cache (硬件)
  L1 TLB: ~64 entries, 1 cycle 延迟 (指令和数据分开)
  L2 TLB: ~1500 entries, ~5 cycles

TLB miss:
  1. 硬件遍历页表 (x86: hardware page walker, 4 次内存访问)
  2. 如果遍历中间任何一级的 Present=0 → page fault → 内核处理
  3. 找到 PTE 后，加载到 TLB

TLB 覆盖率:
  4KB pages:  L2 TLB 1500 entries × 4KB = 6MB 覆盖
  2MB pages:  L2 TLB 1500 entries × 2MB = 3GB 覆盖
  1GB pages:  32 entries (L1 only) × 1GB = 32GB 覆盖
```

### PCID / ASID

```
问题: 上下文切换时写 CR3 → TLB 中所有 non-global entries 失效
       → 新进程的每个内存访问都是 TLB miss → 严重性能损失

解决方案:
  x86 PCID (Process-Context Identifier, 12-bit):
    写 CR3 时附上 ASID → TLB 中不同 ASID 的 entries:
      - 不会被刷掉 (保留在 TLB 中)
      - 不会误命中 (ASID 不匹配)
    → 热进程切换回来后，TLB 仍然大部分命中

  ARM64 ASID (8-bit 或 16-bit):
    类似原理，TTBR0_EL1 写入时指定 ASID
    硬件区分不同 ASID 的 TLB entries

  内核态页表 (Global bit):
    kernel 页表的 PTE 设 Global bit → CR3 切换不刷
    → 内核态 TLB entries 在不同进程间共享
```

### TLB Shootdown

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="TLB Shootdown:多核 TLB 一致性协调流程">
  <defs>
    <marker id="tlbah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="26" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">TLB Shootdown:多核 TLB 一致性协调流程</text>

  <rect x="40" y="40" width="300" height="26" rx="6" fill="#4f46e5"/>
  <text x="190" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">CPU 0 · 发起核</text>
  <rect x="380" y="40" width="300" height="26" rx="6" fill="#0d9488"/>
  <text x="530" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">其他 CPU · 目标核</text>

  <rect x="52" y="76" width="276" height="34" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="91" font-size="12" font-weight="700" fill="#3730a3">1. 修改页表</text>
  <text x="66" y="104" font-size="11" fill="#4f46e5">如 munmap() / mprotect()</text>

  <rect x="52" y="116" width="276" height="34" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="131" font-size="12" font-weight="700" fill="#3730a3">2. 本地立即刷 TLB</text>
  <text x="66" y="144" font-size="11" fill="#4f46e5">INVLPG 指令 或 重写 CR3</text>

  <rect x="392" y="76" width="276" height="74" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="530" y="105" text-anchor="middle" font-size="12" fill="#334155">仍执行原任务</text>
  <text x="530" y="121" text-anchor="middle" font-size="11" fill="#64748b">尚未感知页表已变更</text>
  <text x="530" y="137" text-anchor="middle" font-size="11" fill="#64748b">(TLB 中仍缓存旧 PTE)</text>

  <rect x="52" y="160" width="276" height="38" rx="6" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="66" y="176" font-size="12" font-weight="700" fill="#3730a3">3. 发送 IPI 广播</text>
  <text x="66" y="190" font-size="11" fill="#4f46e5">Inter-Processor Interrupt</text>

  <rect x="392" y="160" width="276" height="38" rx="6" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="406" y="176" font-size="12" font-weight="700" fill="#115e59">收到 IPI 中断</text>
  <text x="406" y="190" font-size="11" fill="#0f766e">暂停当前执行流程</text>

  <line x1="328" y1="179" x2="390" y2="179" stroke="#475569" stroke-width="1.7" marker-end="url(#tlbah)"/>
  <text x="359" y="172" text-anchor="middle" font-size="10" fill="#64748b">IPI</text>

  <rect x="52" y="204" width="276" height="38" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="220" font-size="12" font-weight="700" fill="#3730a3">阻塞等待</text>
  <text x="66" y="234" font-size="11" fill="#4f46e5">flush_tlb_mm_range() 未返回</text>

  <rect x="392" y="204" width="276" height="38" rx="6" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="406" y="220" font-size="12" font-weight="700" fill="#115e59">中断上下文中刷 TLB</text>
  <text x="406" y="234" font-size="11" fill="#0f766e">清除该 mm 的失效 PTE 缓存</text>

  <rect x="392" y="248" width="276" height="34" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="530" y="263" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">回复完成(ACK)</text>
  <text x="530" y="277" text-anchor="middle" font-size="11" fill="#15803d">通知发起核已处理完成</text>

  <line x1="190" y1="242" x2="230" y2="290" stroke="#475569" stroke-width="1.7" marker-end="url(#tlbah)"/>
  <line x1="530" y1="282" x2="490" y2="290" stroke="#475569" stroke-width="1.7" marker-end="url(#tlbah)"/>

  <rect x="140" y="292" width="440" height="36" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="360" y="307" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">所有目标 CPU 回复完成</text>
  <text x="360" y="321" text-anchor="middle" font-size="11" fill="#15803d">flush_tlb_mm_range() 返回,各核 TLB 状态一致</text>

  <rect x="28" y="338" width="216" height="34" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="136" y="353" text-anchor="middle" font-size="11.5" font-weight="700" fill="#334155">Lazy TLB</text>
  <text x="136" y="367" text-anchor="middle" font-size="10" fill="#64748b">目标在内核线程(无用户态)→ 延迟刷</text>

  <rect x="252" y="338" width="216" height="34" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="360" y="353" text-anchor="middle" font-size="11.5" font-weight="700" fill="#334155">Batching</text>
  <text x="360" y="367" text-anchor="middle" font-size="10" fill="#64748b">合并多次 flush → 一次 IPI</text>

  <rect x="476" y="338" width="216" height="34" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="584" y="353" text-anchor="middle" font-size="11.5" font-weight="700" fill="#334155">硬件广播</text>
  <text x="584" y="367" text-anchor="middle" font-size="10" fill="#64748b">AMD INVLPGB / Intel TLB broadcast</text>

  <rect x="30" y="376" width="660" height="40" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="44" y="392" font-size="11" fill="#115e59">为什么必须发 IPI:仅刷本地 TLB 不够——其他核仍缓存旧 PTE,</text>
  <text x="44" y="407" font-size="11" fill="#115e59">不广播通知就会继续用旧翻译,造成核间内存视图不一致甚至安全问题。</text>
</svg>

---

## HugeTLB

预留大块连续物理内存，提供确定的大页映射：

```c
// 配置
echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
// 或 boot param: hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=1024

// 使用方式 1: hugetlbfs
mount -t hugetlbfs -o pagesize=2M none /dev/hugepages
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_HUGETLB, fd, 0);

// 使用方式 2: mmap flag
mmap(NULL, size, PROT_READ|PROT_WRITE,
     MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);

// QEMU/libvirt 使用 HugeTLB 为 VM 映射 guest RAM
// → 减少嵌套页表 (stage-2 page table) 的 TLB miss
```

HugeTLB 的优点：确定可用（预留时分配）、TLB 效率高。缺点：需要连续物理内存、不可 swap（5.x 之前）、小数据浪费整页。

---

## THP (Transparent Huge Pages)

### 机制

内核自动将连续的 4KB 页合并为 2MB 大页，对用户透明：

```
分配:
  进程 malloc(128KB) → 触发 page fault → do_anonymous_page()
    → 如果 VM 支持 THP 且对齐 → 尝试分配 2MB 零页
    → 成功: 整个 2MB 用一个大页映射 (减少后续 512 个 page faults)
    → 失败: 回退到 4KB 页

khugepaged:
  后台内核线程 → 周期性扫描进程地址空间
    → 发现可以合并的连续 4KB 页 → 迁移/合并 → 升级为 2MB THP
    → 发现 THP 内大部分页被释放 → 分裂为 4KB 页
    → 扫描通过 /sys/kernel/mm/transparent_hugepage/khugepaged/ 控制
```

### 策略控制

```bash
cat /sys/kernel/mm/transparent_hugepage/enabled
# always:  总是尝试分配 THP (默认)
# madvise: 仅 MADV_HUGEPAGE 标记的 VMA
# never:   禁用

# 应用级控制:
madvise(ptr, size, MADV_HUGEPAGE);   // 建议使用 THP
```

### THP 的代价

```
优点:
  + TLB miss 率大幅降低 (512 倍覆盖率)
  + page fault 减少 (一次 allocation 512 个页)
  + 内核页表遍历减少一级

缺点:
  - 分配延迟 (需要找 2MB 连续物理内存 → 可能需要 compact/reclaim)
  - 内部碎片 (3KB 有效数据占 2MB 物理内存)
  - 某些 workload 变慢 (如某些数据库自己管理大页)

这就是为什么 DBA 经常建议: echo never > transparent_hugepage/enabled
```

### mTHP (multi-size THP, 6.12+)

```
正在演进的方案: 允许 THP 的大小不是固定 2MB
  → 4KB / 16KB / 32KB / 64KB / 128KB / 256KB / 512KB / 1MB / 2MB
  → 按需选择合适的大小
  → 减少内部碎片 (不需要整个 2MB)
  → 保留 TLB 效率 (比 4KB 好很多)

这是内核社区正在活跃开发的领域
```

---

## KPTI: 页表隔离

### Meltdown 与修复

```
Meltdown (2018):
  CPU 在推测执行中不检查 U/S bit → 用户态可以推测读取内核内存
  → cache 中的残留数据可以通过 side channel 读取

KPTI (Kernel Page Table Isolation):
  x86: PTI (Page Table Isolation)
  ARM: KPTI (Kernel Page Table Isolation)

  每个进程有**两套**页表:
    内核态页表: 完整映射 (user + kernel 地址)
    用户态页表: 最小映射 (user + 仅 trampoline)

  trampoline (入口蹦床):
    只包含 entry_SYSCALL_64 / iret / 中断描述符表 (IDT)
    → 足够进入内核态 → 进入后立即切换到内核态页表
    → syscall 返回前切回用户态页表 → 然后 iret
```

### 性能影响

```
syscall 密集型负载: ~5%~30% 性能退化
  每次 syscall: 2 次 CR3 写 + 2 次 TLB miss (trampoline 太小)
  缓解:
    - PCID: 避免完全刷 TLB (但 CR3 写本身开销还在)
    - 非 Meltdown 影响的 CPU (AMD / newer Intel): KPTI 自动禁用
```

---

## 调试接口

```bash
# 完整页表遍历 (需要 root)
cat /proc/<pid>/pagemap  # 二进制，每页 8 bytes
# bit 63: page present
# bits [54:0]: PFN (物理页号)

# 检查 THP 使用情况
grep -i thp /proc/meminfo
# AnonHugePages: THP 用于匿名内存
# ShmemHugePages: THP 用于 shmem

# 检查是否启用 5-level paging
grep la57 /proc/cpuinfo

# 大页信息
cat /proc/meminfo | grep -i huge
cat /sys/kernel/mm/hugepages/hugepages-*/nr_hugepages

# 页表大小 (per-process)
cat /proc/<pid>/status | grep VmPTE
```

---

## 参考与延伸

- **内核文档**: `Documentation/x86/x86_64/mm.rst`, `Documentation/vm/transhuge.rst`, `Documentation/x86/pti.rst`
- **LWN**: 
  - "5-level paging" (lwn.net/Articles/717293/)
  - "Transparent huge pages in 6.x" (lwn.net/Articles/971579/)
  - "The multi-size THP patch set" (lwn.net/Articles/931701/)
- **源码文件**:
  - `arch/x86/include/asm/pgtable.h` — PTE 格式定义
  - `arch/x86/mm/tlb.c` — TLB flush 实现
  - `mm/huge_memory.c` — THP 核心
  - `mm/hugetlb.c` — HugeTLB
  - `arch/x86/mm/pti.c` — KPTI

---

*关键词: 页表, PGD/PUD/PMD/PTE, PTE flags (A/D/RW/NX), TLB, PCID, ASID, HugeTLB, THP, khugepaged, mTHP, KPTI, Meltdown*
