do_page_fault函数处理流程
本帖最后由 cluter 于 2011-05-28 01:01 编辑
只解释下关键代码的流程,附件里是关于缺页处理思路更清晰的一个整理,无代码的。
顺便提出几个问题:
在"内核态"访问非连续内存时,为何要拷贝“内核页表项”到用“户页表项”,为何不直接使用“内核页表”?
对于一个4G的机器,用户进程的页框大部分分布在物理内存的哪个范围?
为什么当物理内存大于1G的时候,就开始划分出”高端内存“?
64bit的机器上不需要”高端内存“,这是为什么?
只解释下关键代码的流程,附件里是关于缺页处理思路更清晰的一个整理,无代码的。
- //*********************************缺页异常处理函数*******************************************
- do_page_fault(struct pt_regs *regs, unsigned long error_code)
- {
-
- //获取当前cpu正在运行的进程的进程描述符
- //然后获取该进程的内存描述符
- tsk = current;
- mm = tsk->mm;
- /* Get the faulting address: */
- //获取出错的地址
- address = read_cr2();
- /*
- * We fault-in kernel-space virtual memory on-demand. The
- * 'reference' page table is init_mm.pgd.
- *
- * NOTE! We MUST NOT take any locks for this case. We may
- * be in an interrupt or a critical region, and should
- * only copy the information from the master page table,
- * nothing more.
- *
- * This verifies that the fault happens in kernel space
- * (error_code & 4) == 0, and that the fault was not a
- * protection error (error_code & 9) == 0.
- */
- //页访问出错地址address在内核空间
- if (unlikely(fault_in_kernel_space(address))) {
- //检查标志位确定访问发生在"内核态"
- if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
- //如果是内核空间"非连续内存"的访问,
- //则直接拷贝"内核页表项"到"用户页表项"
- //如果"内核页表项"为null,说明内核有BUG,返回-1
- if (vmalloc_fault(address) >= 0)
- return;
- }
- //如果在"用户态"则直接进入"非法访问"处理函数
- //如果vmalloc_fault返回-1,则表示内核BUG
- bad_area_nosemaphore(regs, error_code, address);
- //错误处理函数
- // 1 "用户态"错误-->直接终止进程
- // 2 "内核态"错误
- // 系统调用参数错误 ---->终止进程/返回系统调用错误码
- // 内核BUG ---->内核panic
- return;
- }
- /*
- * If we're in an interrupt, have no user context or are running
- * in an atomic region then we must not take the fault:
- */
- // 1 在中断中,此时没有进程上下文
- // 2 在原子操作流程中
- // 都不允许处理缺页异常
- if (unlikely(in_atomic() || !mm)) {
- bad_area_nosemaphore(regs, error_code, address);
- return;
- }
- /*
- * When running in the kernel we expect faults to occur only to
- * addresses in user space. All other faults represent errors in
- * the kernel and should generate an OOPS. Unfortunately, in the
- * case of an erroneous fault occurring in a code path which already
- * holds mmap_sem we will deadlock attempting to validate the fault
- * against the address space. Luckily the kernel only validly
- * references user space from well defined areas of code, which are
- * listed in the exceptions table.
- *
- * As the vast majority of faults will be valid we will only perform
- * the source reference check when there is a possibility of a
- * deadlock. Attempt to lock the address space, if we cannot we then
- * validate the source. If this is invalid we can skip the address
- * space check, thus avoiding the deadlock:
- */
- //此时可以确定缺页地址address在"用户空间"了
- if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
- //错误发生在"内核态",查看异常表
- //如果在内核态引起缺页,则引起缺页的"指令地址"一定在"异常表"中
- //如果"异常表"中返回指令地址,则说明可能是"请求调页",也可能是"非法访问"
- //如果"异常表"中无地址,则肯定是内核错误
- if ((error_code & PF_USER) == 0 &&
- !search_exception_tables(regs->ip)) {
- //内核panic
- bad_area_nosemaphore(regs, error_code, address);
- return;
- }
- 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();
- }
- //寻找address所在的vma
- vma = find_vma(mm, address);
- //如果address之后无vma,则肯定是非法访问
- if (unlikely(!vma)) {
- bad_area(regs, error_code, address);
- return;
- }
- // 1 如果vma->start_address<=address,则直接跳到 "合法访问"阶段
- // 2 如果vma->start_address>address,则也有可能是用户的"入栈行为"导致缺页
- if (likely(vma->vm_start <= address))
- goto good_area;
- // "入栈"操作,则该vma的标志为 "向下增长"
- if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
- bad_area(regs, error_code, address);
- return;
- }
- // 确定缺页发生在"用户态"
- if (error_code & PF_USER) {
- /*
- * Accessing the stack below %sp is always a bug.
- * The large cushion allows instructions like enter
- * and pusha to work. ("enter $65535, $31" pushes
- * 32 pointers and then decrements %sp by 65535.)
- */
- //验证缺页address和栈顶sp的关系
- if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
- bad_area(regs, error_code, address);
- return;
- }
- }
- //扩展栈
- if (unlikely(expand_stack(vma, address))) {
- bad_area(regs, error_code, address);
- return;
- }
- /*
- * Ok, we have a good vm_area for this memory access, so
- * we can handle it..
- */
- good_area:
- write = error_code & PF_WRITE;
- // 再次验证"权限"
- if (unlikely(access_error(error_code, write, vma))) {
- bad_area_access_error(regs, error_code, address);
- return;
- }
- /*
- * If for any reason at all we couldn't handle the fault,
- * make sure we exit gracefully rather than endlessly redo
- * the fault:
- */
- //分配新"页框"
- fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
- up_read(&mm->mmap_sem);
- }
- //*******************************访问权限验证函数********************************************
- access_error(unsigned long error_code, int write, struct vm_area_struct *vma)
- {
- //如果是"写操作"引起的缺页,则该vma必须可写
- if (write) {
- /* write, present and write, not present: */
- if (unlikely(!(vma->vm_flags & VM_WRITE)))
- return 1;
- return 0;
- }
- /* read, present: */
- //检查该页是否已经在RAM中,如果"特权位"置位表示页框在RAM中
- //表示进程访问"有特权" 页框
- if (unlikely(error_code & PF_PROT))
- return 1;
- /* read, not present: */
- //如果该页不在内存中,该线性区必须可"读"
- if (unlikely(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE))))
- return 1;
- return 0;
- }
do_page_fault处理流程.JPG (4.15 MB, 下载次数: 0)
do_page_fault处理思路
顺便提出几个问题:
在"内核态"访问非连续内存时,为何要拷贝“内核页表项”到用“户页表项”,为何不直接使用“内核页表”?
对于一个4G的机器,用户进程的页框大部分分布在物理内存的哪个范围?
为什么当物理内存大于1G的时候,就开始划分出”高端内存“?
64bit的机器上不需要”高端内存“,这是为什么?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
高端内存的作用是访问内核空间1g不能直接寻址之外的内存
折衷的办法就是
把1G划为直接映射的一部分和高端内存
64位机的内核空间能够足够大到寻址所有内存(虚拟地址空间不止4G)、
回复 2# amarant
LS起这么早。。。关于为何使用高端内存,阁下理解的很透彻。。,第一个问题LS怎么看?
回复 3# cluter
这个问题我说不准,就不敢胡说了。。
第一个问题我还真没看懂是什么意思
第二个问题,我没有仔细研究过代码,这应该取决于内核向Buddy System索要内存时传入的flag参数。新版的代码已经改的乱七八糟了,我在ULK上看过的代码片断里参数好像包括了GFP_HIGHUSER,也就是说,优先在大于896M的物理内存里拿。
第三第四个问题,内核使用高端内存是因为内核拥有的虚拟地址大于物理内存。访问物理内存必需先将其映射于某一块虚拟地址段之上,而大多体系结构中,内核是与进程共用地址空间的。Linux下内核一般只占顶1G,资源有些紧张,能映射到的物理也就紧张。因此在其1G的虚拟地址空间之中,开辟一个小窗口动态映射“够不着”的那些物理内存。所以说,高端内存的出现是因为“内核虚拟地址空间 < 物理内存大小”。
如果“内核虚拟地址空间 > 物理内存大小”,就不会出现高端内存了。这时还可以分为两种情况,一为“内核虚拟地址空间太大”,比如64位操作系统;一为“物理内存太小”,比如32位操作系统时物理内存小于896M。
回复 5# tempname2
呵呵,ls 2 3 问题都理解的很深刻。给进程分配的页框和进程的页表都优先在高端内存索取!!
第一个问题其实就是:当进程系统调用进入内核态,为何不使用内核页表,而是把内核页表项拷贝到进程页表,然后继续访问。
回复 4# amarant
其实,只需要逆向思考下就可以了,如果切换页表,系统必须做其他什么事情?
对第一个问题我的理解是:
每个进程都有一个页表,页表既有内核地址空间的,也有用户地址空间的。
但是,有些内核空间的只在 init_mm的页表中有,而当前进程没有,当访问这些地址的时候,就出现页中断,
这里把 init_mm的页表中的相关项拷贝到当前进程的页表中。这样再访问的时候,就不会页中断了。
我不了解X86页表在内存中的储存结构,如果X86中的页全局表项必须是以数组的方式顺序存储。那么就很好理解为什么要多此一举复制内核页表过来了。
看代码是从init_mm.pgd上拷贝过去的,如果不拷贝而直接切换到init_mm_pgd上,那至少存在这个问题,内核中使用copy_from_user等访问进程用户地址空间的时候必然出错。另外执行signal处理函数时要利用程序在用户空间的栈来保存被冲掉的内核栈保存的内容,也必然出错。其原因在于init_mm.gpd上没有当前进程在用户地址空间的映射表。呵呵,不知俺说的对不对。