MMU(内存管理单元)分析

4978 Views

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 + ((0x40000000 >> 12) & 0xFF) * 4

通过 /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)

‎微信读书 App
wps冻结窗口怎么设置