分类:
2011-12-16 20:38:25
原文地址:Linux内存管理之页面异常处理 作者:xgr180
------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
Linux页面异常处理是一个复杂的过程.它用来处理内存访问各种异常,因为这部份内容涉及到了系统调用的相关部份,所以,我们暂且忽略这部份信息,只要知道,如果有内存访问异常情况,就会转入到do_page_fault()中处理,关于系统调用这部份,我们之后再给出详细的分析,详情请关注本站更新.
同以往一样,本文的代码是基于linux-
/*
参数的含义:
regs:里面保存着异常情况时,各CPU寄存器的值.
Error_code:错误代码.根据作者的注释,代码中各字位的含义如下:
第0位: 0:没有这个页面 1:权限不对
第1位: 0:读错误 1:写错误
第2位: 0:内核 1:用户空间
*/
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct * vma;
unsigned long address;
unsigned long page;
int write;
siginfo_t info;
//发生异常时,CPU把异常的地址压入CR2中.此段嵌入汇编的含意是将CR2中的值取出放到
//address变量中
__asm__("movl %%cr2,%0":"=r" (address));
//事情通知链表
if (notify_die(DIE_PAGE_FAULT, "page fault", regs, error_code, 14,
SIGSEGV) == NOTIFY_STOP)
return;
//恢复中断,CR2中的值已经得到了保存
if (regs->eflags & (X86_EFLAGS_IF|VM_MASK))
local_irq_enable();
//取发生异常的处理 task_struct
tsk = current;
info.si_code = SEGV_MAPERR;
//条件编译.假设开关末打开
#ifdef CONFIG_X86_
/*
* On 4/4 all kernels faults are either bugs, vmalloc or prefetch
*/
/* If it's vm86 fall through */
if (unlikely(!(regs->eflags & VM_MASK) && ((regs->xcs & 3) == 0))) {
if (error_code & 3)
goto bad_area_nosemaphore;
goto vmalloc_fault;
}
#else
if (unlikely(address >= TASK_SIZE)) { //地址大于TASK_SIZE 说明错误发生在内核空间
if (!(error_code & 5)) //5=101 -> error_code = 010 || 000 内核空间的写/读地址错误
goto vmalloc_fault;
//发生在用户空间的,地址超出TASK_SIZE
goto bad_area_nosemaphore;
}
#endif
mm = tsk->mm;
/*
* If we're in an interrupt, have no user context or are running in an
* atomic region then we must not take the fault..
*/
if (in_atomic() || !mm)
goto bad_area_nosemaphore;
if (!down_read_trylock(&mm->mmap_sem)) {
if ((error_code & 4) == 0 &&
!search_exception_tables(regs->eip))
goto bad_area_nosemaphore;
down_read(&mm->mmap_sem);
}
//找到第一个结束地址大于address的VMA
vma = find_vma(mm, address);
//没有这样的VMA.说明异常地址是在进程地址堆栈的上方,非法
if (!vma)
goto bad_area;
//地址落在一个VMA区域中
if (vma->vm_start <= address)
goto good_area;
//VM_GROWSDOWN:向下增长,只有栈才有这样的属性.
//不属于栈而且又落在空洞中,非法
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (error_code & 4) {
//error_code = 1XX :在用户空间
//入栈一次是四个字节
//如果操作不是如入栈引起的,非法
if (address + 32 < regs->esp)
goto bad_area;
}
//只要栈顶没有到达下面的数据段或者MMAP映射区,系统都认为是合法的,扩大栈空间
//参考<
if (expand_stack(vma, address))
goto bad_area;
……
……
}
为了方便分析,我们理一下各标号的代码含义:
1: vmalloc_fault: 内核非连续空间的异常处理
我们在vmalloc/vfree的实现中可以看到(参考本站<
//内核非连续空间的异常处理(R/W)
vmalloc_fault:
{
//计算地址所对应页目录偏移值
int index = pgd_index(address);
unsigned long pgd_paddr;
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pte_t *pte_k;
//从CR3中取当前内核页目录
asm("movl %%cr3,%0":"=r" (pgd_paddr));
//发生异常的页目录项
pgd = index + (pgd_t *)__va(pgd_paddr);
//init_mm中相应的内核页目录项
pgd_k = init_mm.pgd + index;
//如果init_mm中没有相关信息,那么它就是一个不折不扣的错误了,转至no_contex处理
if (!pgd_present(*pgd_k))
goto no_context;
//分别取当前pmd与init_mm中的对应pmd
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
if (!pmd_present(*pmd_k))
goto no_context;
//更新
set_pmd(pmd, *pmd_k);
//取相应的页面项
pte_k = pte_offset_kernel(pmd_k, address);
if (!pte_present(*pte_k))
goto no_context;
//如果到这里没有异常的话,映射信息已经更新好了,返回
return;
}
2:no_context:我们可以看到,在vmalloc_fault中如果异常错误的话,就会转入到这个标号中进行.
这个标号首先它判断是否是由一个错误的系统调用参数引起的.如果是.则向相应进程发送SIGSEGV.如果是内核本身的错误,就打印出Oops错误.然后把内核挂起.代码如下:
no_context:
//地址修正:通常是到异常表里去找相关信息.这个函数的具体实现等到系统调用分析的时候再
//进行
if (fixup_exception(regs))
return;
//如果不是系统调用参数的错误,只可能是内核编程的错误了,Oops
if (is_prefetch(regs, address, error_code))
return;
/*
* Oops. The kernel tried to access some bad page. We'll have to
* terminate things with extreme prejudice.
*/
bust_spinlocks(1);
#ifdef CONFIG_X86_PAE
if (error_code & 16) {
pte_t *pte = lookup_address(address);
if (pte && pte_present(*pte) && !pte_exec_kernel(*pte))
printk(KERN_CRIT "kernel tried to execute NX-protected page - exploit attempt? (uid: %d)\n", current->uid);
}
#endif
if (address < PAGE_SIZE)
printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");
else
printk(KERN_ALERT "Unable to handle kernel paging request");
printk(" at virtual address %08lx\n",address);
printk(KERN_ALERT " printing eip:\n");
printk("%08lx\n", regs->eip);
asm("movl %%cr3,%0":"=r" (page));
page = ((unsigned long *) __va(page))[address >> 22];
printk(KERN_ALERT "*pde = %08lx\n", page);
/*
* We must not directly access the pte in the highpte
* case, the page table might be allocated in highmem.
* And lets rather not kmap-atomic the pte, just in case
* it's allocated already.
*/
#ifndef CONFIG_HIGHPTE
if (page & 1) {
page &= PAGE_MASK;
address &= 0x003ff000;
page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];
printk(KERN_ALERT "*pte = %08lx\n", page);
}
#endif
die("Oops", regs, error_code);
bust_spinlocks(0);
do_exit(SIGKILL);
3: bad_area标号: 地址空间以外的线性地址异常处理.如果线性地址落在vma区域的空洞中,又不是堆栈空间的扩展,则进程发生了错误.
bad_area:
up_read(&mm->mmap_sem);
bad_area_nosemaphore:
//我们可以看到bad_area与bad_area_nosemaphore区别,后者没有锁住信号量
if (error_code & 4) {
/* 4 = 100 -> error_code = 1xx 表示在用户空间*/
//如果是用户空间,则向该进程发送SIGSEGV信号
if (is_prefetch(regs, address, error_code))
return;
tsk->thread.cr2 = address;
/* Kernel addresses are always protection faults */
tsk->thread.error_code = error_code | (address >= TASK_SIZE);
tsk->thread.trap_no = 14;
info.si_signo = SIGSEGV;
info.si_errno = 0;
/* info.si_code has been set above */
info.si_addr = (void __user *)address;
force_sig_info(SIGSEGV, &info, tsk);
return;
}
4: good_area标号的处理.与上述几个标号的处理相比.good_area的处理相对要复杂一些,它主要是处理一些正常的访问.具体代码如下:
// good_area:处理进程空间内的线性地址
good_area:
info.si_code = SEGV_ACCERR;
write = 0;
// 3 = 011
switch (error_code & 3) { //3
default: /* 3: write, present */
#ifdef TEST_VERIFY_AREA
if (regs->cs == KERNEL_CS)
printk("WP fault at %08lx\n", regs->eip);
#endif
/* fall through */
case 2: /* write, not present */
// error_code:010 || 110 写一个不存在的页面
if (!(vma->vm_flags & VM_WRITE)) //区域没有可写的权限
goto bad_area;
write++; //write置为了-1
break;
case 1: /* read, present error_code = 001 || 101 读操作,但是权限不够*/
goto bad_area;
case 0: /* read, not present error_code = 000|100 读一个不存在的页面*/
if (!(vma->vm_flags & (VM_READ | VM_EXEC))) //没有相应的权限
goto bad_area;
}
survive:
//write = 1 :写操作 write = 0 :读操作
switch (handle_mm_fault(mm, vma, address, write)) {
case VM_FAULT_MINOR:
tsk->min_flt++;
break;
case VM_FAULT_MAJOR:
tsk->maj_flt++;
break;
case VM_FAULT_SIGBUS:
goto do_sigbus;
case VM_FAULT_OOM:
goto out_of_memory;
default:
BUG();
}
/*
* Did it hit the DOS screen memory VA from vm86 mode?
*/
if (regs->eflags & VM_MASK) {
unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;
if (bit < 32)
tsk->thread.screen_bitmap |= 1 << bit;
}
up_read(&mm->mmap_sem);
return;
handle_mm_fault是这个处理过程的重点,我们看一下具体的实现:
/*
参数含义:
Mm:进程描述符
Vma:发生异常所在的vma区
Address:发生异常的地址
Write_access:如果为1:表示的是一个写操作.如果是0则为读操作
*/
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
pgd_t *pgd;
pmd_t *pmd;
__set_current_state(TASK_RUNNING);
//取得进程对应的pgd
pgd = pgd_offset(mm, address);
inc_page_state(pgfault);
if (is_vm_hugetlb_page(vma))
return VM_FAULT_SIGBUS; /* mapping truncation does this. */
spin_lock(&mm->page_table_lock);
//返回或者建立一个pmd
pmd = pmd_alloc(mm, pgd, address);
if (pmd) {
//返回或者创建一个pte
pte_t * pte = pte_alloc_map(mm, pmd, address);
if (pte)
//具体异常的处理
return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
}
spin_unlock(&mm->page_table_lock);
return VM_FAULT_OOM;
}
疑问:我们在上面看到,异常处理程序会从PGD->PTE建了映射.那是不是在sys_brk在伸展空间的时候,只要使地址区间包含在进程的VMA区域.没必要为其从PGD->PTE建立映射呢? 有待验证 *^_^*
转入handle_pte_fault()
/*
参数含义:
Mm:进程的内存描述符
Vma:异常地址所在的VMA
Address:发生异常所在的地址
Write_access:1:写 0:读
Pte,pmd:地址所对应的PTE与PMD
*/
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t *pte, pmd_t *pmd)
{
pte_t entry;
//取得PTE的值
entry = *pte;
if (!pte_present(entry)) {
//pte所映射的页面不在内存
if (pte_none(entry))
//PTE没有映射.复习一下前面所讲述的sys_brk在扩展过程的地址区域的时候
//只分配了一个可以访问的线性地址,没有为其映射页面
return do_no_page(mm, vma, address, write_access, pte, pmd);
//pte_file()????
if (pte_file(entry))
return do_file_page(mm, vma, address, write_access, pte, pmd);
//运行到这里的话.说明PTE映射的页面已经被交换到磁盘上去了,把其交换回来
//具体的过程等分析交换的时候再讲述
return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
}
//PTE映射的页面在内存中
if (write_access) {
//写异常
if (!pte_write(entry))
//访问一个没有写权限的页面
return do_wp_page(mm, vma, address, pte, pmd, entry);
entry = pte_mkdirty(entry);
}
//注意:读一个已经映射好的页面,是不会产生异常的
entry = pte_mkyoung(entry);
ptep_set_access_flags(vma, address, pte, entry, write_access);
update_mmu_cache(vma, address, entry);
pte_unmap(pte);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
由于这个函数涉及到的过程较多.我们依次据情况分析
1):请求调页的情况
内核总是把用户空间的内存分配推迟到不能再延迟为止,直到要访问线性地址对应的物理地址时才会为它分配内存,这种情况对应上面代码中的do_no_page()的情况.
static int
do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
{
struct page * new_page;
struct address_space *mapping = NULL;
pte_t entry;
int sequence = 0;
int ret = VM_FAULT_MINOR;
int anon = 0;
//并不是一个磁盘高速缓存的页面
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table,
pmd, write_access, address);
//对于磁盘高速缓存这部份,等到文件系统的时候再给出分析
……
……;
}
//终于转入到正题了
static int
do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
pte_t *page_table, pmd_t *pmd, int write_access,
unsigned long addr)
{
pte_t entry;
struct page * page = ZERO_PAGE(addr);
//零页.对于一个没有为PTE分配页面的读操作,通常是将它映射到零页.这个页面是只读的
//如果其后,对这个页面进行访问的话,再为其分配一个真正的物理页面
entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));
//如果是一个写操作,则为其映射内存
if (write_access) {
//在x86平台,此函数为空
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
if (unlikely(anon_vma_prepare(vma)))
goto no_mem;
//alloc_page_vma à alloc_page().为其分配一个物理内存
page = alloc_page_vma(GFP_HIGHUSER, vma, addr);
if (!page)
goto no_mem;
clear_user_highpage(page, addr);
spin_lock(&mm->page_table_lock);
page_table = pte_offset_map(pmd, addr);
if (!pte_none(*page_table)) {
pte_unmap(page_table);
page_cache_release(page);
spin_unlock(&mm->page_table_lock);
goto out;
}
mm->rss++;
//为刚分得的page建立页表项
entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,
vma->vm_page_prot)),
vma);
lru_cache_add_active(page);
mark_page_accessed(page);
page_add_anon_rmap(page, vma, addr);
}
//如果是一个读操作,则将对应PTE映射到零页
set_pte(page_table, entry);
pte_unmap(page_table);
update_mmu_cache(vma, addr, entry);
spin_unlock(&mm->page_table_lock);
out:
return VM_FAULT_MINOR;
no_mem:
return VM_FAULT_OOM;
}
2):写时复制
从上面的过程可以看出.如果是在用户空间发生的读异常,只会指其映射到零页面.在fork()进程的时候,开始的时候子进程怀父进程是共享地址空间的.这些页面通常是只读的,如果在这些只读的页面执行写操作的时候,就会产生一个异常,内核如何处理呢?继续看代码:
上面说的这种情况对应是do_wp_page().
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, pte_t *page_table, pmd_t *pmd, pte_t pte)
{
struct page *old_page, *new_page;
//将pte的值转换成物理页面号
unsigned long pfn = pte_pfn(pte);
pte_t entry;
//物理页面号不合法.出错.退出
if (unlikely(!pfn_valid(pfn))) {
pte_unmap(page_table);
printk(KERN_ERR "do_wp_page: bogus page at address %08lx\n",
address);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_OOM;
}
//将页面序号转换成page
old_page = pfn_to_page(pfn);
//判断旧页面是否被锁定
if (!TestSetPageLocked(old_page)) {
//判断old_page是否只有一个进程在使用
int reuse = can_share_swap_page(old_page);
unlock_page(old_page);
if (reuse) {
//如果只有一个进程在使用,没必要重新分配页框,直接使用这个页框就行了
flush_cache_page(vma, address);
entry = maybe_mkwrite(pte_mkyoung(pte_mkdirty(pte)),
vma);
ptep_set_access_flags(vma, address, page_table, entry, 1);
update_mmu_cache(vma, address, entry);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
}
pte_unmap(page_table);
if (!PageReserved(old_page))
page_cache_get(old_page);
spin_unlock(&mm->page_table_lock);
if (unlikely(anon_vma_prepare(vma)))
goto no_new_page;
//分配一个新的页面
new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);
if (!new_page)
goto no_new_page;
//将异常的页面拷贝到新页面
copy_cow_page(old_page,new_page,address);
spin_lock(&mm->page_table_lock);
//取得地址对应的PTE
page_table = pte_offset_map(pmd, address);
if (likely(pte_same(*page_table, pte))) {
if (PageReserved(old_page))
++mm->rss;
else
page_remove_rmap(old_page);
//break_cow:将page_table的映射指向刚才分得的新页面
break_cow(vma, new_page, address, page_table);
lru_cache_add_active(new_page);
page_add_anon_rmap(new_page, vma, address);
/* Free the old page.. */
new_page = old_page;
}
// 释放new_page old_page
pte_unmap(page_table);
page_cache_release(new_page);
page_cache_release(old_page);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
no_new_page:
page_cache_release(old_page);
return VM_FAULT_OOM;
}
到这为止,各种异常的处理差不多了.但忽略了堆栈空间的扩展.我们接着分析.这是我们这次分析的最后一个函数了^_^
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
unsigned long grow;
if (unlikely(anon_vma_prepare(vma)))
return -ENOMEM;
anon_vma_lock(vma);
//将address按照PAGE_SIZE对齐
address &= PAGE_MASK;
//计数要增长的页面大小
grow = (vma->vm_start - address) >> PAGE_SHIFT;
//判断系统中内存是否足够
if (security_vm_enough_memory(grow)) {
anon_vma_unlock(vma);
return -ENOMEM;
}
//是否超过了限制
if (over_stack_limit(vma->vm_end - address) ||
((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) >
current->rlim[RLIMIT_AS].rlim_cur) {
anon_vma_unlock(vma);
vm_unacct_memory(grow);
return -ENOMEM;
}
//更改vma 的映射范围
vma->vm_start = address;
vma->vm_pgoff -= grow;
vma->vm_mm->total_vm += grow;
if (vma->vm_flags & VM_LOCKED)
vma->vm_mm->locked_vm += grow;
__vm_stat_account(vma->vm_mm, vma->vm_flags, vma->vm_file, grow);
anon_vma_unlock(vma);
return 0;
}
总结:
由于do_page_fault代码中采用了大量的goto处理,使整个代码的可读性不太好,不过,先把标号的含义处理清楚,代码的逻辑流程是十分清晰的.以前经常在开发板上看到“SIGSEGV””do_page_fault”等错误只知道是内存方面的错误,现在终于知其然亦知其所以然了 ^_^