MMU(内存管理单元)
本文深入讲解 MMU 的核心原理、地址转换机制、页表结构、TLB 优化、内存保护以及在不同架构(ARM Cortex-A、x86、RISC-V)中的实现差异。适合系统开发者和嵌入式工程师。
一、MMU 是什么?为什么需要它?
1.1 MMU 的定义
MMU(Memory Management Unit,内存管理单元) 是 CPU 中的硬件模块,负责将 虚拟地址(Virtual Address, VA) 转换为 物理地址(Physical Address, PA),并提供内存保护功能。
1.2 没有 MMU 的世界
在简单的微控制器(如 Cortex-M)中,CPU 直接访问物理内存:
CPU 执行指令: LDR R0, [0x20000000]
↓
直接访问物理地址 0x20000000 的 SRAM
问题:
内存碎片化:多个程序运行时,必须精确分配不重叠的物理地址安全性差:任何程序都能访问整个内存空间,包括操作系统内核无法支持虚拟内存:无法实现"按需加载"或"内存换页"
1.3 有 MMU 的世界
CPU 执行指令: LDR R0, [0x40000000] ← 虚拟地址
↓
MMU 查表
↓
访问物理地址 0x82000000 ← 实际的 DRAM 地址
优势:
地址空间隔离:每个进程拥有独立的 4GB 虚拟地址空间(32 位系统)内存保护:防止进程访问内核或其他进程的内存虚拟内存:支持 swap、按需分页(demand paging)简化编程:程序总是从固定地址(如 0x00000000)开始,无需重定位
二、MMU 的核心概念
2.1 虚拟地址与物理地址
地址类型描述示例虚拟地址(VA)程序看到的地址,由编译器/链接器生成0x40001234物理地址(PA)实际硬件地址,连接到 DRAM/外设0x82345678地址转换流程:
虚拟地址(32 位)
├─ [31:20] 页目录索引(1024 个条目)
├─ [19:12] 页表索引(1024 个条目)
└─ [11:0] 页内偏移(4KB 页大小)
↓
页表查找(多级)
↓
物理地址 = 页表基地址 + 页内偏移
2.2 页(Page)与页框(Page Frame)
页(Page):虚拟内存的基本单位(通常 4KB)页框(Page Frame):物理内存的基本单位(也是 4KB)
映射关系:
虚拟地址空间 物理内存
┌─────────────┐ ┌─────────────┐
│ 页 0 (4KB) │ ───┐ │ 页框 5 │
├─────────────┤ │ ├─────────────┤
│ 页 1 (4KB) │ ───┼────>│ 页框 0 │
├─────────────┤ │ ├─────────────┤
│ 页 2 (4KB) │ ───┘ │ 页框 3 │
└─────────────┘ └─────────────┘
关键点:
虚拟页和物理页框不必连续允许内存碎片整理和按需分配
2.3 页表(Page Table)
页表 是存储在内存中的数据结构,记录虚拟地址到物理地址的映射关系。
单级页表的问题(32 位系统):
虚拟地址空间:4GB = 2^32 字节页大小:4KB = 2^12 字节需要的页表条目:2^32 / 2^12 = 2^20 = 1M 个条目每个条目 4 字节 → 页表大小 = 4MB
每个进程都需要 4MB 的页表! 这太浪费了。
三、多级页表结构
为了减少页表占用的内存,现代 MMU 使用 多级页表。
3.1 两级页表(ARM Cortex-A 经典实现)
虚拟地址(32 位)
┌────────────┬────────────┬──────────────┐
│ L1 索引 │ L2 索引 │ 页内偏移 │
│ [31:20] │ [19:12] │ [11:0] │
│ 12 位 │ 8 位 │ 12 位 │
└────────────┴────────────┴──────────────┘
↓ ↓ ↓
一级页表 二级页表 4KB 页内
(4096 项) (256 项)
查找流程:
读取页表基地址寄存器(TTBR0)
TTBR0 = 0x80000000 ← 一级页表的物理地址
计算一级页表条目地址
L1_index = VA[31:20] // 取虚拟地址的高 12 位
L1_entry_addr = TTBR0 + (L1_index * 4)
读取一级页表条目
L1_entry = Memory[L1_entry_addr]
检查条目类型:
Section(段映射,1MB):直接返回物理地址Page Table(二级页表):继续查找
读取二级页表条目
L2_table_base = L1_entry[31:10] // 二级页表基地址
L2_index = VA[19:12]
L2_entry_addr = L2_table_base + (L2_index * 4)
L2_entry = Memory[L2_entry_addr]
计算最终物理地址
page_frame = L2_entry[31:12] // 物理页框号
offset = VA[11:0] // 页内偏移
PA = (page_frame << 12) | offset
示例:
假设访问虚拟地址 0x40102ABC:
VA = 0x40102ABC
= 0100 0000 0001 0000 0010 1010 1011 1100
↑─────┬─────↑ ↑──┬───↑ ↑──────┬──────↑
L1索引(401H) L2索引(02H) 偏移(ABCH)
1. L1_index = 0x401 = 1025
L1_entry_addr = TTBR0 + 1025 * 4 = 0x80000000 + 0x1004 = 0x80001004
2. L1_entry = Memory[0x80001004] = 0x82000001
类型 = Page Table (bit[1:0] = 01)
L2_table_base = 0x82000000
3. L2_index = 0x02
L2_entry_addr = 0x82000000 + 0x02 * 4 = 0x82000008
4. L2_entry = Memory[0x82000008] = 0x8500002F
page_frame = 0x85000
5. PA = 0x85000000 | 0xABC = 0x85000ABC
3.2 多级页表的优势
内存占用对比:
页表类型内存占用说明单级页表4MB × 进程数每个进程 4MB,无论是否使用全部地址空间两级页表4KB(L1)+ N × 1KB(L2)L2 按需分配,稀疏地址空间只占用少量内存示例:
假设进程只使用了 8MB 的地址空间:
单级页表:仍需 4MB两级页表:4KB(L1)+ 8KB(L2,8 个 1KB 表)= 12KB
节省了 99.7% 的内存!
四、页表条目(PTE)结构
页表条目不仅包含物理地址,还包含访问权限、缓存属性等控制位。
4.1 ARM Cortex-A 的二级页表条目(Small Page, 4KB)
┌────────────────────────────────────────────────────────────────┐
│ 31 20│19 12│11│10│9│8 6│5 4│3 2│1│0│ │
├─────────────┼──────┼──┼──┼─┼────┼────┼────┼─┼─┤ │
│ 物理页框地址 │ nG │S │AP│TEX│AP │CB │1│XN│ │
└─────────────┴──────┴──┴──┴─┴────┴────┴────┴─┴─┘ │
各字段含义:
位域名称描述[31:12]物理页框地址指向 4KB 对齐的物理页框[11]nG(non-Global)0=全局页(所有进程共享),1=进程私有页[10]S(Shared)1=多核共享(影响缓存一致性)[9]APX扩展访问权限位[8:6]TEX类型扩展(Type Extension),配合 C/B 定义缓存策略[5:4]AP[1:0]访问权限(Access Permission)[3:2]C, B缓存和缓冲属性[1]1必须为 1,标识这是小页(4KB)[0]XN(eXecute Never)1=禁止执行(NX bit)4.2 访问权限(AP 位)
APXAP[1:0]特权模式用户模式典型用途000无访问无访问无效页001R/W无访问内核数据010R/WR共享只读数据011R/WR/W用户数据101R无访问内核只读数据110RR代码段111RR共享只读数据4.3 缓存属性(TEX, C, B)
TEXCB描述典型用途00000强序,不缓存MMIO(外设寄存器)00001设备内存UART、GPIO 等00010写通缓存日志缓冲区00011写回缓存正常内存(DRAM)00100共享设备多核共享外设示例配置:
// 配置 DRAM 区域(写回缓存,读写,用户可访问)
pte = (physical_addr & 0xFFFFF000) | // 物理地址
(0b000 << 6) | // TEX = 000
(0b11 << 4) | // AP = 11(用户可读写)
(1 << 3) | // C = 1(启用缓存)
(1 << 2) | // B = 1(写回)
(1 << 1) | // 小页标记
(0 << 0); // XN = 0(允许执行)
// 配置外设区域(不缓存,强序)
pte = (0x40000000 & 0xFFFFF000) | // GPIO 基地址
(0b000 << 6) | // TEX = 000
(0b01 << 4) | // AP = 01(仅特权访问)
(0 << 3) | // C = 0(禁用缓存)
(0 << 2) | // B = 0(强序)
(1 << 1) | // 小页标记
(1 << 0); // XN = 1(禁止执行)
五、TLB(Translation Lookaside Buffer)
5.1 TLB 是什么?
问题:每次内存访问都要查页表,需要多次内存读取(两级页表需要 2-3 次),太慢!
解决方案:TLB 是一个小型的、全关联的硬件缓存,存储最近使用的虚拟地址到物理地址的映射。
CPU
↓
查 TLB
↓
┌─── 命中? ───┐
│ │
是 否
↓ ↓
直接得到 PA 查页表(慢)
↓
更新 TLB
5.2 TLB 的结构
典型 TLB 条目:
┌──────────────┬──────────────┬────────────┬──────┐
│ 虚拟页号(VPN) │ 物理页框号(PFN)│ 访问权限 │ 有效位 │
│ 20 位 │ 20 位 │ AP, TEX等 │ V │
└──────────────┴──────────────┴────────────┴──────┘
TLB 参数(以 Cortex-A9 为例):
容量:128 条目(Instruction TLB)+ 128 条目(Data TLB)关联度:全关联或 4-way 组关联命中率:通常 > 99%
5.3 TLB 未命中(TLB Miss)
硬件 TLB Miss 处理(ARM):
CPU 自动发起页表遍历(Table Walk)读取多级页表更新 TLB重新执行访问指令
软件 TLB Miss 处理(RISC-V、MIPS):
触发 TLB Miss 异常操作系统查页表手动写入 TLB返回并重试
5.4 TLB 失效(TLB Invalidation)
何时需要失效 TLB?
进程切换:新进程有不同的页表修改页表:改变映射关系或权限取消映射:释放内存
失效指令(ARM):
// 失效所有 TLB 条目
asm volatile("mcr p15, 0, %0, c8, c7, 0" :: "r"(0));
// 失效单个地址的 TLB 条目
asm volatile("mcr p15, 0, %0, c8, c7, 1" :: "r"(va));
// 失效所有非全局条目(进程切换时)
asm volatile("mcr p15, 0, %0, c8, c7, 2" :: "r"(0));
Linux 内核示例:
void flush_tlb_all(void) {
local_flush_tlb_all(); // 失效本核 TLB
smp_call_function(flush_tlb_all_ipi, NULL, 1); // 通知其他核
}
六、内存保护与异常处理
6.1 页错误(Page Fault)
触发条件:
页不存在(Page Not Present):访问未映射的虚拟地址权限违规(Permission Violation):用户态访问内核页、写只读页执行保护违规(NX Violation):执行标记为 XN 的页(如堆栈)
ARM 的数据中止异常(Data Abort):
void DataAbortHandler(void) {
uint32_t fault_addr, fault_status;
// 读取故障地址寄存器(FAR)
asm volatile("mrc p15, 0, %0, c6, c0, 0" : "=r"(fault_addr));
// 读取故障状态寄存器(FSR)
asm volatile("mrc p15, 0, %0, c5, c0, 0" : "=r"(fault_status));
uint32_t fault_type = fault_status & 0x0F;
switch (fault_type) {
case 0x05: // Translation fault (Section)
case 0x07: // Translation fault (Page)
printf("Page fault at 0x%08X\n", fault_addr);
// 可能需要按需加载页
break;
case 0x0D: // Permission fault (Section)
case 0x0F: // Permission fault (Page)
printf("Permission denied at 0x%08X\n", fault_addr);
// 发送 SIGSEGV 信号给进程
break;
}
}
6.2 写时复制(Copy-on-Write, COW)
场景:fork() 创建子进程时,父子进程共享相同的物理页。
实现:
将父进程的所有可写页标记为 只读子进程的页表指向相同的物理页当任何一方尝试写入时,触发 Page Fault操作系统分配新物理页,复制数据,更新页表
void handle_cow_fault(uint32_t fault_addr) {
page_t *page = find_page(fault_addr);
if (page->ref_count > 1) {
// 页被多个进程共享
void *new_page = alloc_physical_page();
memcpy(new_page, page->physical_addr, 4096);
// 更新页表,指向新页
update_page_table(fault_addr, new_page);
// 设置为可写
set_page_writable(fault_addr);
page->ref_count--;
} else {
// 只有一个进程使用,直接设置为可写
set_page_writable(fault_addr);
}
}
七、不同架构的 MMU 实现
7.1 ARM Cortex-A(ARMv7-A)
特点:
两级页表(Section 或 Small Page)硬件页表遍历(Table Walk Unit)独立的 I-TLB 和 D-TLB
页表基地址寄存器:
TTBR0:用户空间(0x00000000 - 0x7FFFFFFF)TTBR1:内核空间(0x80000000 - 0xFFFFFFFF)
配置示例:
void enable_mmu(void) {
uint32_t *page_table = create_page_table();
// 1. 设置 TTBR0(用户空间页表)
asm volatile("mcr p15, 0, %0, c2, c0, 0" :: "r"(page_table));
// 2. 设置 Domain(域访问控制)
uint32_t dacr = 0x00000001; // Domain 0 = Client
asm volatile("mcr p15, 0, %0, c3, c0, 0" :: "r"(dacr));
// 3. 启用 MMU
uint32_t sctlr;
asm volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(sctlr));
sctlr |= 0x1; // M bit(启用 MMU)
sctlr |= 0x4; // C bit(启用数据缓存)
sctlr |= 0x1000; // I bit(启用指令缓存)
asm volatile("mcr p15, 0, %0, c1, c0, 0" :: "r"(sctlr));
}
7.2 x86-64(Intel/AMD)
特点:
四级页表(PML4 → PDPT → PD → PT)48 位虚拟地址空间(256 TB)硬件页表遍历 + TLB
四级页表结构:
虚拟地址(64 位,实际使用 48 位)
┌─────┬──────┬──────┬──────┬──────┬──────────┐
│ 保留 │ PML4 │ PDPT │ PD │ PT │ 页内偏移 │
│63-48│47-39 │38-30 │29-21 │20-12 │ 11-0 │
└─────┴──────┴──────┴──────┴──────┴──────────┘
9位 9位 9位 9位 12位
控制寄存器:
CR3:页表基地址(PML4)CR0.PG:启用分页CR4.PAE:启用物理地址扩展(必须为 1)
7.3 RISC-V(Sv39)
特点:
三级页表(39 位虚拟地址)软件 TLB 管理(通过 sfence.vma 刷新)简洁的页表项格式
页表项(PTE)结构:
┌──────────────────────────────────────────────────────────────┐
│ 63 54│53 28│27 19│18 10│9 8│7│6│5│4│3│2│1│0│
├───────┼───────────┼───────────┼───────────┼────┼─┼─┼─┼─┼─┼─┼─┼─┤
│ 保留 │ PPN[2] │ PPN[1] │ PPN[0] │ RSW│D│A│G│U│X│W│R│V│
└───────┴───────────┴───────────┴───────────┴────┴─┴─┴─┴─┴─┴─┴─┴─┘
权限位:
V(Valid):页有效R(Read):可读W(Write):可写X(eXecute):可执行U(User):用户态可访问G(Global):全局页A(Accessed):已访问D(Dirty):已修改
启用 MMU:
void enable_mmu_riscv(void) {
uint64_t page_table_pa = (uint64_t)page_table >> 12;
uint64_t satp = (8ULL << 60) | page_table_pa; // MODE=Sv39
// 写入 satp 寄存器
asm volatile("csrw satp, %0" :: "r"(satp));
// 刷新 TLB
asm volatile("sfence.vma");
}
八、MMU 在操作系统中的应用
8.1 Linux 的内存布局(32 位)
虚拟地址空间(4GB)
┌─────────────────────────────────────┐ 0xFFFFFFFF
│ 内核空间(Kernel Space) │
│ - 内核代码、数据 │
│ - 驱动程序 │
│ - 页表 │ 1GB
├─────────────────────────────────────┤ 0xC0000000
│ 用户空间(User Space) │
│ - 栈(Stack) │
│ - 内存映射区(mmap) │
│ - 堆(Heap) │
│ - BSS │
│ - 数据段(Data) │
│ - 代码段(Text) │ 3GB
└─────────────────────────────────────┘ 0x00000000
8.2 进程切换时的 MMU 操作
void switch_mm(struct mm_struct *prev_mm, struct mm_struct *next_mm) {
if (prev_mm == next_mm)
return; // 同一进程,无需切换
// 1. 切换页表基地址
uint32_t pgd_pa = virt_to_phys(next_mm->pgd);
asm volatile("mcr p15, 0, %0, c2, c0, 0" :: "r"(pgd_pa));
// 2. 失效非全局 TLB 条目
asm volatile("mcr p15, 0, %0, c8, c7, 2" :: "r"(0));
// 3. 更新 ASID(地址空间 ID,避免全部刷新 TLB)
uint32_t contextidr = next_mm->context.id;
asm volatile("mcr p15, 0, %0, c13, c0, 1" :: "r"(contextidr));
}
8.3 按需分页(Demand Paging)
原理:程序启动时,不立即分配所有物理内存,而是在首次访问时触发 Page Fault 再分配。
void do_page_fault(uint32_t fault_addr, uint32_t fault_status) {
struct vm_area_struct *vma = find_vma(current->mm, fault_addr);
if (!vma || fault_addr < vma->vm_start) {
// 无效地址,发送 SIGSEGV
send_signal(current, SIGSEGV);
return;
}
// 分配物理页
struct page *page = alloc_page(GFP_KERNEL);
// 建立映射
pte_t *pte = get_pte(current->mm, fault_addr);
set_pte(pte, mk_pte(page, vma->vm_page_prot));
// 如果是从磁盘换页回来,读取数据
if (vma->vm_file) {
read_page_from_disk(page, fault_addr);
} else {
// 匿名页,清零
memset(page_address(page), 0, PAGE_SIZE);
}
}
九、MMU 调试技巧
9.1 查看页表内容
通过调试器(以 ARM 为例):
# 读取 TTBR0
(gdb) print/x $cp15_c2_c0_0
# 读取一级页表条目
(gdb) x/1wx 0x80000000 + (0x40000000 >> 18)
# 读取二级页表条目
(gdb) x/1wx
通过 /proc 文件系统(Linux):
# 查看进程的虚拟内存映射
cat /proc/1234/maps
# 示例输出
00400000-00401000 r-xp 00000000 08:01 123456 /bin/app ← 代码段
00600000-00601000 rw-p 00000000 08:01 123456 /bin/app ← 数据段
7ffff7a0d000-7ffff7bcd000 r-xp libc.so.6 ← 共享库
7ffff7dd5000-7ffff7dfc000 rw-p [heap] ← 堆
7ffffffde000-7ffffffff000 rw-p [stack] ← 栈
9.2 常见问题与解决
问题 1:MMU 启用后立即崩溃
// 错误:忘记映射异常向量表
void enable_mmu(void) {
create_page_table();
enable_mmu_control(); // 崩溃!
}
// 正确:先映射异常向量表
void enable_mmu(void) {
create_page_table();
// 映射异常向量表(0xFFFF0000)
map_page(0xFFFF0000, 0x00000000, AP_KERNEL_RO);
enable_mmu_control();
}
问题 2:外设访问失败
// 错误:外设区域启用了缓存
map_page(0x40000000, 0x40000000, AP_KERNEL_RW | CACHEABLE);
// 正确:外设必须禁用缓存
map_page(0x40000000, 0x40000000, AP_KERNEL_RW | STRONGLY_ORDERED);
问题 3:TLB 失效不完全
// 错误:修改页表后忘记失效 TLB
update_page_table(va, new_pa);
// ... CPU 仍使用旧的 TLB 缓存
// 正确
update_page_table(va, new_pa);
flush_tlb_page(va); // 失效对应的 TLB 条目
十、总结
10.1 MMU 的核心功能
功能描述应用地址转换虚拟地址 → 物理地址所有内存访问内存保护访问权限检查(R/W/X)防止越界、权限提升TLB 加速缓存地址映射提高转换速度(99%+ 命中率)多级页表按需分配页表节省内存(稀疏地址空间)缓存控制定义缓存策略优化性能,正确处理 MMIO10.2 不同架构对比
特性ARM Cortex-Ax86-64RISC-V页表级数2 级4 级3 级(Sv39)虚拟地址宽度32 位48 位39 位TLB 管理硬件硬件软件页大小4KB, 64KB, 1MB4KB, 2MB, 1GB4KB, 2MB, 1GB10.3 学习路线建议
基础:理解虚拟地址与物理地址的概念进阶:掌握多级页表的查找流程实践:在裸机环境下手动配置 MMU高级:研究操作系统的内存管理实现(如 Linux)
参考资料:
ARM Architecture Reference Manual(ARMv7-A 和 ARMv8-A)Intel 64 and IA-32 Architectures Software Developer’s ManualRISC-V Privileged Architecture SpecificationLinux 内核源码(arch/arm/mm/, mm/)《Understanding the Linux Kernel》(Daniel P. Bovet)