Linux内核分析之内存管理-05

韩乔落

第5章:页表管理

基于 Linux 6.12.38 源码


5.1 多级页表结构

5.1.1 x86_64 页表结构

Linux 6.12 在 x86_64 上使用 4 级页表 (PGD -> P4D -> PUD -> PMD -> PTE),可选 5 级页表。

48位虚拟地址的页表层次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────────┐
│ 48位虚拟地址 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 63 47 │ 46-39 │ 38-30 │ 29-21 │ 20-12 │ 11-0 │
│ │ 符号扩展 │ PML4索引 │ PUD索引 │ PMD索引 │ PTE索引 │ 偏移 │
│ │ (全0/1) │ (9位) │ (9位) │ (9位) │ (9位) │ (12位) │
│ │
└─────────────────────────────────────────────────────────────────┘

CR3 (页目录基址) → PML4 (Page Map Level 4)
[511] ──→ PUD (Page Upper Directory)
[511] ──→ PMD (Page Middle Directory)
[511] ──→ PTE (Page Table Entry)
[511] ──→ 物理页面
+ 12位偏移

5.1.2 页表级别定义

位置: arch/x86/include/asm/pgtable_64_levels.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 4级页表 */
#define PGDIR_SHIFT 39
#define P4D_SHIFT 39
#define PUD_SHIFT 30
#define PMD_SHIFT 21
#define PAGE_SHIFT 12

#define PTRS_PER_PGD 512
#define PTRS_PER_P4D 1
#define PTRS_PER_PUD 512
#define PTRS_PER_PMD 512
#define PTRS_PER_PTE 512

#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT) /* 512GB */
#define PGDIR_MASK (~(PGDIR_SIZE - 1))

#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) /* 1GB */
#define PUD_MASK (~(PUD_SIZE - 1))

#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) /* 2MB */
#define PMD_MASK ~(PMD_SIZE - 1)

#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT) /* 4KB */
#define PAGE_MASK (~(PAGE_SIZE - 1))

/* 5级页表支持 */
#define CONFIG_PGTABLE_LEVELS 4 /* 可配置为 5 */

5.1.3 页表大小计算

1
2
3
4
5
6
7
8
页表大小 = 每个页表项大小 × 页表项数量

PGD: 8字节 × 512 = 4KB
PUD: 8字节 × 512 = 4KB
PMD: 8字节 × 512 = 4KB
PTE: 8字节 × 512 = 4KB

每个进程的页表大小 ≈ 16KB (不包括实际映射的页面)

5.2 页表项格式

5.2.1 PTE 格式 (x86_64)

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────────────────────┐
│ Page Table Entry │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 63 52 │ 51 40 │ 39 12 │ 11 0 │
│ │ 保留/NI │ 页表属性 │ PFN │ 标志位 │
│ │ │ (索引/Key) │ │ │
│ │
└─────────────────────────────────────────────────────────────────┘

5.2.2 PTE 标志位定义

位置: arch/x86/include/asm/pgtable_types.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 页表项位定义 */
#define _PAGE_BIT_PRESENT 0 /* 页存在 */
#define _PAGE_BIT_RW 1 /* 可写 */
#define _PAGE_BIT_USER 2 /* 用户空间 */
#define _PAGE_BIT_PWT 3 /* 页直写 */
#define _PAGE_BIT_PCD 4 /* 页禁用缓存 */
#define _PAGE_BIT_ACCESSED 5 /* 已访问 */
#define _PAGE_BIT_DIRTY 6 /* 已修改 */
#define _PAGE_BIT_PSE 7 /* 页大小扩展 */
#define _PAGE_BIT_GLOBAL 8 /* 全局页 */
#define _PAGE_BIT_SOFTW1 9 /* 软件可用位 */
#define _PAGE_BIT_SOFTW2 10 /* 软件可用位 */
#define _PAGE_BIT_SOFTW3 11 /* 软件可用位 */
#define _PAGE_BIT_PAT 7 /* 页属性表 (如果启用 PAT) */
#define _PAGE_BIT_PAT_LARGE 12 /* 页属性表 (大页) */
#define _PAGE_BIT_SPECIAL _PAGE_BIT_SOFTW1
#define _PAGE_BIT_CPA_TEST _PAGE_BIT_SOFTW1
#define _PAGE_BIT_UFFD_WP _PAGE_BIT_SOFTW2
#define _PAGE_BIT_SOFT_DIRTY _PAGE_BIT_SOFTW3
#define _PAGE_BIT_DEVMAP _PAGE_BIT_SOFTW1
#define _PAGE_BIT_PROTNONE _PAGE_BIT_SOFTW3

/* 页表标志 */
#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_RW (_AT(pteval_t, 1) << _PAGE_BIT_RW)
#define _PAGE_USER (_AT(pteval_t, 1) << _PAGE_BIT_USER)
#define _PAGE_PWT (_AT(pteval_t, 1) << _PAGE_BIT_PWT)
#define _PAGE_PCD (_AT(pteval_t, 1) << _PAGE_BIT_PCD)
#define _PAGE_ACCESSED (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED)
#define _PAGE_DIRTY (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY)
#define _PAGE_PSE (_AT(pteval_t, 1) << _PAGE_BIT_PSE)
#define _PAGE_GLOBAL (_AT(pteval_t, 1) << _PAGE_BIT_GLOBAL)
#define _PAGE_SOFTW1 (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW1)
#define _PAGE_SOFTW2 (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW2)
#define _PAGE_SOFTW3 (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW3)
#define _PAGE_PAT (_AT(pteval_t, 1) << _PAGE_BIT_PAT)
#define _PAGE_PAT_LARGE (_AT(pteval_t, 1) << _PAGE_BIT_PAT_LARGE)
#define _PAGE_SPECIAL _PAGE_SOFTW1
#define _PAGE_CPA_TEST _PAGE_SOFTW1
#define _PAGE_UFFD_WP _PAGE_SOFTW2

#define _PAGE_NX (_AT(pteval_t, 1) << _PAGE_BIT_NX)

/* 组合标志 */
#define _PAGE_FILE (_AT(pteval_t, 1) << _PAGE_BIT_FILE)
#define _PAGE_PROTNONE (_AT(pteval_t, 1) << _PAGE_BIT_PROTNONE)

#define _PAGE_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \
_PAGE_ACCESSED | _PAGE_DIRTY)
#define _PAGE_KERN_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \
_PAGE_DIRTY)

5.2.3 标志位功能表

名称 功能描述
0 P (Present) 页面在内存中
1 R/W 可写
2 U/S 用户可访问 (0=仅内核)
3 PWT 直写缓存
4 PCD 禁用缓存
5 A (Accessed) 已访问
6 D (Dirty) 已修改
7 PS 页大小 (1=4MB/2MB, 0=4KB)
8 G (Global) 全局页面 (TLB不刷新)
9-11 AVL 可用给软件使用
11 PAT 页属性表索引
52 XD 禁止执行 (NX bit)
63 PROT_NONE 保护位 (用于不可访问页面)

5.3 页表操作

5.3.1 查找页表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 查找 PGD */
static inline pgd_t *pgd_offset(struct mm_struct *mm, unsigned long address);
static inline pgd_t *pgd_offset_k(unsigned long address); /* 内核空间 */

/* 查找 P4D (5级页表) */
static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long address);

/* 查找 PUD */
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address);

/* 查找 PMD */
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address);

/* 查找 PTE */
static inline pte_t *pte_offset_map(pmd_t *pmd, unsigned long address);
#define pte_unmap(pte) kunmap(pte)

/* 遍历页表查找 PTE */
pte_t *pte_offset_map_nolock(struct mm_struct *mm, pmd_t *pmd,
unsigned long addr, pmd_t **pmdvalp);

5.3.2 设置页表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 设置 PTE */
static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte);

/* 清除 PTE */
static inline void pte_clear(struct mm_struct *mm, unsigned long addr,
pte_t *ptep);

/* 设置只读映射 */
static inline void set_pte_readonly(pte_t *ptep, pte_t pte);

/* 创建 PTE 映射 */
#define set_pte(ptep, pte) native_set_pte(ptep, pte)

/* 批量设置 PTE */
void set_ptes(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte, unsigned int nr);

5.3.3 创建页表

1
2
3
4
5
6
7
8
9
10
11
12
/* 分配页表 */
pgd_t *pgd_alloc(struct mm_struct *mm);
p4d_t *p4d_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address);
pud_t *pud_alloc(struct mm_struct *mm, p4d_t *p4d, unsigned long address);
pmd_t *pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address);
pte_t *pte_alloc_map(struct mm_struct *mm, pmd_t *pmd, unsigned long address);

/* 释放页表 */
void pgd_free(struct mm_struct *mm, pgd_t *pgd);
void p4d_free(struct mm_struct *mm, p4d_t *p4d);
void pud_free(struct mm_struct *mm, pud_t *pud);
void pmd_free(struct mm_struct *mm, pmd_t *pmd);

5.3.4 页表属性操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 检查页表项 */
static inline int pte_none(pte_t pte);
static inline int pte_present(pte_t pte);
static inline int pte_write(pte_t pte);
static inline int pte_dirty(pte_t pte);
static inline int pte_young(pte_t pte);
static inline int pte_special(pte_t pte);

/* 修改页表项 */
static inline pte_t pte_wrprotect(pte_t pte);
static inline pte_t pte_mkwrite(pte_t pte);
static inline pte_t pte_mkclean(pte_t pte);
static inline pte_t pte_mkdirty(pte_t pte);
static inline pte_t pte_mkold(pte_t pte);
static inline pte_t pte_mkyoung(pte_t pte);
static inline pte_t pte_mkspecial(pte_t pte);

/* 创建页表项 */
static inline pte_t pfn_pte(unsigned long pfn, pgprot_t pgprot);
static inline pte_t mk_pte(struct page *page, pgprot_t pgprot);

/* 获取 PFN */
static inline unsigned long pte_pfn(pte_t pte);

5.4 TLB (Translation Lookaside Buffer) 管理

5.4.1 什么是 TLB

TLB (Translation Lookaside Buffer) 是 CPU 内部的高速缓存,用于缓存最近使用的虚拟地址到物理地址的映射。它是 MMU (Memory Management Unit) 的重要组成部分。

为什么需要 TLB?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
没有 TLB 的情况:
┌─────────────────────────────────────────────────────────────────┐
│ CPU 访问虚拟地址 0x7fff0000: │
│ │ │
│ ├── 1. 读取 PGD (从内存) │
│ ├── 2. 读取 PUD (从内存) │
│ ├── 3. 读取 PMD (从内存) │
│ ├── 4. 读取 PTE (从内存) │
│ └── 5. 获取物理地址 │
│ │
│ 总共需要 4 次内存访问 = 约 100-200 纳秒 │
└─────────────────────────────────────────────────────────────────┘

有 TLB 的情况:
┌─────────────────────────────────────────────────────────────────┐
│ CPU 访问虚拟地址 0x7fff0000: │
│ │ │
│ ├── 1. 在 TLB 中查找缓存 │
│ └── 2. 命中! 直接获取物理地址 │
│ │
│ 总共需要 1 次 TLB 查找 = 约 1-2 纳秒 │
└─────────────────────────────────────────────────────────────────┘

TLB 的层次结构:

现代 CPU 通常有多级 TLB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────┐
│ 多级 TLB 结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ L1 iTLB (指令 TLB) │
│ ├── 64-128 条目 │
│ ├── 缓存指令页面的映射 │
│ └── 延迟: ~1-2 时钟周期 │
│ │
│ L1 dTLB (数据 TLB) │
│ ├── 64-128 条目 │
│ ├── 缓存数据页面的映射 │
│ └── 延迟: ~1-2 时钟周期 │
│ │
│ L2 STLB (统一 TLB) │
│ ├── 512-2048 条目 │
│ ├── 当 L1 未命中时访问 │
│ └── 延迟: ~5-10 时钟周期 │
│ │
└─────────────────────────────────────────────────────────────────┘

TLB 条目格式:

每个 TLB 条目通常包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* TLB 条目概念结构 (硬件实现) */
struct tlb_entry {
unsigned long virtual_address; /* 虚拟地址 (页对齐) */
unsigned long physical_address; /* 物理地址 (页对齐) */
unsigned int asid; /* 地址空间 ID (用于进程隔离) */
unsigned int flags; /* 标志位 */
/* - P (Present): 有效位 */
/* - R/W: 读写权限 */
/* - U/S: 用户/监督者权限 */
/* - D (Dirty): 脏位 */
/* - A (Accessed): 访问位 */
/* - G (Global): 全局位 */
/* - XD (Execute Disable): 禁止执行 */
};

/* TLB 关联性 (Associativity) */
/* - 全关联: 任意地址可映射到任意位置 */
/* - 组关联: 地址映射到特定组,组内任意位置 */
/* - 直接映射: 每个地址只能映射到一个位置 */

5.4.2 TLB 一致性问题

问题: TLB 与页表不一致

当内核修改页表后,TLB 中可能仍然保留旧的映射,导致访问错误的物理地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* TLB 不一致示例 */
void example_tlb_inconsistency(void)
{
/* 1. CPU 访问虚拟地址 0x1000, TLB 缓存: 0x1000 -> 0x5000 */
/* 内容: "Hello" */

/* 2. 内核修改页表: 0x1000 -> 0x6000 */
/* 但 TLB 仍然是: 0x1000 -> 0x5000 */

/* 3. CPU 再次访问 0x1000 */
/* TLB 命中,使用旧的映射 0x1000 -> 0x5000 */
/* 结果: 读到错误的物理地址! */
}

解决方案: TLB 刷新 (TLB Flush)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 页表修改后必须刷新 TLB */
void safe_page_table_update(void)
{
/* 1. 修改页表 */
pte_t *ptep = get_pte(mm, 0x1000);
pte_t old_pte = ptep_get(ptep);
pte_t new_pte = pte_mkdirty(pte_mkwrite( old_pte));
set_pte(ptep, new_pte);

/* 2. 刷新 TLB (确保一致性) */
__flush_tlb_one(0x1000);

/* 现在 CPU 会使用新的映射 */
}

5.4.3 TLB 刷新函数

位置: arch/x86/include/asm/tlbflush.h

基础刷新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 刷新所有 TLB 条目 */
void __flush_tlb_all(void);

/* 刷新单个地址的 TLB 条目 */
void __flush_tlb_one(unsigned long addr);

/* 刷新整个地址空间 */
void flush_tlb_mm(struct mm_struct *mm);

/* 刷新地址范围 */
void flush_tlb_range(struct vm_area_struct *vma,
unsigned long start,
unsigned long end);

/* 刷新内核地址范围 */
void flush_tlb_kernel_range(unsigned long start,
unsigned long end);

x86_64 特定刷新函数

1
2
3
4
5
6
7
8
9
10
11
12
/* 刷新用户空间单个地址 */
void native_flush_tlb_one_user(unsigned long addr);

/* 刷新本地 CPU 所有 TLB */
void native_flush_tlb_local(void);

/* 刷新全局 TLB (包括全局页) */
void native_flush_tlb_global(void);

/* 刷新多个 CPU 的 TLB (SMP) */
void native_flush_tlb_multi(const struct cpumask *cpumask,
const struct flush_tlb_info *info);

5.4.4 TLB 刷新策略

1. INVLPG 指令 (单地址刷新)

位置: arch/x86/include/asm/special_insns.h

1
2
3
4
5
6
7
8
9
10
11
/* INVLPG 指令 - 刷新单个虚拟地址的 TLB 条目 */
static inline void __invlpg(unsigned long addr)
{
asm volatile("invlpg (%0)" : : "r" (addr) : "memory");
}

/* 使用示例 */
static inline void __native_flush_tlb_one(unsigned long addr)
{
__invlpg(addr); /* 只刷新这个地址的 TLB 条目 */
}

INVLPG 优势:

  • 只刷新指定地址的 TLB 条目
  • 其他 TLB 条目保持不变
  • 精确刷新,减少性能影响

INVLPG 适用场景:

  • 单页面修改 (如页面保护变化)
  • COW (写时复制) 场景
  • 页面锁定操作

2. CR3 重新加载 (地址空间刷新)

1
2
3
4
5
6
7
8
9
10
11
/* 重新加载 CR3 - 刷新当前进程的所有非全局 TLB 条目 */
static inline void __native_flush_tlb(void)
{
/* 读取当前 CR3 值 */
unsigned long cr3 = read_cr3();

/* 重新写入 CR3 (相同的值) */
write_cr3(cr3);

/* 硬件会自动刷新所有非全局的 TLB 条目 */
}

CR3 重新加载效果:

  • 刷新当前进程的所有非全局 TLB 条目
  • 全局页 (PG全局=1) 的 TLB 条目保留
  • 适用于整个地址空间切换或大量修改

3. CR4.PGE 翻转 (全局刷新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 刷新所有 TLB 条目 (包括全局页) */
static inline void __native_flush_tlb_global(void)
{
unsigned long cr4;

/* 1. 读取 CR4 */
cr4 = native_read_cr4();

/* 2. 清除 PGE (Paging Global Enable) 位 */
/* 这会刷新所有 TLB 条目 (包括全局页) */
native_write_cr4(cr4 & ~X86_CR4_PGE);

/* 3. 内存屏障 (确保刷新完成) */
barrier();

/* 4. 恢复 PGE 位 */
native_write_cr4(cr4);
}

为什么需要刷新全局页?

  • 修改内核页表时
  • 修改全局映射 (如直接映射区)
  • 更改整个系统的映射结构

5.4.5 批量 TLB 刷新

flush_tlb_info 结构:

位置: arch/x86/include/asm/tlbflush.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* struct flush_tlb_info - TLB 刷新信息
* @mm: 目标地址空间
* @start: 刷新范围起始地址
* @end: 刷新范围结束地址
* @stride: 刷新步长 (用于大页)
* @freed_tables: 是否释放了页表
*/
struct flush_tlb_info {
struct mm_struct *mm;
unsigned long start;
unsigned long end;
unsigned long stride;
bool freed_tables;
};

批量刷新函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 刷新地址空间的 TLB 范围 */
void flush_tlb_mm_range(struct mm_struct *mm,
unsigned long start,
unsigned long end,
unsigned long stride,
bool freed_tables);

/* 刷新内核范围 */
void flush_tlb_kernel_range(unsigned long start,
unsigned long end);

/* 刷新单个用户地址 */
void flush_tlb_one_user(struct mm_struct *mm,
unsigned long addr);

/* 刷新单个内核地址 */
void flush_tlb_one_kernel(unsigned long addr);

5.4.6 PCID (Process-Context IDentifier)

PCID 概述:

PCID 是 x86_64 架构的一个重要特性,它允许 TLB 同时缓存多个进程的地址映射,避免进程切换时刷新整个 TLB。

位置: arch/x86/include/asm/cpufeatures.h:130

1
#define X86_FEATURE_PCID  ( 4*32+17)  /* Process Context Identifiers */

传统方式 (无 PCID):

1
2
3
4
5
6
7
8
9
10
进程切换时:
┌─────────────────────────────────────────────────────────────────┐
│ 进程 A ──> 进程 B │
│ │ │
│ ├── 加载新的 CR3 (指向进程 B 的页表) │
│ ├── 必须刷新整个 TLB (避免混淆 A 和 B 的映射) │
│ └── TLB 失效,进程 B 需要重新填充 TLB │
│ │
│ 问题: 每次进程切换都要刷新 TLB,性能损失大 │
└─────────────────────────────────────────────────────────────────┘

使用 PCID:

1
2
3
4
5
6
7
8
9
10
进程切换时 (有 PCID):
┌─────────────────────────────────────────────────────────────────┐
│ 进程 A (PCID=1) ──> 进程 B (PCID=2) │
│ │ │
│ ├── 加载新的 CR3 (包含 PCID=2) │
│ ├── TLB 中 PCID=1 的条目保持不变 │
│ └── TLB 中 PCID=2 的条目可以直接使用! │
│ │
│ 优势: 不需要刷新 TLB,进程切换性能大幅提升 │
└─────────────────────────────────────────────────────────────────┘

CR3 寄存器格式 (带 PCID):

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────┐
│ CR3 寄存器 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 63 52 │ 51 12 │ 11 0 │
│ │ 保留 │ 页表基址 (PGD) │ PCID │
│ │ │ │ (地址空间 ID) │
│ │
│ PCID = 0: 非 PCID 模式 (传统模式) │
│ PCID > 0: 指定地址空间的 TLB 条目 │
│ │
└─────────────────────────────────────────────────────────────────┘

INVPCID 指令:

位置: arch/x86/include/asm/invpcid.h

INVPCID 指令允许精确刷新特定 PCID 的 TLB 条目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* INVPCID 类型 */
#define INVPCID_TYPE_INDIV_ADDR 0 /* 单个地址,非全局 */
#define INVPCID_TYPE_SINGLE_CTXT 1 /* 整个上下文,非全局 */
#define INVPCID_TYPE_ALL_INCL_GLOBAL 2 /* 所有上下文,包括全局 */
#define INVPCID_TYPE_ALL_NON_GLOBAL 3 /* 所有上下文,不包括全局 */

/* INVPCID 描述符 */
struct invpcid_desc {
unsigned long pcid : 12; /* PCID 值 */
unsigned long pad : 52; /* 保留 */
unsigned long addr; /* 地址 (对于 INDIV_ADDR) */
};

/* INVPCID 指令封装 */
static inline void __invpcid(unsigned long pcid, unsigned long addr,
unsigned long type)
{
struct { u64 d[2]; } desc = {
{ .pcid = pcid, .pad = 0, .addr = addr },
{ 0 }
};

asm volatile("invpcid %0, %1"
: : "m" (desc), "r" (type) : "memory");
}

INVPCID 使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 刷新单个地址的 TLB (指定 PCID) */
void invpcid_flush_one(unsigned long pcid, unsigned long addr)
{
__invpcid(pcid, addr, INVPCID_TYPE_INDIV_ADDR);
}

/* 刷新整个上下文的 TLB (指定 PCID) */
void invpcid_flush_context(unsigned long pcid)
{
__invpcid(pcid, 0, INVPCID_TYPE_SINGLE_CTXT);
}

/* 刷新所有上下文的 TLB (不包括全局) */
void invpcid_flush_all_nonglobal(void)
{
__invpcid(0, 0, INVPCID_TYPE_ALL_NON_GLOBAL);
}

5.4.7 tlb_state 结构 (Per-CPU TLB 状态)

位置: arch/x86/include/asm/tlbflush.h:72

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* struct tlb_state - Per-CPU TLB 状态
*
* 每个 CPU 都有一个 tlb_state 实例,用于跟踪当前加载的
* 地址空间和 PCID 分配情况。
*/
struct tlb_state {
/* 当前加载的 mm_struct */
struct mm_struct *loaded_mm;

/* 上一个用户 mm (用于 IBPB 优化) */
union {
struct mm_struct *last_user_mm;
unsigned long last_user_mm_spec;
};

/* 当前加载的 ASID (Address Space ID) */
u16 loaded_mm_asid;

/* 下一个可分配的 ASID */
u16 next_asid;

/* 是否需要刷新其他上下文 */
bool invalidate_other;

/* PCID 刷新掩码 */
unsigned short user_pcid_flush_mask;

/* CR4 影子值 */
unsigned long cr4;

/* TLB 上下文数组 */
struct tlb_context ctxs[TLB_NR_DYN_ASIDS];
};

/**
* struct tlb_context - TLB 上下文
* @ctx_id: 上下文标识符 (包含 mm 指针和 ASID)
* @tlb_gen: TLB 代数 (用于判断上下文是否过期)
*/
struct tlb_context {
u64 ctx_id;
u64 tlb_gen;
};

/* 动态 ASID 数量 */
#define TLB_NR_DYN_ASIDS 6

TLB 上下文管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 每个 CPU 的 TLB 状态 */
DECLARE_PER_CPU(struct tlb_state, cpu_tlbstate);

/* 获取当前 CPU 的 TLB 状态 */
static inline struct tlb_state *this_cpu_tlbstate(void)
{
return this_cpu_ptr(&cpu_tlbstate);
}

/* 检查是否需要刷新 */
static inline bool need_flush_tlb(void)
{
struct tlb_state *st = this_cpu_tlbstate();
return st->invalidate_other;
}

5.4.8 TLB 刷新流程

本地 TLB 刷新流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────┐
│ 本地 TLB 刷新流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 页表修改 │
│ │ │
│ ├── 清除 PTE │
│ ├── 修改 PTE 权限 │
│ └── 更改物理地址 │
│ │ │
│ ▼ │
│ 2. 判断刷新范围 │
│ │ │
│ ├── 单地址? ──→ INVLPG addr │
│ ├── 地址范围? ──→ INVLPG 或 CR3 │
│ ├── 整个地址空间? ──→ CR3 重新加载 │
│ └── 全局刷新? ──→ CR4.PGE 翻转 │
│ │ │
│ ▼ │
│ 3. 执行刷新指令 │
│ │ │
│ ├── INVLPG (精确刷新) │
│ ├── MOV to CR3 (地址空间刷新) │
│ └── CR4.PGE 翻转 (全局刷新) │
│ │ │
│ ▼ │
│ 4. 完成 │
│ │
└─────────────────────────────────────────────────────────────────┘

SMP 多 CPU TLB 刷新流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────┐
│ 多 CPU TLB 刷新流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CPU 0 (修改页表的 CPU) │
│ │ │
│ ├── 1. 修改页表 │
│ ├── 2. 刷新本地 TLB │
│ └── 3. 向其他 CPU 发送 IPI (Inter-Processor Interrupt) │
│ │
│ CPU 1, 2, 3, ... │
│ │ │
│ ├── 4. 接收 IPI │
│ ├── 5. 刷新本地 TLB │
│ └── 6. 确认完成 │
│ │
│ CPU 0 │
│ │ │
│ └── 7. 等待所有 CPU 确认 │
│ │
└─────────────────────────────────────────────────────────────────┘

SMP TLB 刷新实现:

位置: arch/x86/mm/tlb.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* SMP TLB 刷新 */
void native_flush_tlb_multi(const struct cpumask *cpumask,
const struct flush_tlb_info *info)
{
/*
* 1. 向目标 CPU 发送 IPI
* 2. 每个 CPU 收到 IPI 后执行本地刷新
* 3. 等待所有 CPU 完成
*/
smp_call_function_many(cpumask, flush_tlb_func,
(void *)info, 1);
}

/* IPI 处理函数 */
static void flush_tlb_func(void *info)
{
const struct flush_tlb_info *f = info;

/* 根据信息执行相应的刷新 */
if (f->end == TLB_FLUSH_ALL) {
/* 刷新整个地址空间 */
__flush_tlb_all();
} else {
/* 刷新指定范围 */
flush_tlb_mm_range(f->mm, f->start, f->end,
f->stride, f->freed_tables);
}
}

5.4.9 TLB 优化技术

1. Lazy TLB (延迟 TLB 刷新)

1
2
3
4
5
6
7
8
/* 概念: 内核线程不需要完整的 TLB */
/*
* 当内核线程运行时,不需要使用用户空间的 TLB。
* 因此可以延迟刷新,直到切换回用户进程。
*/

/* 内核线程使用 swapper_pg_dir (内核页表) */
/* 避免 TLB 刷新,提高性能 */

2. TLB 批处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 概念: 积累多个刷新请求,一次性处理 */

/* 示例: unmap_vmas() */
void unmap_vmas(...)
{
struct mmu_gather tlb;

/* 开始收集 */
tlb_gather_mmu(&tlb, mm);

/* 解除多个页面的映射 */
for (...) {
tlb_remove_page(&tlb, page); /* 加入待刷新列表 */
}

/* 一次性刷新所有 TLB */
tlb_finish_mmu(&tlb);
}

3. 全局页优化

1
2
3
4
5
6
7
8
9
10
11
/* 标记为全局的页面不会在地址空间切换时被刷新 */

/* 内核页面通常标记为全局 */
#define _PAGE_GLOBAL (1 << _PAGE_BIT_GLOBAL)

/* 全局页示例: */
/* - 内核代码段 */
/* - 内核数据段 */
/* - 直接映射区 */

/* 全局页在 CR4.PGE=1 时不会被 CR3 重新加载刷新 */

5.4.10 TLB 调试和监控

查看系统 TLB 信息

1
2
3
4
5
6
# CPU 信息 (包含 TLB 大小)
$ cat /proc/cpuinfo | grep -i tlb

# 示例输出:
# tlb size : 2048 4K pages
# tlb size : 16 4M pages

perf 事件监控

1
2
3
4
5
6
7
8
9
10
# 监控 TLB 失效
$ perf stat -e dtlb_load_misses,mispredicts ./program

# 监控 TLB 命中率
$ perf stat -e dtlb_loads,dtlb_load_misses ./program

# 输出:
# 123456789 dtlb_loads # TLB 加载总次数
# 1234567 dtlb_load_misses # TLB 失效次数
# ~90% hit rate # 命中率

TLB 刷新统计

1
2
3
4
/* 内核可以统计 TLB 刷新次数 */
#ifdef CONFIG_DEBUG_TLBFLUSH
/* 启用 TLB 刷新调试 */
#endif

5.4.11 TLB 常见问题和调试

问题 1: TLB 未刷新导致的内存访问错误

1
2
3
4
5
6
7
8
9
10
11
/* 错误示例 */
void buggy_code(void)
{
/* 修改页表 */
pte_clear(mm, addr, ptep);

/* 忘记刷新 TLB! */

/* CPU 可能仍然使用旧的映射 */
/* 导致访问错误的物理地址 */
}

解决方案:

1
2
3
4
5
6
7
8
9
10
11
/* 正确示例 */
void correct_code(void)
{
/* 修改页表 */
pte_clear(mm, addr, ptep);

/* 刷新 TLB */
__flush_tlb_one(addr);

/* 现在 CPU 会使用新的映射 */
}

问题 2: SMP 环境下的 TLB 一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 问题: CPU 0 修改页表,但 CPU 1 的 TLB 仍然有旧映射 */

/* 解决方案: 向所有相关 CPU 发送 TLB 刷新 IPI */
void smp_safe_page_table_update(struct mm_struct *mm,
unsigned long addr)
{
/* 1. 修改页表 */
update_pte(mm, addr);

/* 2. 刷新本地 TLB */
__flush_tlb_one(addr);

/* 3. 向其他有此 mm 的 CPU 发送 IPI */
cpumask_t mask = mm_cpumask(mm);
smp_call_function_many(mask, flush_tlb_ipi, &addr, 1);
}

5.4.12 TLB 性能优化建议

  1. 尽量减少 TLB 刷新

    • 使用 PCID 避免进程切换时的刷新
    • 使用 INVLPG 而不是 CR3 重新加载
  2. 利用局部性原理

    • 相关的数据放在相邻的虚拟地址
    • 减少页面切换
  3. 使用大页

    • 减少页表深度
    • 减少 TLB 压力
  4. 优化地址空间布局

    • 将频繁使用的数据放在一起
    • 减少 TLB 失效

5.5 巨页

5.5.1 巨页类型

Linux 支持两种巨页:

  1. 普通巨页 (Huge Pages): 需要显式配置和预留
  2. 透明巨页 (THP): 对应用程序透明,自动管理

5.5.2 巨页大小定义

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 2MB 巨页 (x86_64) */
#define HPAGE_PMD_SHIFT PMD_SHIFT
#define HPAGE_PMD_SIZE (1UL << HPAGE_PMD_SHIFT) /* 2MB */
#define HPAGE_PMD_MASK ~(HPAGE_PMD_SIZE - 1)

/* 1GB 巨页 */
#define HPAGE_PUD_SHIFT PUD_SHIFT
#define HPAGE_PUD_SIZE (1UL << HPAGE_PUD_SHIFT) /* 1GB */
#define HPAGE_PUD_MASK ~(HPAGE_PUD_SIZE - 1)

/* 检查是否为巨页 */
static inline bool pmd_huge(pmd_t pmd);
static inline bool pud_huge(pud_t pud);

5.5.3 透明巨页 (THP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* THP 配置 */
#define HPAGE_PMD_ORDER (PMD_SHIFT - PAGE_SHIFT)
#define HPAGE_PMD_NR (1 << HPAGE_PMD_ORDER)

/* THP 管理 */
bool transparent_hugepage_enabled(struct vm_area_struct *vma);
int hugepage_madvise(struct vm_area_struct *vma,
unsigned long *vm_flags, int advice);
void hugepage_vma_check(struct vm_area_struct *vma);

/* 分配透明巨页 */
struct folio *vma_alloc_folio(gfp_t gfp, int order, struct vm_area_struct *vma,
unsigned long addr, bool hugepage);

/* 检查是否为透明巨页 */
static inline bool transparent_hugepage_active(struct vm_area_struct *vma);

5.5.4 巨页操作

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 分配巨页 */
struct page *alloc_huge_page(struct vm_area_struct *vma,
unsigned long addr);

/* 释放巨页 */
void free_huge_page(struct page *page);

/* 检查巨页 */
bool hugepage_vma_check(struct vm_area_struct *vma);

/* 分割巨页 */
void split_huge_page(struct page *page);
void split_huge_pmd(struct vm_area_struct *vma, unsigned long address);

5.6 页表描述符 (ptdesc)

5.6.1 ptdesc 结构

Linux 6.6+ 引入了页表描述符 (ptdesc) 来管理页表页面。

位置: include/linux/mm_types.h:458

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct ptdesc {
unsigned long __page_flags;

union {
struct rcu_head pt_rcu_head;
struct list_head pt_list;
struct {
unsigned long _pt_pad_1;
pgtable_t pmd_huge_pte;
};
};
unsigned long __page_mapping;

union {
pgoff_t pt_index;
struct mm_struct *pt_mm;
atomic_t pt_frag_refcount;
#ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
atomic_t pt_share_count;
#endif
};

union {
unsigned long _pt_pad_2;
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
unsigned int __page_type;
atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long pt_memcg_data;
#endif
};

5.6.2 page 到 ptdesc 转换

1
2
3
4
5
6
7
8
9
10
11
#define ptdesc_page(pt)         (_Generic((pt), \
const struct ptdesc *: (const struct page *)(pt), \
struct ptdesc *: (struct page *)(pt)))

#define ptdesc_folio(pt) (_Generic((pt), \
const struct ptdesc *: (const struct folio *)(pt), \
struct ptdesc *: (struct folio *)(pt)))

#define page_ptdesc(p) (_Generic((p), \
const struct page *: (const struct ptdesc *)(p), \
struct page *: (struct ptdesc *)(p)))

5.7 MMU 管理

5.7.1 MMU 概述

MMU (Memory Management Unit) 是 CPU 的内存管理单元,负责虚拟地址到物理地址的转换。它是 CPU 内部的硬件组件,与 IOMMU (管理设备 DMA) 相对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────────┐
│ CPU MMU vs IOMMU 对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CPU MMU: │
│ - 管理 CPU 的虚拟地址转换 │
│ - 使用进程页表 (CR3 寄存器指向) │
│ - TLB 缓存虚拟地址转换 │
│ │
│ IOMMU: │
│ - 管理 I/O 设备的 DMA 地址转换 │
│ - 使用设备域页表 │
│ - IOTLB 缓存 IOVA 转换 │
│ │
└─────────────────────────────────────────────────────────────────┘

MMU 核心功能:

  • 地址转换: 虚拟地址 → 物理地址
  • 内存保护: 页级权限控制 (读/写/执行)
  • TLB 管理: 缓存页表转换
  • 地址空间隔离: 通过切换页表实现进程隔离

5.7.2 MMU Gather (批量 TLB 刷新)

mmu_gather 结构:

位置: include/asm-generic/tlb.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* struct mmu_gather - MMU gather 数据结构
*
* mmu_gather 数据结构由 mm 代码使用,以实现释放页面和
* TLB 失效的正确和高效排序。
*
* 正确的排序是:
* 1) unhook page - 从页表断开页面
* 2) TLB invalidate - 使 TLB 条目失效
* 3) free page - 释放页面
*
* 我们绝不能在确保没有活着的转换之前释放页面。
*/
struct mmu_gather {
struct mm_struct *mm;
unsigned long start;
unsigned long end;
/* ... 其他字段 ... */

/* 批量释放的页面 */
struct page **pages;
struct page **local;
unsigned int pages_nr;
unsigned int max;
unsigned int need_flush : 1,

/* 是否释放整个 mm */
fullmm : 1,

/* 是否需要刷新所有 TLB */
need_flush_all : 1,

/* 快速路径 */
fast_mode : 1,

/* 释放了页表页面 */
freed_tables : 1;
};

MMU Gather API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 开始 gather */
void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm);
void tlb_gather_mmu_fullmm(struct mmu_gather *tlb, struct mm_struct *mm);

/* 结束 gather */
void tlb_finish_mmu(struct mmu_gather *tlb);

/* VMA 边界标记 */
void tlb_start_vma(struct mmu_gather *tlb, struct vm_area_struct *vma);
void tlb_end_vma(struct mmu_gather *tlb, struct vm_area_struct *vma);

/* 移除页面 (加入待释放队列) */
void tlb_remove_page(struct mmu_gather *tlb, struct page *page);
void tlb_remove_page_size(struct mmu_gather *tlb, struct page *page,
int page_size);

/* 移除页表 */
void tlb_remove_table(struct mmu_gather *tlb, void *table);

/* 刷新 TLB */
void tlb_flush_mmu(struct mmu_gather *tlb);
void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb);

/* 更改页面大小 */
void tlb_change_page_size(struct mmu_gather *tlb, unsigned int new_page_size);

MMU Gather 使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 示例: 解除页面映射 */
void unmap_page_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long end)
{
struct mmu_gather tlb;
struct page *page;

/* 开始 gather */
tlb_gather_mmu(&tlb, vma->vm_mm);

/* 标记 VMA 开始 */
tlb_start_vma(&tlb, vma);

for (; addr < end; addr += PAGE_SIZE) {
pte_t *ptep = get_pte(vma->vm_mm, addr);
pte_t pte = ptep_get(ptep);

/* 获取页面 */
page = pte_page(pte);

/* 清除 PTE */
pte_clear(vma->vm_mm, addr, ptep);

/* 将页面加入待释放队列 */
tlb_remove_page(&tlb, page);
}

/* 标记 VMA 结束 */
tlb_end_vma(&tlb, vma);

/* 结束 gather (刷新 TLB 并释放页面) */
tlb_finish_mmu(&tlb);
}

MMU Gather 配置选项:

1
2
3
4
5
6
7
8
9
10
11
/* 配置选项 */
CONFIG_MMU_GATHER_TABLE_FREE /* 独立页表释放 */
CONFIG_MMU_GATHER_RCU_TABLE_FREE /* RCU 延迟页表释放 */
CONFIG_MMU_GATHER_PAGE_SIZE /* 页面大小跟踪 */
CONFIG_MMU_GATHER_MERGE_VMAS /* 合并相邻 VMA */
CONFIG_MMU_GATHER_NO_RANGE /* 无范围刷新 */

/* 架构特定配置 */
HAVE_MMU_GATHER_PAGE_SIZE /* 架构支持页面大小 */
HAVE_RCU_TABLE_FREE /* 架构支持 RCU 表释放 */
HAVE_MMU_GATHER_BATCH_SIZE /* 批量大小配置 */

5.7.3 MMU Notifier (外部 MMU 通知)

概述:

MMU Notifier 机制允许非 CPU MMU (如 GPU、加速器、虚拟化) 跟踪 CPU 页表的变化。当内核修改页表时,会通知注册的 notifiers。

mmu_notifier 结构:

位置: include/linux/mmu_notifier.h:228

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* struct mmu_notifier - MMU notifier 订阅
* @hlist: notifiers 列表节点
* @ops: notifiers 操作
* @mm: 目标 mm_struct
* @rcu: RCU 回调
* @users: 引用计数
*/
struct mmu_notifier {
struct hlist_node hlist;
const struct mmu_notifier_ops *ops;
struct mm_struct *mm;
struct rcu_head rcu;
unsigned int users;
};

mmu_notifier_ops 结构:

位置: include/linux/mmu_notifier.h:64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* struct mmu_notifier_ops - MMU notifier 操作
*/
struct mmu_notifier_ops {
/* 释放通知 */
void (*release)(struct mmu_notifier *subscription,
struct mm_struct *mm);

/* 访问位操作 */
int (*clear_flush_young)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start,
unsigned long end);
int (*clear_young)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start,
unsigned long end);
int (*test_young)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long address);

/* 范围失效 */
int (*invalidate_range_start)(struct mmu_notifier *subscription,
const struct mmu_notifier_range *range);
void (*invalidate_range_end)(struct mmu_notifier *subscription,
const struct mmu_notifier_range *range);

/* 二级 TLB 失效 */
void (*arch_invalidate_secondary_tlbs)(struct mmu_notifier *subscription,
struct mm_struct *mm,
unsigned long start,
unsigned long end);

/* 分配/释放 */
struct mmu_notifier *(*alloc_notifier)(struct mm_struct *mm);
void (*free_notifier)(struct mmu_notifier *subscription);
};

MMU Notifier 事件:

位置: include/linux/mmu_notifier.h:51

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* enum mmu_notifier_event - mmu notifier 回调原因
*/
enum mmu_notifier_event {
/* munmap() 或 mremap() 取消映射范围 */
MMU_NOTIFY_UNMAP = 0,

/* 清除页表项 (madvise()、页面替换等) */
MMU_NOTIFY_CLEAR,

/* VMA 保护变化 (mprotect() 使用 vm_page_prot) */
MMU_NOTIFY_PROTECTION_VMA,

/* 页面保护变化 (需检查 CPU 页表) */
MMU_NOTIFY_PROTECTION_PAGE,

/* 软脏页记账 */
MMU_NOTIFY_SOFT_DIRTY,

/* mmu_interval_notifier 失效信号 */
MMU_NOTIFY_RELEASE,

/* 页面迁移 (device private memory) */
MMU_NOTIFY_MIGRATE,

/* 设备独占访问结束 */
MMU_NOTIFY_EXCLUSIVE,
};

mmu_notifier_range 结构:

位置: include/linux/mmu_notifier.h:262

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* struct mmu_notifier_range - MMU notifier 范围
* @mm: 目标 mm_struct
* @start: 范围起始地址
* @end: 范围结束地址
* @flags: 标志 (MMU_NOTIFIER_RANGE_BLOCKABLE)
* @event: 事件类型 (enum mmu_notifier_event)
* @owner: 设备私有 pgmap 所有者 (用于迁移)
*/
struct mmu_notifier_range {
struct mm_struct *mm;
unsigned long start;
unsigned long end;
unsigned int flags;
enum mmu_notifier_event event;
void *owner;
};

MMU Notifier API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 注册/注销 */
int mmu_notifier_register(struct mmu_notifier *subscription,
struct mm_struct *mm);
void mmu_notifier_unregister(struct mmu_notifier *subscription,
struct mm_struct *mm);

/* 获取/释放 */
struct mmu_notifier *mmu_notifier_get(const struct mmu_notifier_ops *ops,
struct mm_struct *mm);
void mmu_notifier_put(struct mmu_notifier *subscription);

/* 同步 */
void mmu_notifier_synchronize(void);

/* 范围失效 */
void mmu_notifier_range_init(struct mmu_notifier_range *range,
enum mmu_notifier_event event,
unsigned int flags,
struct mm_struct *mm,
unsigned long start,
unsigned long end);
void mmu_notifier_invalidate_range_start(struct mmu_notifier_range *range);
void mmu_notifier_invalidate_range_end(struct mmu_notifier_range *range);

MMU Notifier 使用示例 (KVM):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* KVM MMU notifier 实现 */
static struct mmu_notifier *kvm_mmu_notifier_alloc(struct mm_struct *mm)
{
struct kvm *kvm = kzalloc(sizeof(struct kvm), GFP_KERNEL);

if (!kvm)
return ERR_PTR(-ENOMEM);

/* 初始化 KVM MMU */
kvm_init_mmu(kvm);

return &kvm->mmu_notifier;
}

static void kvm_mmu_notifier_release(struct mmu_notifier *mn,
struct mm_struct *mm)
{
struct kvm *kvm = mmu_notifier_to_kvm(mn);

/* 释放所有影子页表 */
kvm_mmu_unload_guest_mmu(kvm);
}

static int kvm_mmu_notifier_invalidate_range_start(
struct mmu_notifier *mn, const struct mmu_notifier_range *range)
{
struct kvm *kvm = mmu_notifier_to_kvm(mn);

/* 使失效范围的影子页表条目失效 */
kvm_mmu_invalidate_range(kvm, range->start, range->end);

return 0;
}

static const struct mmu_notifier_ops kvm_mmu_notifier_ops = {
.release = kvm_mmu_notifier_release,
.clear_flush_young = kvm_mmu_notifier_clear_flush_young,
.invalidate_range_start = kvm_mmu_notifier_invalidate_range_start,
.invalidate_range_end = kvm_mmu_notifier_invalidate_range_end,
};

MMU Interval Notifier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 区间 notifiers 用于特定地址范围的跟踪 */

struct mmu_interval_notifier {
struct interval_tree_node interval_tree;
const struct mmu_interval_notifier_ops *ops;
struct mm_struct *mm;
unsigned long invalidate_seq;
};

struct mmu_interval_notifier_ops {
bool (*invalidate)(struct mmu_interval_notifier *interval_sub,
const struct mmu_notifier_range *range,
unsigned long cur_seq);
};

/* API */
int mmu_interval_notifier_insert(struct mmu_interval_notifier *interval_sub,
struct mm_struct *mm,
unsigned long start,
unsigned long length,
const struct mmu_interval_notifier_ops *ops);
void mmu_interval_notifier_remove(struct mmu_interval_notifier *interval_sub);

5.7.4 MMU 上下文切换

概述:

MMU 上下文切换涉及更改 CPU 使用的页表基址 (CR3 on x86)。

mm_struct 上下文切换:

1
2
3
4
5
6
7
8
9
10
11
/* 切换到新的 mm */
void switch_mm(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk);

/* 激活 mm */
void activate_mm(struct mm_struct *prev, struct mm_struct *next);

/* 检查 mm */
static inline bool __mm_is_not_active(struct mm_struct *mm, struct task_struct *tsk);
static inline void __mm_grab(struct mm_struct *mm);
static inline void __mm_drop(struct mm_struct *mm);

x86_64 上下文切换:

位置: arch/x86/mm/tlb.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 切换 mm (CR3) */
void switch_mm_irqs_off(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk)
{
struct mm_struct *real_prev = this_cpu_read(cpu_tlbstate.loaded_mm);
u16 prev_asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);
unsigned long new_cr3;

/* 相同 mm,无需切换 */
if (real_prev == next) {
VM_WARN_ON_ONCE(!cpumask_test_cpu(smp_processor_id(),
mm_cpumask(next)));
return;
}

/* 获取新 CR3 */
new_cr3 = __pa(next->pgd);

/* 加载新的 CR3 (页表基址) */
write_cr3(new_cr3);

/* 更新 per-CPU 状态 */
this_cpu_write(cpu_tlbstate.loaded_mm, next);
this_cpu_write(cpu_tlbstate.loaded_mm_asid, next_asid);

/* 更新 mm CPU 掩码 */
cpumask_set_cpu(cpu, mm_cpumask(next));
}

PCID (Process-Context IDentifiers):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* PCID 允许同时缓存多个地址空间的 TLB 条目 */
#define TLB_NR_DYN_ASIDS 12

struct tlb_state {
/* 当前加载的 mm */
struct mm_struct *loaded_mm;
u16 loaded_mm_asid;
unsigned long cr4;

/* PCID 管理 */
u16 next_asid;

/* 是否需要 CR3 刷新 */
bool invalidate_other;
};

5.7.5 MMU 与 IOMMU 交互

SVA (Shared Virtual Addressing) 场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────┐
│ SVA 场景 MMU 与 IOMMU 协作 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CPU ──→ MMU ──→ CPU 页表 ──→ 物理内存 │
│ │ │
│ └──→ MMU Notifier ──→ 通知 IOMMU │
│ │ │
│ 设备 ──→ IOMMU ──→ IOMMU 页表 ──→ 物理内存 ←──────────┘ │
│ │
│ 两种 MMU 维护同一地址空间的映射,通过 notifiers 同步 │
│ │
└─────────────────────────────────────────────────────────────────┘

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 设备驱动使用 SVA */
struct device *dev = /* 设备 */;
struct mm_struct *mm = current->mm;

/* 1. 绑定设备到进程 (SVA) */
struct iommu_sva *handle = iommu_sva_bind_device(dev, mm);

/* 2. 获取 PASID */
u32 pasid = iommu_sva_get_pasid(handle);

/* 3. 内核页表变化时,通过 MMU notifier 通知 IOMMU:
* - CPU: munmap/mprotect/madvise
* ↓
* - MMU notifier 回调
* ↓
* - IOMMU: 更新设备页表
*/

/* 4. 设备可以直接使用进程虚拟地址 */
/* 设备驱动将 PASID 和虚拟地址传递给硬件 */

/* 5. 解绑 */
iommu_sva_unbind_device(handle);

5.8 本章小结

本章介绍了 Linux 6.12 的页表管理:

  1. 多级页表结构: 4级/5级页表,PGD->P4D->PUD->PMD->PTE
  2. 页表项格式: 64位 PTE,包含 PFN 和标志位
  3. 页表操作: 查找、设置、创建页表项
  4. TLB 管理: 刷新函数、批量刷新、INVLPG 指令
  5. 巨页支持: 普通巨页和透明巨页 (THP)
  6. 页表描述符: ptdesc 结构,管理页表页面
  7. MMU 管理:
    • MMU Gather: 批量 TLB 刷新和页面释放
    • MMU Notifier: 外部 MMU 通知机制 (KVM、GPU 等)
    • MMU 上下文切换: CR3 切换、PCID 支持
    • MMU 与 IOMMU 交互: SVA 场景下的协作

下一章将介绍虚拟内存区域 (VMA)。

  • Title: Linux内核分析之内存管理-05
  • Author: 韩乔落
  • Created at : 2026-01-20 21:39:07
  • Updated at : 2026-02-24 14:04:52
  • Link: https://jelasin.github.io/2026/01/20/Linux内核分析之内存管理-05/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments