现代处理器大部分都有mmu,除了一些小型嵌入式设备。MMU可以做虚拟地址到物理地址的转换,使用MMU我们就可以使用更多的内存空间,因为程序具有局部性原理,我们可以将暂时用不到的数据存放到磁盘,当访问到时会发生缺页中断,从磁盘中将所需要的数据加载到内存。所以我们可以通过mmu运行程序大小大于内存的程序和打开大于内存的文件。现代处理器通过分段分页机制实现虚拟地址到物理地址转换一般支持二级页表或四级页表。

嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。

linux提示内核死机不同步(一文搞懂Linux内核缺页中断处理)(1)

需要的朋友私信【内核】即可领取。

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

缺页中断处理一般流程:

1.硬件陷入内核,在堆栈中保存程序计数器,大多数当前指令的各种状态信息保存在特殊的cpu寄存器中。

2.启动一个汇编例程保存通用寄存器和其他易丢失信息,以免被操作系统破坏。

3.当操作系统发现缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这些信息,如果没有的话操作系统必须检索程序计数器,取出当前指令,分析当前指令正在做什么。

4.一旦知道了发生缺页中断的虚拟地址,操作系统会检查地址是否有效,并检查读写是否与保护权限一致,不过不一致,则向进程发一个信号或者杀死该进程。如果是有效地址并且没有保护错误发生则系统检查是否有空闲页框。如果没有,则执行页面置换算法淘汰页面。

5.如果选择的页框脏了,则将该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程让其他进程运行直到写入磁盘结束。且回写的页框必须标记为忙,以免其他原因被其他进程占用。

6.一旦页框干净后,操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入,当页面被装入后,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一用户进程运行。

7.当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映他的位置,页框也标记位正常状态。

8.恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。

9.调度引发缺页中断的进程,操作系统返回调用他的汇编例程

10.该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过。

linux内核对缺页异常的处理流程很复杂,但是基本思想和上述流程差不多。

首先看一个重要的结构体struct vm_area_struct,虚拟内存区域,比如.text段,数据段,都有自己对应的vma,在加载二进制文件的时候会创建这些vma。

struct vm_area_struct { struct mm_struct * vm_mm; /* 此vma所属的mm,指向所属的内存描述符*/ unsigned long vm_start; /* 内存区域起始地址 */ unsigned long vm_end; /* 内存区域结束地址 */ /* 指向下一个vma */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* vma访问权限 */ unsigned long vm_flags; /* vma属性比如:只读,读写,可执行 */ struct rb_node vm_rb; /*vma结构组成的红黑树*/ /* * 对于有地址空间和后备存储器的映射区域,链接到address_space */ union { struct { struct list_head list; void *parent; /* aligns with prio_tree_node parent */ struct vm_area_struct *head; } vm_set; struct raw_prio_tree_node prio_tree_node; } shared; /* *anon_vma_node和anon_vma用于管理源自匿名映射( anonymous mapping)的共享页。指向相 *同页的映射都保存在一个双链表上, anon_vma_node充当链表元素。 *有若干此类链表,具体的数目取决于共享物理内存页的映射集合的数目。 anon_vma成员是一 *个指向与各链表关联的管理结构的指针,该管理结构由一个表头和相关的锁组成。 */ struct list_head anon_vma_node; /* 通过vma->lock串行访问,连接所有匿名映射区域 */ struct anon_vma *anon_vma; /* 通过page_table_lock串行访问*/ /* vma操作集合 */ struct vm_operations_struct * vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* 当前区域在映射文件中的偏移量,只用于映射部分文件,如果整个文件则为0*/ struct file * vm_file; /* 映射的文件指针*/ void * vm_private_data; /* 共享内存*/ };

页面分类:

匿名页:数据段,堆,栈,mmap匿名映射的页。

文件页:可执行文件代码段映射的页,普通文件映射的页。

缺页异常分类:1.内核态缺页异常,2.用户态缺页异常。其中内核态异常分为1.vmalloc区异常,因为非vmalloc的内核区是直接对等映射的,只有vmalloc区是动态映射的。而vmalloc出现异常比较好处理,只需要页表同步就可以了(因为伴随着进程的切换可能用户进程的页表不是最新的,需要将内核的页表更新到用户进程的页表),2.内核引用用户空间地址发生的异常,比如用户态的地址非法,或者页面已经被交换到了磁盘。3.内核bug。内核态缺页异常频率很低,因为内核态的数据不会换出到磁盘的。所以用户态才会经常出现缺页异常,因为用户态的数据经常写到交换区和文件。并且在进程刚创建运行时也会伴随着大量的缺页异常。

下面看linux的基本处理流程:

linux提示内核死机不同步(一文搞懂Linux内核缺页中断处理)(2)

内核处理缺页异常的主函数就是do_page_fault:

/* * 缺页异常处理函数 * pt_regs 各个寄存器的值 * error_code,由硬件产生: * bit 0 == 0 表示页面不存在引发的异常, 1 页面访问权限不正确引发的异常 * bit 1 == 0 表示读页面异常, 1 表示写页面引起的异常 * bit 2 == 0 表示内核态引起的异常, 1 表示用户态引起的异常 * bit 3 == 1 没发生地址转换出现的异常 * bit 4 == 1 表示取指异常 */ fastcall void __kprobes 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, si_code; /* 获取缺页异常的地址 */ address = read_cr2(); tsk = current; //缺页进程 si_code = SEGV_MAPERR; /*如果缺页地址发生在内核空间*/ if (unlikely(address >= TASK_SIZE)) { /*判断错误码是否发生在内核态,并且必须是地址转换和页面不存在引发的异常, * 所以error_code bit0 2 3必须是0.如果符合则调用vmalloc_fault修复内核 * 缺页异常 */ if (!(error_code & 0x0000000d) && vmalloc_fault(address) >= 0) return; if (notify_page_fault(DIE_PAGE_FAULT, "page fault", regs, error_code, 14, SIGSEGV) == NOTIFY_STOP) return; /* * Don't take the mm semaphore here. If we fixup a prefetch * fault we could otherwise deadlock. */ goto bad_area_nosemaphore; } if (notify_page_fault(DIE_PAGE_FAULT, "page fault", regs, error_code, 14, SIGSEGV) == NOTIFY_STOP) return; /* 如果中断关闭则开中断 */ if (regs->eflags & (X86_EFLAGS_IF|VM_MASK)) local_irq_enable(); mm = tsk->mm; //获取当前进程的内存描述符 /* *如果我们是在中断期间,也没有用户上下文,或者代码处于原子操作范围内,则不能处理该异 *异常 */ if (in_atomic() || !mm) goto bad_area_nosemaphore; vma = find_vma(mm, address);//查找到缺页地址对应的vma if (!vma) goto bad_area; //如果没有对应的vma,则会跳到bad_area处理并且杀死进程 if (vma->vm_start <= address) //判断address是否属于vma goto good_area; //如果找到address属于的vma则跳到good_area /*走到这说明缺页异常的地址只可能位于栈空间并且栈向下增长 *因为栈所属的vma大小初始是参数大小 EXTRA_STACK_VM_PAGES * PAGE_SIZE */ if (!(vma->vm_flags & VM_GROWSDOWN)) // goto bad_area; //如果不是则杀死进程 if (error_code & 4) {//如果是用户态发生的栈操作异常 /* * 扩展栈的空间每次必须小于65536 32 * sizeof(unsigned long),否则段异常 */ if (address 65536 32 * sizeof(unsigned long) < regs->esp) goto bad_area; } //扩展栈空间的vma if (expand_stack(vma, address)) goto bad_area; good_area: si_code = SEGV_ACCERR; write = 0; switch (error_code & 3) { //判断错误码的情况 default: /* 3: write, present写发生异常,但是页面存在,会进入到 * case2判断vma是否可写,如果可以,则说明是写时复制发生的异常 */ /* fall through */ case 2: /* 如果是写入操作*/ if (!(vma->vm_flags & VM_WRITE)) //判断vma是否有可写属性,如果没有则段错误 goto bad_area; write ; //如果有则写入标志 1 break; case 1: /* 如果是读操作,则表示没有读权限,杀死进程 */ goto bad_area; case 0: /* 如果是读或执行操作引起的异常并且页面不存在 */ // 如果所属vma没有读或者执行属性则杀死进程 if (!(vma->vm_flags & (VM_READ | VM_EXEC))) goto bad_area; } survive: //执行到这说明是正常的缺页异常,则调用handle_mm_fault处理 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(); } up_read(&mm->mmap_sem); return; bad_area: up_read(&mm->mmap_sem); bad_area_nosemaphore: /*如果是用户态发生的异常说明是用户态访问了内核地址,则直接杀死该进程*/ if (error_code & 4) { tsk->thread.cr2 = address; /* Kernel addresses are always protection faults */ tsk->thread.error_code = error_code | (address >= TASK_SIZE); tsk->thread.trap_no = 14; force_sig_info_fault(SIGSEGV, si_code, address, tsk); return; } /*在内核态发生的异常,如果上面没处理好,则进入这个标记*/ no_context: /* *发生在内核空间的异常,在引用用户空间地址时发生的,此时修正异常,直接返回用户空间 *一般是在调用get_user或者copy_from_user时出现的异常。 */ if (fixup_exception(regs)) return; /* * Oops. The Kernel tried to access some bad page. We'll have to * terminate things with extreme prejudice. */ /* * 进入oops,内核在使用一些坏页面,需要杀死进程 */ bust_spinlocks(1); if (oops_may_print()) { #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 "BUG: unable to handle kernel NULL " "pointer dereference"); else printk(KERN_ALERT "BUG: unable to handle kernel paging" " request"); printk(" at virtual address lx\n",address); printk(KERN_ALERT " printing eip:\n"); printk("lx\n", regs->eip); } page = read_cr3(); page = ((unsigned long *) __va(page))[address >> 22]; if (oops_may_print()) printk(KERN_ALERT "*pde = lx\n", page); 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); /* * 内存不够,需要杀死进程 */ out_of_memory: up_read(&mm->mmap_sem); if (tsk->pid == 1) { //如果是init进程,则循环进入survive,直到有空闲页框 yield(); down_read(&mm->mmap_sem); goto survive; } printk("VM: killing process %s\n", tsk->comm); if (error_code & 4) do_exit(SIGKILL); goto no_context; do_sigbus: up_read(&mm->mmap_sem); /* Kernel mode? Handle exceptions or die */ if (!(error_code & 4)) goto no_context; tsk->thread.cr2 = address; tsk->thread.error_code = error_code; tsk->thread.trap_no = 14; force_sig_info_fault(SIGBUS, BUS_ADRERR, address, tsk); }

do_page_fault大致流程图如下:

linux提示内核死机不同步(一文搞懂Linux内核缺页中断处理)(3)

下面看do_page_fault内调用的主要函数。首先是find_vma,查找缺页地址所处的vma,vma在用户进程创建的时候分配好的。内核用了链表和红黑树对vma进行了排序。

/* 查找addr所属的vma,这查找并不是必须找vma->start_addr <= addr < vma->end_addr * 而是只要addr < vma->end_addr,并且是和end_addr离的最近就可以,栈所属的vma就是 *这种情况,由于栈的初始vma 空间并不大,并且地址向下增长,end_addr固定,但是start_addr不 * 固定,所以可能出现addr < vma->end_addr && addr > start_addr */ struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr) { struct vm_area_struct *vma = NULL; if (mm) { /* vma cache,表示上次查找到的vma,这次很可能还是,程序局部性原理, * 可以加快35%的速度*/ vma = mm->mmap_cache; /*如果不是上次的vma,则通过vma组织的红黑树查找*/ if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { struct rb_node * rb_node; rb_node = mm->mm_rb.rb_node; vma = NULL; //遍历红黑树 while (rb_node) { //vma临时变量 struct vm_area_struct * vma_tmp; vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); //如果临时变量大于addr,则将临时vma赋值给返回的vma if (vma_tmp->vm_end > addr) { vma = vma_tmp; //如果start <= addr说明属于此vma则直接跳出遍历 if (vma_tmp->vm_start <= addr) break; //否则遍历左子树 rb_node = rb_node->rb_left; } else //遍历右子树 rb_node = rb_node->rb_right; } if (vma) //如果vma不为null,则将vma赋值给vma缓存 mm->mmap_cache = vma; } } return vma; }

在栈的生长方向往低地址方向生长时,可能会出现缺页异常的地址不在任何vma闭区间内,而是 vma->vm_start > address,此时需要向下扩展栈所属的vma的起始地址,expand_stack函数如下:

/* * vma address < vma->vm_start. 必须扩展vma. */ int expand_stack(struct vm_area_struct *vma, unsigned long address) { int error; /* * 由于栈的vma属于匿名映射,所以要先判断是否有anon_vma,如果没有则分配 */ if (unlikely(anon_vma_prepare(vma))) return -ENOMEM; anon_vma_lock(vma); address &= PAGE_MASK; //address 4KB对齐 error = 0; /* 扩展 */ if (address < vma->vm_start) { unsigned long size, grow; size = vma->vm_end - address; //vma大小 grow = (vma->vm_start - address) >> PAGE_SHIFT; //vma要增加的页面个数 error = acct_stack_growth(vma, size, grow);//测试是否可以增长 if (!error) { //可以增长,重置vma的起始地址 vma->vm_start = address; vma->vm_pgoff -= grow; //重置vma的偏移 } } anon_vma_unlock(vma); return error; }

如果是用户空间发生的正常缺页异常时,就需要调用handle_mm_fault处理页表,在获取到缺页地址所对应的页表项指针后,会进入到handle_pte_fault,会根据条件分别调用不同的页框交换函数,具体看代码:

int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, int write_access) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; //将当前进程设置为正在执行 __set_current_state(TASK_RUNNING); count_vm_event(PGFAULT); //如果vma有巨页属性则调用hugetlb_fault if (unlikely(is_vm_hugetlb_page(vma))) return hugetlb_fault(mm, vma, address, write_access); //获取页目录 pgd = pgd_offset(mm, address); //获取pud pud = pud_alloc(mm, pgd, address); if (!pud) return VM_FAULT_OOM; //获取pmd pmd = pmd_alloc(mm, pud, address); if (!pmd) return VM_FAULT_OOM; //获取页表项地址 pte = pte_alloc_map(mm, pmd, address); if (!pte) return VM_FAULT_OOM; return handle_pte_fault(mm, vma, address, pte, pmd, write_access); } /* * 如果页表项页框存在标志为0,说明页框不存在,此时分为两种情况,第一种情况是页表项为 * 空,说明此页表项是第一次进行映射,并且还会分为是匿名映射还是文件映射。第二种情况是 * 页表项不为null,说明此页表项映射过页框,只不过被换到了磁盘,所以也分为匿名映射和文件 * 映射两种情况,其中匿名映射从swap区加载数据,文件映射从对应的文件加载数据 * */ static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, int write_access) { pte_t entry; pte_t old_entry; spinlock_t *ptl; old_entry = entry = *pte; //如果页表项页框存在标志不存在, if (!pte_present(entry)) { //如果页表项不存在并且页表项为null,说明是第一次进行映射 if (pte_none(entry)) { //如果vma没有操作集合这说明缺页是匿名页,调用do_anonymous_page分配页面 if (!vma->vm_ops || !vma->vm_ops->nopage) return do_anonymous_page(mm, vma, address, pte, pmd, write_access); //第一次对文件映射页处理,会读取vma对应的文件数据,并且为了减少磁盘 //IO会进行预读 return do_no_page(mm, vma, address, pte, pmd, write_access); } //如果页表项有文件映射页的标志,说明是以前存在映射并且页面被交换到了 //所对应的磁盘文件,也会进行预读 if (pte_file(entry)) return do_file_page(mm, vma, address, pte, pmd, write_access, entry); //最后一种情况就是页表项存在但是是匿名页,说明页面被交换到了swap区,需要从 //swap区加载 return do_swap_page(mm, vma, address, pte, pmd, write_access, entry); } /*以下是页表项页框标志存在的情况,说明不是因为缺页引起的异常,而是由于其他原因,, * 比如fork进程,子进程写时复制时会发生 */ ptl = pte_lockptr(mm, pmd); spin_lock(ptl); //比较pte是否发生改变 if (unlikely(!pte_same(*pte, entry))) goto unlock; //如果vma有写权限,这种情况就是在COW时会发生,比如子进程copy了父进程的页表 //但是把页表中可写的页表项设置为只读,然后子进程在真正写的时候会发生保护异常 //然后进行页框复制,重建页表项 if (write_access) { //如果页表项没有写权限 if (!pte_write(entry)) //调用此函数处理COW的情况 return do_wp_page(mm, vma, address, pte, pmd, ptl, entry); //对页表项标记脏标志,也就是表示写过该页面 entry = pte_mkdirty(entry); } //对页面标记使用标志 entry = pte_mkyoung(entry); //如果页表项发生变化,导致不相等 if (!pte_same(old_entry, entry)) { //如果有写标志则将entry复制给页表项,并且刷新tlb ptep_set_access_flags(vma, address, pte, entry, write_access); //体系结构相关函数,i386下为空 update_mmu_cache(vma, address, entry); //体系结构相关函数,i386下为空 lazy_mmu_prot_update(entry); } else { /* * This is needed only for protection faults but the arch code * is not yet telling us if this is a protection fault or not. * This still avoids useless tlb flushes for .text page faults * with threads. */ if (write_access) flush_tlb_page(vma, address); } unlock: pte_unmap_unlock(pte, ptl); return VM_FAULT_MINOR; }

经过以上处理用户态缺的页框,就会从磁盘加载到内存然后重新建立映射。

下面看内核态缺页异常具体的处理函数,内核态分为当前进程的页表内核映射部分没更新到最新的,此时需要进行页表同步,调用vmalloc_fault

/* * 处理vmalloc异常或者模块区域映射异常 */ static inline int vmalloc_fault(unsigned long address) { unsigned long pgd_paddr; //页目录地址 pmd_t *pmd_k; //init进程中间页目录指针 pte_t *pte_k; //init进程页表项指针 /* * Synchronize this task's top level page-table * with the 'reference' page table. * *这里不适用current因为有可能处于任务切换的中断内 */ pgd_paddr = read_cr3(); //从cr3获取页目录地址 //获取中间目录的地址,在i386下就是pte的地址,并且已经做了页表项复制 pmd_k = vmalloc_sync_one(__va(pgd_paddr), address); if (!pmd_k) return -1; //获取pte指针 pte_k = pte_offset_kernel(pmd_k, address); //如果页框不存在则会出错 if (!pte_present(*pte_k)) return -1; return 0; } static inline pmd_t *vmalloc_sync_one(pgd_t *pgd, unsigned long address) { unsigned index = pgd_index(address); //获取页目录偏移量 pgd_t *pgd_k; //页目录 pud_t *pud, *pud_k; //缺页进程的pud和init进程的pud pmd_t *pmd, *pmd_k; //缺页进程的pmd和init进程的pmd pgd = index; //init进程的页表内核部分肯定是最新的,所以使用init进程处理,加上偏移量 pgd_k = init_mm.pgd index; //如果页目录项不存在返回null if (!pgd_present(*pgd_k)) return NULL; //获取缺页进程的pud和init进程的pud,i386下就是所需pte的指针 pud = pud_offset(pgd, address); pud_k = pud_offset(pgd_k, address); //如果init进程的pud为空则返回null if (!pud_present(*pud_k)) return NULL; //获取缺页进程的pmd和init进程的pmd pmd = pmd_offset(pud, address); pmd_k = pmd_offset(pud_k, address); //如果init进程的pmd为空则返回null if (!pmd_present(*pmd_k)) return NULL; //如果缺页进程的pmd项为空,则将init进程的pmd项赋值给缺页进程的pmd项,i386下就是pte //项的复制 if (!pmd_present(*pmd)) set_pmd(pmd, *pmd_k); //如果缺页进程的pmd项不为null,则init进程和缺页进程的这一项必须不能相等 else BUG_ON(pmd_page(*pmd) != pmd_page(*pmd_k)); return pmd_k;//返回init进程pmd项指针 }

内核态缺页还有一种情况就是内核需要访问用户空间的地址,这时用户空间页表所对应的页框可能已经被换出内存,或者本身就是一个错误的地址,这时候需要使用fixup_exception处理,内核在编译的时候会留下一段空间做为异常表。i386下的链接脚本内存布局中有以下一段:

. = ALIGN(16); /* Exception table */ __start___ex_table = .; __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { *(__ex_table) } __stop___ex_table = .;

__start__ex_table 和 __stop__ex_table指定了__ex_table段的起始和结束地址*(__ex_table)表示将所有输入目标文件的__ex_table段组合成一个__ex_table。下面看在内核调用copy_from_user时发生缺页异常的处理过程:

/* 异常表项结构体 */ struct exception_table_entry { unsigned long insn; unsigned long fixup; }; int fixup_exception(struct pt_regs *regs) { const struct exception_table_entry *fixup; fixup = search_exception_tables(regs->eip); //如果查找到对应的exception_table_entry,将fixup赋值给regs->eip,在退出缺页异常时会跳转到 //fixup处处理,这样不会导致死循环中断,内核设计很安全。 if (fixup) { regs->eip = fixup->fixup; return 1; } return 0; } /* Given an address, look for it in the exception tables. */ const struct exception_table_entry *search_exception_tables(unsigned long addr) { const struct exception_table_entry *e; //使用二分查找算法,查找exception_table_entry e = search_extable(__start___ex_table, __stop___ex_table - __start___ex_table, addr); if (!e) e = search_module_extables(addr); return e; } static __always_inline unsigned long __copy_from_user(void *to, const void __user *from, unsigned long n) { might_sleep(); if (__builtin_constant_p(n)) { unsigned long ret; switch (n) { //要复制字节个数 case 1: __get_user_size(*(u8 *)to, from, 1, ret, 1); //复制单字节 return ret; case 2: __get_user_size(*(u16 *)to, from, 2, ret, 2);//复制双字节 return ret; case 4: __get_user_size(*(u32 *)to, from, 4, ret, 4);//复制四字节 return ret; } } return __copy_from_user_ll(to, from, n); } #define __get_user_size(x,ptr,size,retval,errret) \ do { \ retval = 0; \ __chk_user_ptr(ptr); \ switch (size) { \ case 1: __get_user_asm(x,ptr,retval,"b","b","=q",errret);break; \ case 2: __get_user_asm(x,ptr,retval,"w","w","=r",errret);break; \ case 4: __get_user_asm(x,ptr,retval,"l","","=r",errret);break; \ //以复制4字节为例 default: (x) = __get_user_bad(); \ } \ } while (0) //以复制4字节为例 #define __get_user_asm(x, addr, err, itype, rtype, ltype, errret) \ __asm__ __volatile__( \ //复制时可能会发生缺页异常 "1: mov"itype" %2,%"rtype"1\n" \ //itype, rtype会被替换变成movl %2,%1 "2:\n" \ ".section .fixup,\"ax\"\n" \ //.fixup包含在代码段内 //如果在内核态,内核调用copy_from_user时发生缺页异常,内核会将regs->ip也就是中断返回地址设置 //为这个地址,退出中断后不会在执行1: mov"itype" %2,%"rtype"1\n", "3: movl %3,%0\n" \ //将4复制给err,如果发生了缺页异常会返回4,否则返回0 " xor"itype" %"rtype"1,%"rtype"1\n" \ //xorl %1,%1,将x清零 " jmp 2b\n" \ //跳转到标号2 ".previous\n" \ ".section __ex_table,\"a\"\n" \ " .align 4\n" \ //此处对应一个struct exception_table_entry变量,1b表示insn,3b表示fixup,当内核态发生异常时 //会搜索异常表找到insn,然后找到insn对应的fixup,退出异常时跳转到fixup地址 " .long 1b,3b\n" \ ".previous" \ : "=r"(err), ltype (x) \ //"=r"(err), "=r" (x) : "m"(__m(addr)), "i"(errret), "0"(err)) //"m"(__m(addr)), "i"(4), "0"(4))

经过以上分析,对linux内核的缺页异常处理有了一个比较深入的理解,当然还有很多细枝末节没分析到,但是整个处理逻辑已经分析完了。

原文地址:https://cloud.tencent.com/developer/article/2028576(版权归原作者所有,侵删)

,