malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,系统触发缺页中断,缺页中断机制根据所访问页面的状态来分配物理页面并建立映射关系。
触发缺页中断的情况有两种 , 第一,程序访问了非法地址;第二,访问的地址是合法的,但是该地址还未分配物理页框。
缺页中断要经历汇编和C语言两个阶段:
1 汇编阶段
当程序访问的虚拟页面没有进行过物理页面的映射时,会通过发生缺页中断来分配和映射物理页面。发生缺页中断时,处理器会跳转到异常向量表 Data abort 向量中开始执行缺页中断的汇编阶段,这个阶段与处理器架构紧密联系,例如对于ARMv7-A架构,汇编处理流程为:__vectors_start -> vector_dabt -> __dabt_usr/__dabt_svc -> dabt_helper -> v7_early_abort 。
从缺页中断的汇编处理流程可以看到对于不同的处理器模式,汇编流程有所不同,比如进入data abort之前处于usr模式,那么跳转到__dabt_usr;如果处于svc模式,那么跳转到__dabt_svc;否则跳转到__dabt_invalid。
实际上,进入异常向量前Linux只能处于usr或者svc两种模式之一。这是因为irq等其他异常在跳转表中都要经过vector_stub宏,而不管之前是哪种状态,这个宏都会将CPU状态改为svc模式。usr模式即Linux中的用户态模式,svc即内核模式。
以下是ARM 处理器模式的总结。
1.1 ARM处理器的几种模式
处理器模式 | 描述 |
---|---|
usr(user) | 属于正常的用户模式,ARM处理器正常的程序执行状态,不能切换到其他模式 |
fiq(fiq) | 快速中断模式,对高速数据传输或通道处理 |
irq(irq) | 对一般情况下的中断进行处理 |
svc(Supervisor) | 操作系统保护模式,系统复位或软件中断响应时进入此模式 |
abt(abort) | 数据访问终止模式,当数据或指令预取终止时进入该模式,可用于处理存储器故障、实现虚拟存储器和存储器保护 |
sys(system) | 系统模式,运行具有特权的操作系统任务,可以直接切换到其他模式 |
und(undefined) | 未定义指令终止模式,处理未定义的指令陷阱,当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真 |
缺页异常中断,汇编阶段最后调用do_DataAbort函数进入到c语言世界,其位置在 arch/arm/mm/abort_ev7.S :
ENTRY(v7_early_abort)
mrc p15, 0, r1, c5, c0, 0 @ get FSR
mrc p15, 0, r0, c6, c0, 0 @ get FAR
···
b do_DataAbort
ENDPROC(v7_early_abort)
ARM的MMU中有如下两个与存储访问失效相关的寄存器:
- c5:失效状态寄存器(Data Fault Status Register , FSR)
- c6:失效地址寄存器(Data Fault Address Register,FAR)
当发生存储访问失效时,失效状态寄存器FSR会反映所发生的存储失效的相关信息,包括存储访问所属域和存储访问类型等,表明是什么原因导致data abort。同时失效地址寄存器会记录访问失效的虚拟地址。汇编函数v7_early_abort通过读取协处理器的寄存器c5和c6来获得导致data abort的相关信息,然后调用C语言的do_DataAbort()函数。
1.2 协处理器
基于ARM的系统中,存储系统的操作通常由协处理器CP15来完成,CP15有16个32位的寄存器,其编号为0~15。访问CP15寄存器的指令主要是MCR和MRC这两个指令。
- MCR: ARM处理器寄存器到协处理器寄存器的数据传送指令(写入协处理器寄存器)。
- MRC: 协处理器寄存器到ARM处理器寄存器的数据传送指令(读出协处理器寄存器)。
1.2.1 指令格式
MRC{cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2>
MCR{cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2>
cond:为指令执行的条件码。当cond忽略时指令为无条件执行。
Opcode_1:协处理器的特定操作码. 对于CP15寄存器来说,Opcode_1=0 。
Rd:作为源寄存器的ARM寄存器,其值将被传送到协处理器寄存器中,或者将协处理器寄存器的值传送到 该寄存器里面 ,通常为R0 。
CRn:作为目标寄存器的协处理器寄存器,其编号是C0~C15。
CRm:协处理器中附加的目标寄存器或源操作数寄存器。如果不需要设置附加信息,将CRm设置为C0,否则结果未知 。
Opcode_2:可选的协处理器特定操作码。(用来区分同一个编号的不同物理寄存器,当不需要提供附加信息时,指定为0)。
关于详细的协处理器知识请移步至:ARM协处理器介绍
2 C语言阶段
缺页中断的C语言阶段是从调用do_DataAbort函数开始。下面看看该函数都做了什么工作?
asmlinkage void do_DataAbort(
unsigned long addr, //导致异常的内存地址
unsigned int fsr, //协处理器CP15寄存器中的值
struct pt_regs *regs //进入异常时CPU寄存器的值
)
{
const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
struct siginfo info;
if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))
return;
pr_alert("Unhandled fault: %s (0x%03x) at 0x%08lx\n",
inf->name, fsr, addr);
show_pte(current->mm, addr);
clear_siginfo(&info);
info.si_signo = inf->sig;
info.si_errno = 0;
info.si_code = inf->code;
info.si_addr = (void __user *)addr;
arm_notify_die("", regs, &info, fsr, 0);
}
进入do_DataAbort函数后,首先根据 fsr 的值从全局数组 fsr_info 中获得处理此异常的 fsr_info 结构,然后调用 fsr_info 结构中的 fn 函数来处理该种异常。如果 fn 函数为空,那么就要调用 arm_notify_die 函数。
void arm_notify_die(const char *str, struct pt_regs *regs,
struct siginfo *info, unsigned long err, unsigned long trap)
{
if (user_mode(regs)) {
current->thread.error_code = err;
current->thread.trap_no = trap;
force_sig_info(info->si_signo, info, current);
} else {
die(str, regs, err);
}
}
该函数首先调用 user_mode 函数来判断是否是 usr 模式,如果是 usr 模式,那么就发送一个信号给导致异常的任务(注意这里的任务可能是一个线程)。具体哪个信号被发送是由 struct fsr_info 结构体中定义的值决定,一般来说,是一个能使进程停止的信号,比如 SIGSEGV 等等(SIGSEGV之类的信号即使被发给一个线程,也会停止整个进程)。如果是 svc 模式,即内核模式,那么就调用die函数,这是 kernel 处理 Oops 的标准函数。
2.1 fsr_info
fsr_info是一个 struct fsr_info 类型的全局数组,定义在 arch/arm/mm/fsr_2level.c中:
static struct fsr_info fsr_info[] = {
/*
* The following are the standard ARMv3 and ARMv4 aborts. ARMv5
* defines these to be "precise" aborts.
*/
{ do_bad, SIGSEGV, 0, "vector exception" },
{ do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
{ do_bad, SIGKILL, 0, "terminal exception" },
{ do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
{ do_bad, SIGBUS, 0, "external abort on linefetch" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },
{ do_bad, SIGBUS, 0, "external abort on linefetch" },
{ do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" },
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
{ do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" },
{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },
{ do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },
{ do_bad, SIGBUS, 0, "external abort on translation" },
{ do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" },
{ do_bad, SIGBUS, 0, "external abort on translation" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" },
/*
* The following are "imprecise" aborts, which are signalled by bit
* 10 of the FSR, and may not be recoverable. These are only
* supported if the CPU abort handler supports bit 10.
*/
{ do_bad, SIGBUS, 0, "unknown 16" },
{ do_bad, SIGBUS, 0, "unknown 17" },
{ do_bad, SIGBUS, 0, "unknown 18" },
{ do_bad, SIGBUS, 0, "unknown 19" },
{ do_bad, SIGBUS, 0, "lock abort" }, /* xscale */
{ do_bad, SIGBUS, 0, "unknown 21" },
{ do_bad, SIGBUS, BUS_OBJERR, "imprecise external abort" }, /* xscale */
{ do_bad, SIGBUS, 0, "unknown 23" },
{ do_bad, SIGBUS, 0, "dcache parity error" }, /* xscale */
{ do_bad, SIGBUS, 0, "unknown 25" },
{ do_bad, SIGBUS, 0, "unknown 26" },
{ do_bad, SIGBUS, 0, "unknown 27" },
{ do_bad, SIGBUS, 0, "unknown 28" },
{ do_bad, SIGBUS, 0, "unknown 29" },
{ do_bad, SIGBUS, 0, "unknown 30" },
{ do_bad, SIGBUS, 0, "unknown 31" },
};
fsr_info 数组中的每一项对应一个 struct fsr_info 结构:
struct fsr_info {
int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs); //异常处理函数
int sig;//当 fn 函数执行失败或为空时,内核发送给进程的信号
int code;//
const char *name;//表示这条异常的名称,从这个字串中可以看出发生什么异常
};
kernel 对于大多数异常都会调用 do_bad 函数:
static int do_bad(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
return 1; /* "fault" */
}
该函数只是简单地返回1,让内核去调用后面的 arm_notify_die 函数来处理该异常。
kernel有几个异常是需要单独处理:
- do_translation_fault : 段转换错误,即找不到二级页表
- do_page_fault:页表错误,即线性地址无效,没有对应的物理地址
- do_sect_fault:段权限错误,即二级页表权限错误
- do_page_fault:页权限错误
2.2 do_page_fault()
do_page_fault()函数是缺页中断的核心函数:
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk;
struct mm_struct *mm;
int sig, code;
vm_fault_t fault;
unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
/*如果打开了kprobe,并且由内核态触发,则交由kprobe处理完成并返回*/
if (notify_page_fault(regs, fsr))
return 0;
/*获得当前进程*/
tsk = current;
/*获得当前进程的内存描述符*/
mm = tsk->mm;
/*进入该异常之前中断处于打开状态,则还要保持中断打开,意味着在下面的异常处理中还可以被中断打断*/
if (interrupts_enabled(regs))
local_irq_enable();
/*
* faulthandler_disabled():判断是否关闭了page_fault 和 判断当前状态是否处于中断上下文或禁止抢占状态,
* 若是,则调用__do_kernel_fault 触发kernel panic
* !mm : 若mm不存在,则判断是内核线程,则则调用__do_kernel_fault 触发kernel panic。说明内核线程不会
* 缺页异常。
*/
if (faulthandler_disabled() || !mm)
goto no_context;
/*判断是否由用户态触发进来*/
if (user_mode(regs))
flags |= FAULT_FLAG_USER;
/*判断是否由可写权限触发进来*/
if (fsr & FSR_WRITE)
flags |= FAULT_FLAG_WRITE;
/*尝试获取当前进程的mm读写信号量,返回1表示成功获得锁,返回0表示锁已被别人占用*/
if (!down_read_trylock(&mm->mmap_sem)) {
/*锁在内核空间被占用,并且在exception table没有查询到该地址,则触发kernel panic*/
if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))
goto no_context;
retry:
/*锁在用户空间被占用,则调用down_read()函数来睡眠等待锁持有者释放锁*/
down_read(&mm->mmap_sem);
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case, we'll have missed the might_sleep() from
* down_read()
*/
might_sleep();
#ifdef CONFIG_DEBUG_VM
if (!user_mode(regs) &&
!search_exception_tables(regs->ARM_pc))
goto no_context;
#endif
}
/*上面已经将内核态触发的异常过滤了,内核态的异常走no_context,来到这里说明是用户态引发的异常*/
fault = __do_page_fault(mm, addr, fsr, flags, tsk);
/* If we need to retry but a fatal signal is pending, handle the
* signal first. We do not need to release the mmap_sem because
* it would already be released in __lock_page_or_retry in
* mm/filemap.c. */
if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)) {
if (!user_mode(regs))
goto no_context;
return 0;
}
/*
* Major/minor page fault accounting is only done on the
* initial attempt. If we go through a retry, it is extremely
* likely that the page will be found in page cache at that point.
*/
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
if (fault & VM_FAULT_MAJOR) {
tsk->maj_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
regs, addr);
} else {
tsk->min_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
regs, addr);
}
if (fault & VM_FAULT_RETRY) {
/* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
* of starvation. */
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED;
goto retry;
}
}
up_read(&mm->mmap_sem);
/*
* Handle the "normal" case first - VM_FAULT_MAJOR
*/
if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
return 0;
/*
* If we are in kernel mode at this point, we
* have no context to handle this fault with.
*/
if (!user_mode(regs))
goto no_context;
if (fault & VM_FAULT_OOM) {
/*
* We ran out of memory, call the OOM killer, and return to
* userspace (which will retry the fault, or kill us if we
* got oom-killed)
*/
pagefault_out_of_memory();
return 0;
}
if (fault & VM_FAULT_SIGBUS) {
/*
* We had some memory, but were unable to
* successfully fix up this page fault.
*/
sig = SIGBUS;
code = BUS_ADRERR;
} else {
/*
* Something tried to access memory that
* isn't in our memory map..
*/
sig = SIGSEGV;
code = fault == VM_FAULT_BADACCESS ?
SEGV_ACCERR : SEGV_MAPERR;
}
__do_user_fault(tsk, addr, fsr, sig, code, regs);
return 0;
no_context:
__do_kernel_fault(mm, addr, fsr, regs);
return 0;
}
跟踪该函数,得到以下这样的调用流程: