缺页异常的处理

    在实际需要某个虚拟内存区的数据之前,虚拟和物理内存之间的关联是不会建立的,我们就默认程序不着急用,先去处理认为重要的事情。如果要访问一个页面这而它却不在物理内存中,处理器自动引发一个缺页异常,内核必须处理此异常。这时需要考虑的几个问题是:1、出错地址有什么特点?2、出错的地址有相对应的现有映射吗?3、要怎样获取该区域的数据?

    看过代码(还有网上关于这块的流程图)之后感觉实在太复杂了。处理缺页异常的函数是

void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)

下面就沿着这个函数开始看内核是怎么处理的。

    首先看传进来的参数是什么。regs包含了当异常发生时处理器中寄存器的值,而error_code有(在异常发生时由控制单元压入栈中),如果0位为1表示是在访问一个不存在的页时引起的异常,否则表示异常是访问权限引起的。如果1位为1表示操作时“读”或者“执行”,否则为“写”操作。如果2为1表示出于内核态,否则为用户态。如果3位为1表示检测到使用了保留位。4位为1表示1表示缺页异常是在取指令的时候出现的。这是我们现在知道的,不过我们从《计算器基础中》知道了CR2的作用,这些大概就是我们掌握的信息,下面开始看函数处理的过程。

取得出错时的线性地址:

address = read_cr2();

如果address>=TASK_SIZE我们因为异常而进入内核虚拟内存空间,这种情况下不能获取任何锁(可能处于中断或临界区中),只应当从主页表中复制信息,不允许其他操作:

#ifdef CONFIG_X86_32
if (unlikely(address >= TASK_SIZE)) {
#else
if (unlikely(address >= TASK_SIZE64)) {
#endif
if (!(error_code & (PF_RSVD|PF_USER|PF_PROT)) && vmalloc_fault(address) >= 0)
return;
if (spurious_fault(address, error_code))
return;
goto bad_area_nosemaphore;
}

上面代码说明异常发生在内核空间,调用vmalloc(模块映射区)的错误处理函数vmalloc_fault(在下面再详细地说,这里出现了分支),如果没有解决问题那么可能是由留下的旧的TLB引起的(具体的处理过程在后面给出)。为什么这里取得mm信号量修复指令造成的缺页异常会引起死锁?

上面看完了访问内核空间时异常的处理(当然细节还没有说),下面开始看发生在用户空间的情况:

if (unlikely(in_atomic() || !mm || current->pagefault_disabled))
goto bad_area_nosemaphore;

如果正在运行在一个中断中、或者这时没有上下文、或者是在一个临界区,这时是不能处理错误的(是不是因为正在执行关键的任务?)。为了检查进程所拥有的线性区以决定引起缺页的线性地址是否包含在进程的线性地址中,为此必须获得进程的mmap_sem信号量(如果不是内核bug或者硬件错误,那么产生缺页的时候就不会占有mmap_sem,但是万一有的话就会发生死锁):

if (!down_read_trylock(&mm->mmap_sem)) {
if ((error_code & PF_USER) == 0 && !search_exception_tables(regs->ip))
goto bad_area_nosemaphore;
down_read(
&mm->mmap_sem);
}

使用down_read函数来等待这个信号量被释放,下面就开始处理该进程的线性地址空间了,

vma = find_vma(mm, address);
if (!vma)
goto bad_area;
if (vma->vm_start <= address)
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;

如果找不到vma显然是不合适的,但是如果找到了vma并且address在其中就要去查看访问权限了,如果address不在其中(这里需要注意的是栈是向下增长的,也就是说这个地址刚好在栈的下方),这时需要检查vm_flags。当然仅靠检查这个标志是不行的(不然栈下面的地址不是可以随便访问了嘛):

if (error_code & PF_USER) {
if (address + 65536 + 32 * sizeof(unsigned long) < regs->sp)
goto bad_area;
}
if (expand_stack(vma, address))
goto bad_area;

通过address的值和regs->sp值的比较进一步判断是不是用户的栈地址。因为pusha指令是在访问内存后才将其压入栈中的,所有预留出32*sizeof(unsigned long)的空间来存放要pusha的值(那65536又是为什么呢?)。下面调用expand_stack来尝试扩大栈的大小,如果成功的话就应该检查是不是访问权限导致的错误。

如果在一个“good_aread”,那么我们首先检查是不是访问权限出问题了:

good_area:
si_code
= SEGV_ACCERR;
write
= 0;
switch (error_code & (PF_PROT|PF_WRITE)) {
default:
case PF_WRITE:
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
write
++;
break;
case PF_PROT:
goto bad_area;
case 0:
if (!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE)))
goto bad_area;
}

如果是因为写操作引起的,并且要写的地址也是可写的(为什么会出现这种情况?很显然是写时复制嘛,这个在以后的文章中详细的来说明)。其他的情况就跳到bad_area了,这个很好理解:

survive:
fault
= handle_mm_fault(mm, vma, address, write);
if (unlikely(fault & VM_FAULT_ERROR)) {
if (fault & VM_FAULT_OOM)
goto out_of_memory;
else if (fault & VM_FAULT_SIGBUS)
goto do_sigbus;
BUG();
}
if (fault & VM_FAULT_MAJOR)
tsk
->maj_flt++;
else
tsk
->min_flt++;

上面的这这段代码是紧接着处理上面的“可写但是不在”的情况的。这段代码显示调用handle_mm_fault方法,然后对返回的结果一通判断。那么handle_mm_fault方法是做什么的?这个函数就进入了请求分页/写时复制的处理程序中了(详细地在以后的日记中再写)。对handle_mm_fault函数返回的参数fault,VM_FAULT_OOM表示没有足够的内存了,VM_FAULT_SIGBUS的话要向进程发送SIGBUS信号,VM_FAULT_MAJOR表示缺页会导致进程睡眠,

#ifdef CONFIG_X86_32
if (v8086_mode(regs)) {
unsigned
long bit = (address - 0xA0000) >> PAGE_SHIFT;
if (bit < 32)
tsk
->thread.screen_bitmap |= 1 << bit;
}
#endif
up_read(
&mm->mmap_sem);
return;

下面开始处理bad_area里面的代码,

bad_area:
up_read(
&mm->mmap_sem);
bad_area_nosemaphore:
if (error_code & PF_USER) {
local_irq_enable();
if (is_prefetch(regs, address, error_code))
return;
if (is_errata100(regs, address))
return;
if (show_unhandled_signals && unhandled_signal(tsk, SIGSEGV) && printk_ratelimit()) {
printk(
"%s%s[%d]: segfault at %lx ip %08lx sp %08lx error %lx",
task_pid_nr(tsk)
> 1 ? KERN_INFO : KERN_EMERG,
tsk
->comm, task_pid_nr(tsk), address, regs->ip,
regs
->sp, error_code);
print_vma_addr(
" in ", regs->ip);
printk(
"\n");
}
tsk
->thread.cr2 = address;
tsk
->thread.error_code = error_code | (address >= TASK_SIZE);
tsk
->thread.trap_no = 14;
force_sig_info_fault(SIGSEGV, si_code, address, tsk);
return;
}
if (is_f00f_bug(regs, address))
return;

很多情况都调到了bad_area来处理,不过从这里得不到什么好结果。。。释放信号量之后首先判断是不是在用户态出的问题,如果查询异常表之后还不能解决问题就向该进程发送一个“SIGSEGV”信号。force_sig_info_fault函数确信进程不会忽略或阻塞该信号,附加信息通过si_code传送过去(表示出现了一个什么问题)。用户态才处理完了,下面看内核态的:

no_context:
if (fixup_exception(regs))
return;
if (is_prefetch(regs, address, error_code))
return;
if (is_errata93(regs, address))
return;
bust_spinlocks(
1);
show_fault_oops(regs, error_code, address);
tsk
->thread.cr2 = address;
tsk
->thread.trap_no = 14;
tsk
->thread.error_code = error_code;
die(
"Oops", regs, error_code);
bust_spinlocks(
0);
do_exit(SIGKILL);

如果发生在内核态也可以分成两种情况,一种情况是把线性地址当做系统调用的参数(错误的系统调用参数)引起的异常,另一种只能说是系统的bug。对于第一种情况想进程发送一个SIGSEGV信号(或者返回一个错误码终止程序处理)。如果是第二种情况,那只能杀死这个进程了。

do_sigbus:
up_read(
&mm->mmap_sem);
if (!(error_code & PF_USER))
goto no_context;
if (is_prefetch(regs, address, error_code))
return;
tsk
->thread.cr2 = address;
tsk
->thread.error_code = error_code;
tsk
->thread.trap_no = 14;
force_sig_info_fault(SIGBUS, BUS_ADRERR, address, tsk);

这个处理的过程大概就理顺了,根据上面要考虑的几种情况很容易想到这个顺序的(详细的情况以后会写出来)。

-------------------------

个人理解,欢迎拍砖。

原文地址:https://www.cnblogs.com/ggzwtj/p/2140217.html