1. 前言
本专题我们开始学习内存管理部分,本文为缺页中断处理相关学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
Linux的缺页异常(page fault)必须区分两种情况:由编程错误引发的异常;由引用属于进程地址空间但尚未分配物理页框的页引起的异常。具体发生page fault的情况:
1.如果写地址落在VMA区域,且权限合法
访问地址落在VMA区域且权限为R+W,页表权限为R,则第一次写时会发生page fault ,内核会修改页表权限为R+W,并为之申请一页内存;
2.如果访问地址落在VMA区域,但是访问权限不合法
如果访问地址没有写权限,写入会引发page fault, 且内核发出SIGV
3.如果访问地址落在非法区域
如果访问地址落在空白区域,即不在任何一个VMA,则引发page fault且发出SEGV,杀死进程
在之前介绍brk系统调用和mmap系统调用时,它们只是建立了进程地址空间(VMA),用户空间可以看到虚拟内存,但是没有建立虚拟内存与物理内存之间的映射,即没有创建相应的页表项。当进程访问这些还没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常(也称缺页中断),Linux会处理此异常,它依赖于处理器架构,缺页异常底层的处理流程在内核代码中特定体系架构的部分。
kernel版本:5.10
平台:arm64
2. page fault的底层处理
esr_el1为arm64异常综合信息寄存器,寄存器结构如上,其中bit31-26为异常类型(EC), bit24-0为具体的异常指令编码(ISS),不同的异常类型ISS有不同的编码方式
可以根据esr.EC简单判断异常的类型,ESR支持几十种不同的异常类型,page fault为数据异常,此处以发生在EL1的数据异常为例
对于esr寄存器不同的异常类型EC有不同的ISS表(bit24-0),本例中对于数据异常种类,ISS表结构如下:
其中DFSC为Data Fault Status Code的缩写,指示了访问权限错误或页表转换错误等。Linux内核定义了fault_info结构体,指示了不同的DFSC对应了不同的回调函数。
far寄存器是Fault Address Register的缩写,这个寄存器保存了发生异常的虚拟地址
综上,arm64处理数据异常的底层逻辑如下:
- arm64异常分为同步异常和异步异常,当数据异常发生时,会跳转到arm64同步异常向量处理;
- 通过读取esr寄存器,进一步通过esr.ec来确定异常分类,确定分类处理函数;
- 不同的异常分类EC对应不同的ISS结构,对于数据异常也对应特定的结构,此时ISS的DFSC描述了不同的数据异常编码;
- arm64针对不同的DFSC编码,定义了fault_info表,表中针对每种数据异常,都定义了回调函数。
3. el1_sync
page fault作为arm64的一种同步异常而被处理,其主要的异常处理函数位于arch/arm64/kernel/entry.S, 其异常处理向量为el1_sync->el1_sync_handler
el1_sync
|--el1_sync_handler
|--esr = read_sysreg(esr_el1)
|--switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_DABT_CUR:
case ESR_ELx_EC_IABT_CUR:
el1_abort(regs, esr);
|--far = read_sysreg(far_el1)
|--far = untagged_addr(far)
|--do_mem_abort(far, esr, regs)
break;
...
}
esr_el1:为arm64异常综合信息寄存器, esr为读取的寄存器的值
ESR_ELx_EC(esr): ESR_ELx_EC(esr)可以根据esr.EC简单判断异常的类型,ESR支持几十种不同的异常类型,此处以发生在EL1的数据异常ESR_ELx_EC_DABT_CUR(EC为0x25)为例,处理函数为el1_abort
el1_abort:读取far寄存器,far寄存器是Fault Address Register的缩写,这个寄存器保存了发生异常的虚拟地址
do_mem_abort:根据发生异常的虚拟地址addr进行处理
|- -do_mem_abort
do_mem_abort的参数far为读取的far_el1寄存器,保存了发生异常的虚拟地址,esr为esr_el1寄存器,为异常综合信息寄存器,regs为发生异常时的pt_regs指针,对于esr寄存器不同的异常类型EC有不同的ISS表(bit24-0),本例中对于数据异常种类,其中DFSC为Data Fault Status Code的缩写,指示了访问权限错误或页表转换错误等。Linux内核定义了fault_info结构体,指示了不同的DFSC对应了不同的回调函数。
do_mem_abort(far, esr, regs)
|--const struct fault_info *inf = esr_to_fault_info(esr);
|--inf->fn(addr, esr, regs)
esr_to_fault_info:当发生数据异常时,根据esr寄存器的ISS字段获取到DFSC字段,对于DFSC的每一个数据异常码都对应了一个struct fault_info结构体变量,所有的struct fault_info结构体变量形成一个fault_info表,通过DFSC就可以获取到对应的struct fault_info,从而得到相应的回调函数。
inf->fn(addr, esr, regs):针对缺页异常有常见的几种修复方案,fault_info中相应的回调函数主要包括:
#arch/arm64/mm/fault.c
static const struct fault_info fault_info[] = {
.......
{ do_bad, SIGKILL, SI_KERNEL, "level 1 address size fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
{ do_alignment_fault, SIGBUS, BUS_ADRALN, "alignment fault" },
{ do_sea, SIGKILL, SI_KERNEL, "level 1 synchronous parity error (translation table walk)" }, // Reserved when RAS is implemented
- do_translation_fault:处理页表转换相关的异常错误
- do_page_fault:处理页表访问或权限相关的异常错误
- do_alignment_fault:处理与对齐相关的异常错误
- do_bad:处理与位置的错误或硬件相关的错误,如TLB冲突等
- do_sea:
后续将以do_page_fault为例,说明缺页异常的主要过程。
参考文档
- 奔跑吧,Linux内核
- ARM TRM