通过一个具体的用户空间程序案例,详细拆解ARMv7-A架构下缺页中断(Page Fault)处理的原理。这是一个现代操作系统(如Linux)实现按需分页的关键机制。
案例:用户程序使用malloc申请内存并首次写入
1.场景描述:
用户程序(运行在 ARMv7-A 处理器的用户模式 USR_MODE)调用 malloc(sizeof(int)) 申请一个整型变量的内存。
malloc 通常不会立即分配物理内存页,它只是在内核为该进程维护的虚拟地址空间中找到一段足够大的空闲虚拟地址区域(比如 0x00008000 - 0x00008FFF),标记为已用,并返回起始地址(例如 0x00008000)给程序。
内核在进程的页表中,将虚拟地址 0x00008000 对应的页表项,标记为无效(Invalid)。具体表现为 PTE 中的 valid 位为 0(或者特定的 “not present” 状态)。但页表项中关于该页后续所需权限(如可读 R、可写 W、用户可访问 U)的信息可能已经设置好。 这一步是“承诺”虚拟地址,但不给“货”(物理页帧)。
2.触发缺页中断:
用户程序获得地址 0x00008000 后,尝试执行一条指令向该地址写入数据,比如 STR R1, [R0](假设 R0 保存着 0x00008000)。
CPU(具体来说是其中的 MMU)在执行这条存储指令时,需要将虚拟地址 0x00008000 转换成物理地址。
MMU 查找当前进程的页表:
L1页表(主页表):根据虚拟地址的高位(0x00008)找到对应的 L1页表项(L1 Descriptor)。该 L1 项应该指向一个 L2页表(二级页表)。
L2页表:根据虚拟地址的中位(0x00008000 中的中间几位),在 L2 页表中找到对应的 L2页表项(L2 Descriptor / Page Table Entry, PTE)。
MMU 检查该 L2 PTE 的状态,发现 valid 位为 0(无效),表明这个虚拟页目前没有映射到任何物理页帧。
检测到无效页表项是触发缺页中断的直接原因。 MMU 会向 CPU 核心报告一个 数据中止异常。
3.ARMv7-A 异常入口:
CPU 核心检测到 MMU 报告的数据中止异常后,会立即:
切换到中止模式 (ABT_MODE):ARMv7-A 为不同的异常类型分配了特定的处理器模式,拥有独立的栈指针 (SP_abt) 和链接寄存器 (LR_abt)。
保存关键状态:将发生异常的指令的下一条指令地址存入 LR_abt。将当前 CPSR 复制到 SPSR_abt(Saved Program Status Register for Abort)。
设置CPSR:关闭中断(设置 I 位),切换至 ABT_MODE。
跳转到异常向量:根据 异常向量表基地址(通常配置在 VBAR 寄存器中,例如 0xFFFF0000) + 数据中止异常的偏移量 0x10,跳转到数据中止异常的处理程序入口点 0xFFFF0010(实践中,这个入口通常是一条跳转到更复杂处理函数的指令)。
4.操作系统缺页中断处理程序(以Linux内核为例):
CPU 核心现在在 ABT_MODE 下,开始执行内核中处理数据中止异常的代码。
a.获取缺页信息:
通过读协处理器 CP15 寄存器获取关键信息:
DFAR (Data Fault Address Register, CP15 c6): 保存导致中止的虚拟地址 (0x00008000)。这就是用户程序试图访问的地址。
IFSR (Instruction Fault Status Register, CP15 c5): 保存故障状态。包含中断原因、访问类型(读/写)、发生故障的访问域等关键信息。例如,IFSR 的位会明确指示是“段转换错误”(L1错误)还是“页转换错误”(L2错误,即我们这里的缺页),以及是读访问还是写访问触发的。
b.保存上下文:
内核需要保存当前被打断的用户进程在 USR_MODE 下的完整寄存器状态 (r0-r15, CPSR),以便之后恢复执行。这部分通常使用内核栈(可能是进程内核栈的一部分)。
c.查询虚拟地址状态:
内核处理程序(通常是 do_page_fault 或类似的函数)使用 DFAR 地址 (0x00008000),查询当前进程的内存描述符结构 (如 mm_struct)。
检查这个虚拟地址 0x00008000 是否属于当前进程的合法地址空间范围。
检查访问类型(从 IFSR 得知是写入操作)是否符合该虚拟内存区域 (vma->vm_flags) 的权限(如 VM_WRITE)。如果权限不足(比如试图写入只读区域),则可能引发 SIGSEGV 信号终止进程。
d.在我们的案例中:
地址合法,且 malloc 分配的地址通常是可写的。因此内核确定这是一个合法的请求调页 (Demand Paging) 场景。
5.分配物理内存页并更新页表:
a.分配物理页帧:
内核调用物理内存分配器(如 Buddy Allocator),分配一页(通常是 4KB)空闲的物理页帧,获取其物理地址(例如 0x2AAAA000)。
b.创建有效的页表项:
找到虚拟地址 0x00008000 对应的 L2 PTE(之前无效那个)。
设置 valid 位为 1。
设置 物理页帧号 (PFN) 为 0x2AAAA(物理地址 0x2AAAA000 的高位部分)。
设置页权限位:U (用户可访问), R, W (可读写)。
根据需要设置其他位(如缓存策略)。
c.刷新 TLB:
CPU 核心内部缓存的页表信息(TLB - Translation Lookaside Buffer)包含旧条目(无效的那个)。在更新页表之后,必须使包含该虚拟地址的 TLB 条目失效,确保 MMU 在下一次访问时使用新的有效 PTE。ARMv7-A 通过执行 CP15 c8 操作(如 TLBIALL 或更精确的 TLBIMVA)来完成。
6.返回用户空间并重试指令:
a.内核缺页处理程序完成上述步骤后:
恢复之前保存的进程上下文(用户态寄存器)。
修改 LR_abt 或 SPSR_abt 中的返回地址: 让它指向触发缺页中断的那条指令 (STR R1, [R0]),而不是它的下一条指令。
执行一个特殊的返回指令(如 movs pc, lr)。这会将 SPSR_abt 复制回 CPSR(恢复模式、中断状态等),并跳转到 LR_abt 指定的地址(触发异常的指令 STR R1, [R0])。
CPU 核心切换回用户模式 (USR_MODE),重新执行 STR R1, [R0] 指令。
这次,MMU 转换虚拟地址 0x00008000:
L1 查找正常。
L2 查找找到有效的 PTE (valid=1, PFN=0x2AAAA, AP=RW)。
MMU 成功将虚拟地址 0x00008000 转换为物理地址 0x2AAAA000。
数据 (R1 的值) 被成功写入物理地址 0x2AAAA000 对应的内存位置。
程序继续执行下一条指令。
7.总结与ARMv7-A要点强调:
a.触发条件:
应用程序访问了一个映射存在但页表项被标记为无效的虚拟地址(通常是首次访问 malloc 分配的内存)。MMU负责检测此无效状态。
b.异常机制:
MMU 检测到无效页表项或权限错误时,触发数据中止异常。
处理器自动切换至 ABT_MODE,保存 PC+4/PC+8 (取决于具体实现和指令长度) 到 LR_abt,保存 CPSR 到 SPSR_abt。
通过异常向量表跳转到内核处理程序。
c.关键信息获取:
DFAR (CP15 c6):获取故障虚拟地址 (如 0x00008000),是处理的基础。
IFSR (CP15 c5):获取详细的故障原因(页转换失败、读/写、权限错误)。
d.内核职责:
上下文保存/恢复: 无缝切换的核心。
地址与权限检查: 验证 DFAR 是否有效、操作是否被允许,非法访问会导致程序终止 (SIGSEGV)。
物理页分配: 通过 Buddy Allocator 等获取物理页帧。
页表更新: 将对应的 PTE 更新为有效状态,填写物理页帧号,设置正确权限。
TLB 失效: 至关重要! CP15 c8 操作确保 MMU 使用新 PTE。不执行此步,即使页表已改,MMU 仍可能使用缓存中的旧无效项。
e.恢复执行:
内核修改返回地址到触发异常的指令。
处理器返回到用户模式 (SPSR->CPSR) 并跳转到触发异常的指令 (LR_abt)。
指令重新执行,这次 MMU 可以成功转换,访问完成。
案例价值: 这个 malloc + 首次写入的案例,清晰地展示了 ARMv7-A 的硬件机制(MMU、异常、CP15寄存器)如何与操作系统内核协作,实现了按需分配物理内存的优化策略,使程序可以使用远超物理内存大小的虚拟地址空间。整个过程对应用程序员透明,极大提高了系统的资源利用率和灵活性。同时突出了 DFAR, IFSR, TLB 失效 (CP15 c8) 这些 ARMv7-A 特有的关键操作点。