第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
| #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) #define PGDIR_MASK (~(PGDIR_SIZE - 1))
#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE - 1))
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) #define PMD_MASK ~(PMD_SIZE - 1)
#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE - 1))
#define CONFIG_PGTABLE_LEVELS 4
|
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 #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
| static inline pgd_t *pgd_offset(struct mm_struct *mm, unsigned long address); static inline pgd_t *pgd_offset_k(unsigned long address);
static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long address);
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address);
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address);
static inline pte_t *pte_offset_map(pmd_t *pmd, unsigned long address); #define pte_unmap(pte) kunmap(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
| static inline void set_pte_at(struct mm_struct *mm, unsigned long addr, pte_t *ptep, pte_t 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);
#define set_pte(ptep, pte) native_set_pte(ptep, 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);
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
| struct tlb_entry { unsigned long virtual_address; unsigned long physical_address; unsigned int asid; unsigned int flags; };
|
5.4.2 TLB 一致性问题
问题: TLB 与页表不一致
当内核修改页表后,TLB 中可能仍然保留旧的映射,导致访问错误的物理地址。
1 2 3 4 5 6 7 8 9 10 11 12 13
| void example_tlb_inconsistency(void) {
}
|
解决方案: TLB 刷新 (TLB Flush)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void safe_page_table_update(void) { 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);
__flush_tlb_one(0x1000);
}
|
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
| void __flush_tlb_all(void);
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);
void native_flush_tlb_local(void);
void native_flush_tlb_global(void);
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
| 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); }
|
INVLPG 优势:
- 只刷新指定地址的 TLB 条目
- 其他 TLB 条目保持不变
- 精确刷新,减少性能影响
INVLPG 适用场景:
- 单页面修改 (如页面保护变化)
- COW (写时复制) 场景
- 页面锁定操作
2. CR3 重新加载 (地址空间刷新)
1 2 3 4 5 6 7 8 9 10 11
| static inline void __native_flush_tlb(void) { unsigned long cr3 = read_cr3();
write_cr3(cr3);
}
|
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
| static inline void __native_flush_tlb_global(void) { unsigned long cr4;
cr4 = native_read_cr4();
native_write_cr4(cr4 & ~X86_CR4_PGE);
barrier();
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 { 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
| 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)
|
传统方式 (无 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
| #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
struct invpcid_desc { unsigned long pcid : 12; unsigned long pad : 52; unsigned long addr; };
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
| void invpcid_flush_one(unsigned long pcid, unsigned long addr) { __invpcid(pcid, addr, INVPCID_TYPE_INDIV_ADDR); }
void invpcid_flush_context(unsigned long pcid) { __invpcid(pcid, 0, INVPCID_TYPE_SINGLE_CTXT); }
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 { struct mm_struct *loaded_mm;
union { struct mm_struct *last_user_mm; unsigned long last_user_mm_spec; };
u16 loaded_mm_asid;
u16 next_asid;
bool invalidate_other;
unsigned short user_pcid_flush_mask;
unsigned long cr4;
struct tlb_context ctxs[TLB_NR_DYN_ASIDS]; };
struct tlb_context { u64 ctx_id; u64 tlb_gen; };
#define TLB_NR_DYN_ASIDS 6
|
TLB 上下文管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| DECLARE_PER_CPU(struct tlb_state, cpu_tlbstate);
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
| void native_flush_tlb_multi(const struct cpumask *cpumask, const struct flush_tlb_info *info) {
smp_call_function_many(cpumask, flush_tlb_func, (void *)info, 1); }
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 刷新)
2. TLB 批处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void unmap_vmas(...) { struct mmu_gather tlb;
tlb_gather_mmu(&tlb, mm);
for (...) { tlb_remove_page(&tlb, page); }
tlb_finish_mmu(&tlb); }
|
3. 全局页优化
1 2 3 4 5 6 7 8 9 10 11
|
#define _PAGE_GLOBAL (1 << _PAGE_BIT_GLOBAL)
|
5.4.10 TLB 调试和监控
查看系统 TLB 信息
1 2 3 4 5 6
| $ cat /proc/cpuinfo | grep -i tlb
|
perf 事件监控
1 2 3 4 5 6 7 8 9 10
| $ perf stat -e dtlb_load_misses,mispredicts ./program
$ perf stat -e dtlb_loads,dtlb_load_misses ./program
|
TLB 刷新统计
1 2 3 4
| #ifdef CONFIG_DEBUG_TLBFLUSH
#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);
}
|
解决方案:
1 2 3 4 5 6 7 8 9 10 11
| void correct_code(void) { pte_clear(mm, addr, ptep);
__flush_tlb_one(addr);
}
|
问题 2: SMP 环境下的 TLB 一致性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
void smp_safe_page_table_update(struct mm_struct *mm, unsigned long addr) { update_pte(mm, addr);
__flush_tlb_one(addr);
cpumask_t mask = mm_cpumask(mm); smp_call_function_many(mask, flush_tlb_ipi, &addr, 1); }
|
5.4.12 TLB 性能优化建议
尽量减少 TLB 刷新
- 使用 PCID 避免进程切换时的刷新
- 使用 INVLPG 而不是 CR3 重新加载
利用局部性原理
使用大页
优化地址空间布局
5.5 巨页
5.5.1 巨页类型
Linux 支持两种巨页:
- 普通巨页 (Huge Pages): 需要显式配置和预留
- 透明巨页 (THP): 对应用程序透明,自动管理
5.5.2 巨页大小定义
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define HPAGE_PMD_SHIFT PMD_SHIFT #define HPAGE_PMD_SIZE (1UL << HPAGE_PMD_SHIFT) #define HPAGE_PMD_MASK ~(HPAGE_PMD_SIZE - 1)
#define HPAGE_PUD_SHIFT PUD_SHIFT #define HPAGE_PUD_SIZE (1UL << HPAGE_PUD_SHIFT) #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
| #define HPAGE_PMD_ORDER (PMD_SHIFT - PAGE_SHIFT) #define HPAGE_PMD_NR (1 << HPAGE_PMD_ORDER)
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 { 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,
fullmm : 1,
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
| 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);
void tlb_finish_mmu(struct mmu_gather *tlb);
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);
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;
tlb_gather_mmu(&tlb, vma->vm_mm);
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_clear(vma->vm_mm, addr, ptep);
tlb_remove_page(&tlb, page); }
tlb_end_vma(&tlb, vma);
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 CONFIG_MMU_GATHER_PAGE_SIZE CONFIG_MMU_GATHER_MERGE_VMAS CONFIG_MMU_GATHER_NO_RANGE
HAVE_MMU_GATHER_PAGE_SIZE HAVE_RCU_TABLE_FREE 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 { 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 { 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);
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_NOTIFY_UNMAP = 0,
MMU_NOTIFY_CLEAR,
MMU_NOTIFY_PROTECTION_VMA,
MMU_NOTIFY_PROTECTION_PAGE,
MMU_NOTIFY_SOFT_DIRTY,
MMU_NOTIFY_RELEASE,
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 { 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
| 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_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
|
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); };
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
| void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk);
void activate_mm(struct mm_struct *prev, struct mm_struct *next);
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
| 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;
if (real_prev == next) { VM_WARN_ON_ONCE(!cpumask_test_cpu(smp_processor_id(), mm_cpumask(next))); return; }
new_cr3 = __pa(next->pgd);
write_cr3(new_cr3);
this_cpu_write(cpu_tlbstate.loaded_mm, next); this_cpu_write(cpu_tlbstate.loaded_mm_asid, next_asid);
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
| #define TLB_NR_DYN_ASIDS 12
struct tlb_state { struct mm_struct *loaded_mm; u16 loaded_mm_asid; unsigned long cr4;
u16 next_asid;
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
| struct device *dev = ; struct mm_struct *mm = current->mm;
struct iommu_sva *handle = iommu_sva_bind_device(dev, mm);
u32 pasid = iommu_sva_get_pasid(handle);
iommu_sva_unbind_device(handle);
|
5.8 本章小结
本章介绍了 Linux 6.12 的页表管理:
- 多级页表结构: 4级/5级页表,PGD->P4D->PUD->PMD->PTE
- 页表项格式: 64位 PTE,包含 PFN 和标志位
- 页表操作: 查找、设置、创建页表项
- TLB 管理: 刷新函数、批量刷新、INVLPG 指令
- 巨页支持: 普通巨页和透明巨页 (THP)
- 页表描述符: ptdesc 结构,管理页表页面
- MMU 管理:
- MMU Gather: 批量 TLB 刷新和页面释放
- MMU Notifier: 外部 MMU 通知机制 (KVM、GPU 等)
- MMU 上下文切换: CR3 切换、PCID 支持
- MMU 与 IOMMU 交互: SVA 场景下的协作
下一章将介绍虚拟内存区域 (VMA)。