深入理解Linux虚拟内存管理(七)(中):https://developer.aliyun.com/article/1597882
5、缺页中断
这一部分关于缺页中断处理程序。首先是 x86 体系结构相关的函数,然后转移到体系结构无关层。特定体系结构的函数都具有相同的任务。
(1)x86 缺页中断处理程序
(a)do_page_fault
该函数的调用图如图 4.11 所示。它是处理缺页中断异常的 x86 体系结构相关函数。每种体系结构都只注册它自己,但它们的任务却都相似。
// arch/i386/mm/fault.c /* * This routine handles page faults. It determines the address, * and the problem, and then passes it off to one of the appropriate * routines. * * error_code: * bit 0 == 0 means no page found, 1 means protection fault * bit 1 == 0 means read, 1 means write * bit 2 == 0 means kernel, 1 means user-mode */ // 这个是函数的开始部分,它获取异常地址并开中断。 // 参数如下: // regs 是在异常时包含所有寄存器的结构。 // error_code 表明发生异常的类型。 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; unsigned long fixup; int write; siginfo_t info; /* get the address */ // 如注释所示,cr2寄存器保留有异常地址。 __asm__("movl %%cr2,%0":"=r" (address)); /* It's safe to allow irq's after cr2 has been saved */ // 如果异常是从中断中来,这里开中断。 if (regs->eflags & X86_EFLAGS_IF) local_irq_enable(); // 设置当前的进程。 tsk = current; /* * 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 & 1) == 0. */ // 这一块检查例外异常、核心异常、陷入中断和无内存上下文异常。 // 如果异常地址超过TASK_SIZE ,则它在内核地址空间。如果错误码是5,则意味着 // 错误发生在核态且不是一个保护性错误,所以这里处理vmalloc异常。 if (address >= TASK_SIZE && !(error_code & 5)) goto vmalloc_fault; // 记录一个工作中的mm。 mm = tsk->mm; info.si_code = SEGV_MAPERR; /* * If we're in an interrupt or have no user * context, we must not take the fault.. */ // 如果这是一个中断且没有内存上下文(如在一个内核线程中),则没有方法来安全地 // 处理这个异常,所以转到no_context。 if (in_interrupt() || !mm) goto no_context; // 如果异常发生在用户空间,这一块找到异常地址的VMA,以确定它是一块好区域,还是 // 一块坏区域,或者异常是否发生在一块可扩展区域附近,如栈。 // 获取长生命期的mm信号量。 down_read(&mm->mmap_sem); // 找到在异常地址的或与异常地址最近的VMA。 vma = find_vma(mm, address); // 如果根本就不存在VMA,则转到bad_area。 if (!vma) goto bad_area; // 如果区域的始端在该地址前面,则意味着这个VMA就是发生异常的VMA,所 // 以转到good_area,在那里检查权限。 if (vma->vm_start <= address) goto good_area; // 对最邻近的区域,这里检查它是否可以向下延伸(VM_GROWSDOWN)。如果 // 可以,则意味着栈可以扩展。如果不可以,则转到bad_area。 if (!(vma->vm_flags & VM_GROWSDOWN)) goto bad_area; if (error_code & 4) { /* * accessing the stack below %esp is always a bug. * The "+ 32" is there due to some instructions (like * pusha) doing post-decrement on the stack and that * doesn't show up until later.. */ if (address + 32 < regs->esp) goto bad_area; } // 栈是惟一设置了 VM_GROWSDOWN 的区域。所以,如果我们到达这里,将调 // 用expand_stack()来扩展栈(见D. 5. 2. 1)。如果失败,则转到bad_area。 if (expand_stack(vma, address)) goto bad_area; /* * Ok, we have a good vm_area for this memory access, so * we can handle it.. */ // 这一块是在好的区域中处理异常的部分。这里需要检查权限以防止这是一个保护性异常。 good_area: // 缺省情况下,这里返回一个错误。 info.si_code = SEGV_ACCERR; write = 0; // 检查错误码的0位和1位。0位为0表示页面不存在,为1表示这是一个如向只读区 // 域写一样的保护性异常。1位为0表示这是一个读异常,为1表示这是一个写异常。 switch (error_code & 3) { // 如果错误码为3,而0位和1位都为1,则为一个写保护异常。 default: /* 3: write, present */ #ifdef TEST_VERIFY_AREA if (regs->cs == KERNEL_CS) printk("WP fault at %08lx\n", regs->eip); #endif /* fall through */ // 位1为1,则为一个写异常。 case 2: /* write, not present */ // 如果该区域不能写入,则是一个转到bad_area的坏的写。如果该区域可以写 // 入,则这是一个标记为写时复制(COW)的页面。 if (!(vma->vm_flags & VM_WRITE)) goto bad_area; // 发生写的标志位。 write++; break; // 这是一个读操作,页面也存在。由于该异常的发生没有理由解释,则这里肯定 // 是其他的异常,比如被0除,处理时转到bad_area。 case 1: /* read, present */ goto bad_area; // 在缺失页上进行读操作。在这里保证该页面可读,或可对页面进行exec处理。 // 如果不是这样,则转到bad_area。在这里对exec进行检查是因为x86不能对页面进行exec // 保护,而是使用了读保护标志位。这也正是两者都必须进行检查的原因。 case 0: /* read, not present */ if (!(vma->vm_flags & (VM_READ | VM_EXEC))) goto bad_area; } // 在这里,试着调用handle_mm_fault()来恰当地处理异常。 survive: /* * If for any reason at all we couldn't handle the fault, * make sure we exit gracefully rather than endlessly redo * the fault. */ // 调用handle_mm_fault(),传入与异常有关的信息。这是处理程序独立于体系结构的部分。 switch (handle_mm_fault(mm, vma, address, write)) { // 返回1表示这是一个次异常,更新统计信息。 case 1: tsk->min_flt++; break; // 返回2表示这是一个主异常,更新统计信息。 case 2: tsk->maj_flt++; break; // 返回 0 表示在异常时发生了一些I/O错误,所以转到 do_sigbus 处理程序。 case 0: goto do_sigbus; // 其他的返回值表示不能为该异常分配内存,即发生了内存溢出。实际上,这几 // 乎不会发生,因为 mm/com_kill.c 中涉及的 out_of_memory() 在其他函数中可能已经发生, // out_of_memory() 是一个处理这种情况更为恰当的函数。 default: goto out_of_memory; } /* * 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; } // 释放mm上的锁。 up_read(&mm->mmap_sem); // 因为异常都成功地处理,则在这里返回。 return; // 这是一个坏区域处理程序,坏区域是指比如使用了没有vm_area_struct管理的内存区域 // 等。如果该异常不是一个用户进程异常或者 f00fbug ,则转到no_context标记。 /* * Something tried to access memory that isn't in our memory map.. * Fix it, but check if it's kernel or user first.. */ bad_area: up_read(&mm->mmap_sem); /* User mode accesses just cause a SIGSEGV */ // 错误码为4表示是用户空间,所以这是一个简单的情形,发送SIGSEGV来杀死进程。 if (error_code & 4) { // 设置关于所发生事件的线程信息,这些信息可以在后面由调试器来读。 tsk->thread.cr2 = address; tsk->thread.error_code = error_code; tsk->thread.trap_no = 14; // 对发送了一个SIGSEGV信号进行记录。 info.si_signo = SIGSEGV; // 清除错误码,因为SIGSEGV已经足够用来解释这个错误。 info.si_errno = 0; /* info.si_code has been set above */ // 记录地址。 info.si_addr = (void *)address; // 发送SIGSEGB信号。这个进程将退出,并打印所有的相关信息。 force_sig_info(SIGSEGV, &info, tsk); // 因为异常都成功地处理,这里返回。 return; } /* * Pentium F0 0F C7 C8 bug workaround. */ // 在奔腾第1版中有一个bug是f00f_bug,它会导致处理器不断进行缺页中断。 // 它用于在运行Linux系统中进行局部Dos攻击。这个bug在发布的几个小时后就被发现,并 // 且打上了布丁。现在它只会导致进程的无害中止,而不是系统重启。 if (boot_cpu_data.f00f_bug) { unsigned long nr; nr = (address - idt) >> 3; if (nr == 6) { do_invalid_op(regs, 0); return; } } // 调用search_exception_table()查找异常表, 来确定该异常已经被处理过。如果 // 处理过,则在返回后调用一个合适的异常处理程序。 // 这在 copy_from_user() 和 copy_to_user()时非常重要,在这时会设置一个异常处理程序 // 来监控读和写用户空间中的无效区域,而不需要进行多次代价巨大的检查。 // 这意味着可以调用一个小部分的修整代码,而不是退回到会产生oops的下一块。 no_context: /* Are we prepared to handle this kernel fault? */ if ((fixup = search_exception_table(regs->eip)) != 0) { regs->eip = fixup; return; } /* * Oops. The kernel tried to access some bad page. We'll have to * terminate things with extreme prejudice. */ // 这是一个no_context处理程序。在发生一些破坏性异常时,它们会使进程中止。否则, // 在确定要发生内核异常时,生成一个 oops 报告,然后开始内核陷入。 // // // 否则,在不应该发生内核异常时发生内核陷入,这是一个内核bug,这一块产生 // 一个oops报告。 // 强制性的释放自旋锁,这可能阻止信息显示到屏幕上。 bust_spinlocks(1); // 如果地址 < PAGE_SIZE,则意味着使用了一个null指针,Linux中故意不分配 // 0 页面,这样可以捕获此种类型的异常,这是一个常见的程序错误。 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(" 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); // 打印关于缺页的信息。 if (page & 1) { page &= PAGE_MASK; address &= 0x003ff000; page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT]; printk(KERN_ALERT "*pte = %08lx\n", page); } // 死亡,并产生oops报告,这些可以在后面用于跟踪堆栈,这样开发人员可以更加清晰 // 地看到在哪里或是什么时候发生了异常。 die("Oops", regs, error_code); bust_spinlocks(0); // 强制性地杀死异常进程。 do_exit(SIGKILL); // 这一块处理内存溢出。除非是init,这里经常以结束异常进程为收尾。 /* * We ran out of memory, or some other thing happened to us that made * us unable to handle the page fault gracefully. */ out_of_memory: // 如果进程是init,则折返到survive,在那里试着恰当地处理异常。init应该永远 // 都不会被杀死。 if (tsk->pid == 1) { yield(); goto survive; } // 释放mm信号量。 up_read(&mm->mmap_sem); // 打印帮助信息"You are Dead”。 printk("VM: killing process %s\n", tsk->comm); // 如果这是来自用户空间的,则这里仅杀死该进程。 if (error_code & 4) do_exit(SIGKILL); // 如果来自内核空间,则转到no_context处理程序,在那里将可能导致一个内核oops。 goto no_context; do_sigbus: // 释放mm锁。 up_read(&mm->mmap_sem); /* * Send a sigbus, regardless of whether we were in kernel * or user mode. */ // 填充信息,显示在异常地址处发生了一个SIGBUS,这样在后面调试器可以跟踪到这里。 tsk->thread.cr2 = address; tsk->thread.error_code = error_code; tsk->thread.trap_no = 14; info.si_signo = SIGBUS; info.si_errno = 0; info.si_code = BUS_ADRERR; info.si_addr = (void *)address; // 发送信号。 force_sig_info(SIGBUS, &info, tsk); /* Kernel mode? Handle exceptions or die */ // 如果处于内核模式,则这里试着在no_context时处理该异常。 if (!(error_code & 4)) goto no_context; // 如果是用户空间,则这里仅返回,所有的进程都按预定时的情况全部被杀死。 return; // 这是vmalloc异常处理程序。当页面映射到vmalloc空间时,则仅更新引用页表。对该区 // 域中的各进程引用,将陷入一个异常,进程页表也会在这里与引用页表进行同步操作。 vmalloc_fault: { /* * Synchronize this task's top level page-table * with the 'reference' page table. * * Do _not_ use "tsk" here. We might be inside * an interrupt in the middle of a task switch.. */ // 获得在PGD中的偏移。 int offset = __pgd_offset(address); pgd_t *pgd, *pgd_k; pmd_t *pmd, *pmd_k; pte_t *pte_k; // 从cr3中复制进程的PGD地址到pgd。 asm("movl %%cr3,%0":"=r" (pgd)); // 从进程PGD中计算pgd指针。 pgd = offset + (pgd_t *)__va(pgd); // 计算内核引用PGD。 pgd_k = init_mm.pgd + offset; // 如果pgd项对内核页表无效,则转入no_context。 // 从内核引用页表中复制一份页表项,设置进程页表中的页表项。 if (!pgd_present(*pgd_k)) goto no_context; set_pgd(pgd, *pgd_k); // 对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。 pte_k = pte_offset(pmd_k, address); // 如果PTE不存在,则意味着即使在内核引用页表中该页也无效,所以转到 // no_context进行处理。这里可能是一个内核bug,或者是一个对无用内核空间中随机部分的 // 引用。 if (!pte_present(*pte_k)) goto no_context; // 返回已经更新过的进程页表,并与内核页表进行同步操作。 return; } }
① ⇒ find_vma
find_vma 函数
② ⇒ expand_stack
expand_stack 函数
③ ⇒ handle_mm_fault
(2)扩展栈
(a)expand_stack
这是一个依赖于体系结构的缺页异常处理程序调用函数。VMA 就是一个可以扩大来覆盖该地址的例子。
// include/linux/mm.h /* vma is the first one with address < vma->vm_end, * and even address < vma->vm_start. Have to extend vma. */ static inline int expand_stack(struct vm_area_struct * vma, unsigned long address) { unsigned long grow; /* * vma->vm_start/vm_end cannot change under us because the caller is required * to hold the mmap_sem in write mode. We need to get the spinlock only * before relocating the vma range ourself. */ // 将地址向下取整到页面边界。 address &= PAGE_MASK; // 上锁页表自旋锁。 spin_lock(&vma->vm_mm->page_table_lock); // 计算栈需要扩展多大的页面。 grow = (vma->vm_start - address) >> PAGE_SHIFT; // 检查以保证栈的大小不会超过进程限度。 if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur || // 检查以保证地址空间的大小在栈扩展之后不会超过进程限度。 ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur) { // 如果达到了任何一个限度,则这里返回-ENOMEM,它将导致一个段错误的异常进程。 spin_unlock(&vma->vm_mm->page_table_lock); 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; // 更新进程页表,返回成功。 spin_unlock(&vma->vm_mm->page_table_lock); return 0; }
(3)独立体系结构的页面中断处理程序
这是独立体系结构的页面中断处理程序的顶层函数对。
(a)handle_mm_fault
这个函数的调用图如 4.13 所示。这个函数为待分配的新 PTE 分配所需的 PMD 和 PTE 。它先获取必要的锁来保护页表,然后调用 handle_pte_fault() 陷入页面本身。
// mm/memory.c /* * By the time we get here, we already hold the mm semaphore */ // 该函数的参数如下: // mm 是异常进程的 mm_struct。 // vma 是管理异常发生区域的 vm_area_struct。 // address 是异常地址 // write_access 如果是一个写异常则为1。 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; // 设置该进程的当前状态。 current->state = TASK_RUNNING; // 获取顶层页表中的 pgd 项。 pgd = pgd_offset(mm, address); /* * We need the page table lock to synchronize with kswapd * and the SMP-safe atomic PTE updates. */ // 由于页表将会被改变,所以锁住 mm_struct。 spin_lock(&mm->page_table_lock); // 如果pmd_t不存在,pmd_alloc()将分配一个。 pmd = pmd_alloc(mm, pgd, address); // 如果已经成功分配了 pmd, 则... if (pmd) { // 如果PTE不存在,则为该地址分配一个。 pte_t * pte = pte_alloc(mm, pmd, address); // 调用handle_pte_fault() (见D. 5. 3. 2)来处理缺页中断,返回状态; if (pte) return handle_pte_fault(mm, vma, address, write_access, pte); } // 失败路径,解锁mm_struct。 spin_unlock(&mm->page_table_lock); // 返回-1,这将被解释为一个内存溢出异常。这里是正确的,因为要到达这条线,当 // 且仅当不能分配PMD或PTE。 return -1; }
① ⇒ pmd_alloc
pmd_alloc 函数
② ⇒ pte_alloc
pte_alloc 函数
③ ⇒ handle_pte_fault
(b)handle_pte_fault
这个函数确定发生的异常类型,以及调用的处理函数。如果这是第一次分配页面,则调用 do_no_page() ,do_swap_page() 处理在页面从 tmpfs 中意外地换出到磁盘的情形。do_wp_page() 分割 COW 页面。如果它们都不合适,则简单地更新 PTE 表项。如果是写入,则标记为脏,如果是初期页面,则标记为已访问。
// mm/memory.c /* * These routines also need to handle stuff like marking pages dirty * and/or accessed for architectures that don't do it in hardware (most * RISC architectures). The early dirtying is also good on the i386. * * There is also a hook called "update_mmu_cache()" that architectures * with external mmu caches can use to update those (ie the Sparc or * PowerPC hashed page tables that act as extended TLBs). * * Note the "page_table_lock". It is to protect against kswapd removing * pages from under us. Note that kswapd only ever _removes_ pages, never * adds them. As such, once we have noticed that the page is not present, * we can drop the lock early. * * The adding of pages is protected by the MM semaphore (which we hold), * so we don't need to worry about a page being suddenly been added into * our VM. * * We enter with the pagetable spinlock held, we are supposed to * release it when done. */ // 这个函数的参数与 handle_mm_fault 的参数基本相同,除了这个函数包括的异常 PTE。 static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t * pte) { pte_t entry; // 记录 PTE。 entry = *pte; // 处理存在PTE的情形。 if (!pte_present(entry)) { /* * If it truly wasn't present, we know that kswapd * and the PTE updates will not touch it later. So * drop the lock. */ // 如果PTE从来就没有被填充,则这里利用do_no_page() 来处理 PTE 的分配。 if (pte_none(entry)) return do_no_page(mm, vma, address, write_access, pte); // 如果页面已经被交换到后援存储器,这里利用do_swap_page() 处理。 return do_swap_page(mm, vma, address, pte, entry, write_access); } // 处理页面已经被写入的情形。 if (write_access) { // 如果PTE被标记为只写,则它是一个COW页面,所以这里利用do_wp_page() 处理。 if (!pte_write(entry)) return do_wp_page(mm, vma, address, pte, entry); // 否则,这里仅标记页面为脏。 entry = pte_mkdirty(entry); } // 标记页面为已访问。 entry = pte_mkyoung(entry); // establish_pte() 复制PTE,然后更新TLB和MMU高速缓存。这里并不复制新的 // PTE,但是某些体系结构需要更新TLB和MMU。 establish_pte(vma, address, pte, entry); // 解锁 mm_struct 并返回一个发生了的小异常。 spin_unlock(&mm->page_table_lock); return 1; }
① ⇒ do_no_page
do_no_page 函数
② ⇒ do_swap_page
do_swap_page 函数
③ ⇒ do_wp_page
do_wp_page 函数
(4)请求分配
(a)do_no_page
这个函数的调用图如图 4.14 所示。这个函数在页面第一次被引用时调用,这样它可以在需要时分配和填充数据。如果它是一个由 VMA 中的 vm_ops 或缺少 nopage() 函数所决定的匿名页,则调用 do_anonymous_page() 。否则,调用替代的 nopage() 函数来分配页面,然后插入到这里的页表中。函数的功能如下:
检查是否有可用 do_anonymous_page() 。如果可以则调用它,返回它分配的页面。如果不可以调用,则调用替代的 nopage() 函数,并保证它能够成功地分配一页。
如果需要则尽早使 COW 失效。
将页面加入到页表项,然后调用适合的体系结构相关的钩子函数。
// mm/memory.c /* * do_no_page() tries to create a new page mapping. It aggressively * tries to share with existing pages, but makes a separate copy if * the "write_access" parameter is true in order to avoid the next * page fault. * * As this is called only for pages that do not currently exist, we * do not need to flush old virtual caches or the TLB. * * This is called with the MM semaphore held and the page table * spinlock held. Exit with the spinlock released. */ // 这里提供的参数与 handle_pte_fault() 的相同。 static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t *page_table) { struct page * new_page; pte_t entry; // 如果没有提供vm_ops,或者没有提供nopageO函数,则这里调用do_anonymous_page() // 来分配一个页面并返回该页面。 if (!vma->vm_ops || !vma->vm_ops->nopage) return do_anonymous_page(mm, vma, page_table, write_access, address); // 否则,这里释放页表锁,因为nopage()在上锁自旋锁的时候不能被调用。 spin_unlock(&mm->page_table_lock); // 调用替代的nopage()函数,在文件系统的情形下,这里频繁地调用 // filemap_nopage() (见D. 6. 4. 1),但每个不同的设备驱动器不同。 new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0); // 如果返回了 NULL,则意味着在nopage()函数中发生了某些错误,如在读磁 // 盘的时候发生了一个I/O错误。在这种情形下,返回0,它将导致发送一个SIGBUS到中断处 // 理进程。 if (new_page == NULL) /* no page was available -- SIGBUS */ return 0; // 如果返回了 NOPAGE_OOM, 则物理页面分配器分配页面失败,返回-1,它 // 将强制性地杀死该进程。 if (new_page == NOPAGE_OOM) return -1; // 这一块在适当的时候尽早使cow失效。如果发生的异常是写异常,且区域没有利用 // VM_SHARED共享,则要使COW失效。如果在这种情况下没有使COW失效,则一旦返回 // 就立即发生第二次异常。 /* * Should we do an early C-O-W break? */ // 检查COW是否需要立即失效。 if (write_access && !(vma->vm_flags & VM_SHARED)) { // 如果是,则这里为进程分配一个新页。 struct page * page = alloc_page(GFP_HIGHUSER); // 如果不能分配页面,则这里将利用函数nopage()返回的页面引用计数减1, // 并返回-1报告内存不足。 if (!page) { page_cache_release(new_page); return -1; } // 否则,复制页面内容。 copy_user_highpage(page, new_page, address); // 将返回页面的引用计数减1,该页面可能还被其他进程使用。 page_cache_release(new_page); // 将新页加入到LRU链表中,这样可以在后面由kswapd回收。 lru_cache_add(page); new_page = page; } // 再一次上锁页表,因为已经完成了分配,页表即将被更新。 spin_lock(&mm->page_table_lock); /* * This silly early PAGE_DIRTY setting removes a race * due to the bad i386 page protection. But it's valid * for other architectures too. * * Note that if write_access is true, we either now have * an exclusive copy of the page, or this is a shared mapping, * so we can make it writable and dirty to avoid having to * handle that later. */ /* Only go through if we didn't race with anybody else... */ // 检查在我们将使用的表项中是否仍然没有PTE。如果在这里同时有两个异常,则可 // 能是另外一个处理器已经完成了缺页中断处理,所以这个应该退出。 if (pte_none(*page_table)) { // 如果没有PTE进入,这里就完成异常处理。 // 将RSS计数加1,因为进程现在正在使用另外一个页面。在这里确实需要一次检查 // 来保证这不是一个全局0页面,否则RSS计数将会不正确。 ++mm->rss; // 因为页面将被映射到进程空间,所以对某些体系结构而言可能需要将页面写到那些 // 不为进程所见的内核空间页面中。flush_page_to_ram()将保证CPU高速缓存是连贯的。 flush_page_to_ram(new_page); // flush_icache_page()原理上是相似的,除了它保证icache和dcache是连贯的。 flush_icache_page(vma, new_page); // 以合适的权限来创建pte_t 。 entry = mk_pte(new_page, vma->vm_page_prot); // 如果这是一个写,则保证PTE拥有写权限。 if (write_access) entry = pte_mkwrite(pte_mkdirty(entry)); // 将新PTE放入到进程页表中。 set_pte(page_table, entry); } else { // 如果已经填充了 PTE,则从函数nopage()获取的页面将被释放。 /* One of our sibling threads was faster, back out. */ // 将页面引用计数减1,如果减到0,则释放页面。 page_cache_release(new_page); // 释放 mm_struct锁,并返回1来表明这是一个次缺页中断,因为不需要对这 // 种异常进行大的处理,大部分工作已经在竞争胜方中完成。 spin_unlock(&mm->page_table_lock); return 1; } /* no need to invalidate: a not-present page shouldn't be cached */ // 更新体系结构所需的MMU高速缓存。 update_mmu_cache(vma, address, entry); // 释放 mm_struct锁,并返回2来表明这是一个主缺页中断。 spin_unlock(&mm->page_table_lock); return 2; /* Major fault */ }
① ⇒ do_anonymous_page
② ⇒ filemap_nopage
(b)do_anonymous_page
这个函数为第一次访问页面的进程分配一个新页面。如果是读访问,则将一个系统范围内全 0 页面映射到进程。如果是写访问,则分配一个填充 0 的页面,然后放入进程页表。
// mm/memory.c /* * We are called with the MM semaphore and page_table_lock * spinlock held to protect against concurrent faults in * multithreaded programs. */ // 参数与传递给handle_pte_fault() (见D. 5. 3. 2)的参数相同。 static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr) { pte_t entry; /* Read-only mapping of ZERO_PAGE. */ // 对读访问,这里只是简单地映射系统范围的empty_zero_page,它由给定权限的 // ZERO_PAGE() 宏返回。由于页面写保护,所以如果对这个页面进行写操作将导致 // 一个缺页中断。 entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); /* ..except if it's a write access */ // 如果是写异常,则分配一个新页并用0填充。 if (write_access) { struct page *page; /* Allocate our own private page. */ // 解锁mm_struct,这样分配新页的进程将睡眠。 spin_unlock(&mm->page_table_lock); // 分配一个新页。 page = alloc_page(GFP_HIGHUSER); // 如果不能分配一页,则这里返回-1来处理OOM的情形。 if (!page) goto no_mem; // 用0填充页面。 clear_user_highpage(page, addr); // 重新获得锁,因为页表将被更新。 spin_lock(&mm->page_table_lock); if (!pte_none(*page_table)) { page_cache_release(page); spin_unlock(&mm->page_table_lock); return 1; } // 更新进程的RSS。注意如果RSS由全局0页面映射,则没有被更新。因为在1195 // 行已经发生了只读异常。 mm->rss++; // 保证高速缓存连贯。 flush_page_to_ram(page); // 标记PTE为可写和脏的,因为它已经被写入。 entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot))); // 将页面加入到LRU链表,这样它可能由换出进程回收。 lru_cache_add(page); // 标记页面为已访问,这样保证页面标记为活跃,并在active链表的首部。 mark_page_accessed(page); } // 为该进程调整页表中的PTE。 set_pte(page_table, entry); /* No need to invalidate - it was non-present before */ // 如果体系结构需要则更新MMU高速缓存。 update_mmu_cache(vma, addr, entry); // 释放页表锁。 spin_unlock(&mm->page_table_lock); // 返回次缺页中断。即使页面分配器可能已经开始写出页面,但是仍然没有从磁盘读 // 入数据来填充这个页面。 return 1; /* Minor fault */ no_mem: return -1; }
① ⟺ clear_user_highpage
// include/linux/highmem.h static inline void clear_user_highpage(struct page *page, unsigned long vaddr) { void *addr = kmap_atomic(page, KM_USER0); clear_user_page(addr, vaddr); kunmap_atomic(addr, KM_USER0); }
(5)请求分页
(a)do_swap_page
这个函数的调用图如图 4.15 所示。这个函数处理页面已经被换出的情形。一个已经被换出的页面在预读时如果是被多个进程所共享或者是刚刚换出则可能存在于交换高速缓存中。这个函数分成 3 个部分:
- 在交换高速缓存中找到该页面。
- 如果不存在,则调用 swapin_readahead() 来读入页面。
- 将页面插入到进程页表中。
// mm/memory.c /* * We hold the mm semaphore and the page_table_lock on entry and * should release the pagetable lock on exit.. */ // 参数与 handle_pte_fault() (见 D 5.3.2)的一样 static int do_swap_page(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long address, pte_t * page_table, pte_t orig_pte, int write_access) { // 这一块是函数的前面部分。它检查在交换高速缓存中的页面。 struct page *page; // 从PTE中获取交换项信息。 swp_entry_t entry = pte_to_swp_entry(orig_pte); pte_t pte; int ret = 1; // 释放mm_struct自旋锁。 spin_unlock(&mm->page_table_lock); // 在交换高速缓存中查找该页面。 page = lookup_swap_cache(entry); // 如果页面不在交换高速缓存中,则这一块利用swapin_readahead()从后援存储器中读入该 // 页,swapin_readahead()读入请求页以及其后的一些页。完成后,read_swap_cache_async()将 // 返回该页。 // // 如果页面不在交换高速缓存中则执行这一块。 if (!page) { // swapin_readahead() (见D. 6. 6. 1)读入请求页,以及其后的一些页。读入页数由 mm/swap.c // 中page_cluster变量决定,这个变量在小于16 MB内存时初始化为2,否则初始化为3。 // 除非已经读到错误的或者空的页表项,这一块在读取请求页后还将读取 2^page_cluster 个页面 swapin_readahead(entry); // read_swap_cache_async() (见K. 3. L 1)将查找请求页并在需要时将其从磁盘读入 // 交换高速缓存。 page = read_swap_cache_async(entry); // 如果页面不存在,则换入的该页上将有异常,并在释放自旋锁时将其从高速缓存中移除。 if (!page) { /* * Back out if somebody else faulted in this pte while * we released the page table lock. */ int retval; // 上锁 mm_struct。 spin_lock(&mm->page_table_lock); // 将两个PTE进行比较。如果它们不匹配,就返回-1表明一个I/O错误。如果匹 // 配,则返回1表明一个幂次缺页中断,因为在这个特殊页上不需要一次磁盘访问。 retval = pte_same(*page_table, orig_pte) ? -1 : 1; // 释放 mm_struct 并返回状态位。 spin_unlock(&mm->page_table_lock); return retval; } /* Had to read the page from swap area: Major fault */ // 磁盘必须被访问以标记这是一个主缺页中断。 ret = 2; } // 这一块将页面放到进程页表中。 // mark_page_accessed() (见J. 2. 3.1)将标记页面为活跃,这样它将被移到活跃LRU // 链表的顶端。 mark_page_accessed(page); // 上锁页面,这里的副作用是等待换入页面I/O完成。 lock_page(page); /* * Back out if somebody else faulted in this pte while we * released the page table lock. */ spin_lock(&mm->page_table_lock); // 如果其他人在我们之前已经陷入页面,则撤销对页面的引用,释放锁,返回 // 个次中断。 if (!pte_same(*page_table, orig_pte)) { spin_unlock(&mm->page_table_lock); unlock_page(page); page_cache_release(page); return 1; } /* The page isn't present yet, go ahead with the fault. */ // 函数swap_free()(见K. 2. 2. 1)减少对交换区表项的引用,如果减到0,则它实际上被释放了。 swap_free(entry); // 在页面已经交换出去以避免每次都必须搜索一个空闲槽之后,在交换空间的 // 页槽依旧为页面保留着。如果交换空间已满,则打破保留,释放该槽给另外一个页面。 if (vm_swap_full()) remove_exclusive_swap_page(page); // 即将使用这个页面,所以这里增加mm_struct的RSS计数。 mm->rss++; // 标记该页面的PTE。 pte = mk_pte(page, vma->vm_page_prot); // 如果页面已经被写入,而且没有被多个进程共享,这里将标记该页面为脏,这样它可 // 以与后援存储器和其他进程的交换缓冲区保持同步。 if (write_access && can_share_swap_page(page)) pte = pte_mkdirty(pte_mkwrite(pte)); // 解锁页面。 unlock_page(page); // 因为页面将被映射到进程空间,所以对某些体系结构而言就可能将页面写入到进程 // 不可见的内核空间。flush_page_to_ram()保证高速缓存是连贯的。 flush_page_to_ram(page); // flush_icache_page()原理上相似,此外它还要保证icache和dcache连贯。 flush_icache_page(vma, page); // 设置进程页表中的PTE。 set_pte(page_table, pte); /* No need to invalidate - it was non-present before */ // 如果体系结构需要则更新MMU高速缓存。 update_mmu_cache(vma, address, pte); // 解锁 mm_struct, 并返回它是否是一个主或次缺页中断。 spin_unlock(&mm->page_table_lock); return ret; }
① ⇒ swapin_readahead
(b)can_share_swap_page
这个函数决定该页面的交换高速缓存表项是否可用。如果没有其他的引用则它可用。大部分的工作都由 exclusive_swap_page() 来完成,但是这个函数首先进行一些基本的检查以避免必须获取很多的锁。
// mm/swapfile.c /* * We can use this swap cache entry directly * if there are no other references to it. * * Here "exclusive_swap_page()" does the real * work, but we opportunistically check whether * we need to get all the locks first.. */ int can_share_swap_page(struct page *page) { int retval = 0; // 这个函数从异常路径中调用,所以页面必须上锁。 if (!PageLocked(page)) BUG(); // 按照引用次数进行分支选择。 switch (page_count(page)) { // 如果计数为3,但没有与之相关的缓冲区,则有多于一个的进程正在使用它。如 // 果页面由一个交换文件而不是一个分区后援支持,则缓冲区可能仅与一个进程相关联。 case 3: if (!page->buffers) break; /* Fallthrough */ // 如果计数仅为2,但是它不是一个交换高速缓存的页面,则它没有槽可供共享, // 所以它返回false。否则,它利用exclusive_swap_page()(见D. 5. 5. 3)进行完全检查。 case 2: if (!PageSwapCache(page)) break; retval = exclusive_swap_page(page); break; // 如果页面是保留页面,则是一个全局ZERO_PAGE,不能被共享。否则,可以确 // 定这个页面就是仅有的这个ZERO_PAGE。 case 1: if (PageReserved(page)) break; retval = 1; } return retval; }
(c)exclusive_swap_page
这个函数检查进程是否是上锁交换页面的惟一用户。
// mm/swapfile.c /* * Check if we're the only user of a swap page, * when the page is locked. */ static int exclusive_swap_page(struct page *page) { // 缺省情况下,这里返回false。 int retval = 0; struct swap_info_struct * p; swp_entry_t entry; // 如2.5节所述,页面的swp_entry_t存放于page->index中。 entry.val = page->index; // 利用 swap_info_get() ( K. 2. 3. 1)来获取 swap_info_struct。 p = swap_info_get(entry); // 如果存在槽,则这里检查我们是否是专门用户,如果是则返回true。 if (p) { /* Is the only swap cache user the cache itself? */ // 检查这个槽是否仅被高速缓存自身使用。如果是,则需要利用获得的 pagecache_lock // 再次检查页面计数。 if (p->swap_map[SWP_OFFSET(entry)] == 1) { /* Recheck the page count with the pagecache lock held.. */ spin_lock(&pagecache_lock); // 如果存在缓冲区,则page->buffers将赋值为1。这样可以有效地检查该进程是 // 否是页面的惟一用户。如果是retval设为1,则返回true。 if (page_count(page) - !!page->buffers == 2) retval = 1; spin_unlock(&pagecache_lock); } // 释放由swap_info_get()(见K. 2. 3. 1)持有的槽引用。 swap_info_put(p); } return retval; }
(6)写时复制(COW) 页面
(a)do_wp_page
这个函数的调用图如图 4.16 所示。函数处理当一个用户试着向一个由多进程共享的私有页写的情形,如在发生 fork() 后。基本操作是分配页面,复制内容到新页,旧页的共享计数减 1 。
// mm/memory.c /* * This routine handles present pages, when users try to write * to a shared page. It is done by copying the page to a new address * and decrementing the shared-page counter for the old page. * * Goto-purists beware: the only reason for goto's here is that it results * in better assembly code.. The "default" path will see no jumps at all. * * Note that this routine assumes that the protection checks have been * done by the caller (the low-level page fault routine in most cases). * Thus we can safely just mark it writable once we've done any necessary * COW. * * We also mark the page dirty at this point even though the page will * change only once the write actually happens. This avoids a few races, * and potentially makes it more efficient. * * We hold the mm semaphore and the page_table_lock on entry and exit * with the page_table_lock released. */ // 参数与 handle_pte_fault()的相同。 static int do_wp_page(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, pte_t *page_table, pte_t pte) { struct page *old_page, *new_page; old_page = pte_page(pte); // 在PTE中获取当前页面的引用,并保证它有效。 if (!VALID_PAGE(old_page)) goto bad_wp_page; // 首先试着锁住页面。如果返回0,则意味着页面在前面已经被释放锁。 if (!TryLockPage(old_page)) { // 如果我们想上锁,这里调用can_share_swap_page()(见D. 5. 5. 2)以确定我们是否是 // 该页面交换槽的专门用户。如果是,则意味着我们是最后一个使COW失效的进程,这样我们 // 可以简单地使用这一页,而不用再次分配一个新页。 int reuse = can_share_swap_page(old_page); unlock_page(old_page); // 如果我们是交换槽的惟一用户,则意味着我们是该页的惟一用户和最后一个使 // COW失效的进程。因此,可以重新构造PTE,而我们返回一个次异常。 if (reuse) { flush_cache_page(vma, address); establish_pte(vma, address, page_table, pte_mkyoung(pte_mkdirty(pte_mkwrite(pte)))); spin_unlock(&mm->page_table_lock); return 1; /* Minor fault */ } } /* * Ok, we need to copy. Oh, well.. */ // 我们需要复制该页,所以首先获取对旧页的引用,在我们还没有完成之前,它不会消失。 page_cache_get(old_page); // 因为我们将调用alloc_page() (见F. 2.1小节),而这可能睡眠,所以释放自旋锁。 spin_unlock(&mm->page_table_lock); // 分配页面,并保证成功返回一页。 new_page = alloc_page(GFP_HIGHUSER); if (!new_page) goto no_mem; // 猜测这个函数的操作是很容易的。如果页面打散为全局0页面,则使用 // clear_user_highpage()将其内容清 0。否则,copy_user_highpage()复制实际的内容。 copy_cow_page(old_page,new_page,address); /* * Re-check the pte - we dropped the lock */ // 由于alloc_page() (见F. 2.1小节)释放页表锁,这样重新获取页表锁。 spin_lock(&mm->page_table_lock); // 保证在此期间PTE没有改变,如果在释放自旋锁时发生了另外一个异常则它可能发生改变。 if (pte_same(*page_table, pte)) { // 当PageReserved()为真时更新RSS。这只会发生在如果陷入的页面是全局 // ZERO_PAGE,而且没有在RSS中计数的情况下。如果这是一个普通页面,进程将在异常发 // 生之后使用相同数量的物理帧。在以前,除了 0 页面,它将使用一个新页面帧, // 所以 rss++ 反映新页面的使用情况。 if (PageReserved(old_page)) ++mm->rss; // break_cow()负责调用体系结构的钩子函数来保证CPU高速缓存和TLB得到更新, // 然后在PTE中建立一个新页面。它首先调用flush_page_to_ram(),这个函数在 // 一个struct page将被放到用户空间时必须调用。下一步是调用flush_cache_page() // 清理来自于CPU高速缓存的页面。最后调用sestablish_pte(),它在PTE中建立一个新页面。 break_cow(vma, new_page, address, page_table); // 将页面加入到LRU链表中。 lru_cache_add(new_page); /* Free the old page.. */ new_page = old_page; } // 释放自旋锁。 spin_unlock(&mm->page_table_lock); // 撤销页面引用。 page_cache_release(new_page); page_cache_release(old_page); // 返回一个次异常。 return 1; /* Minor fault */ // 这是一个错误的COW失效,它仅在轻型内核中使用。这里打印一条提示信息,然后返回。 bad_wp_page: spin_unlock(&mm->page_table_lock); printk("do_wp_page: bogus page at address %08lx (page 0x%lx)\n",address, (unsigned long)old_page); return -1; // 这次页面分配失败,所以这里释放对旧页面的引用并返回 -1 no_mem: page_cache_release(old_page); return -1; }
① ⇒ can_share_swap_page
② ⟺ copy_cow_page
// mm/memory.c /* * We special-case the C-O-W ZERO_PAGE, because it's such * a common occurrence (no need to read the page to know * that it's zero - better for the cache and memory subsystem). */ static inline void copy_cow_page(struct page * from, struct page * to, unsigned long address) { if (from == ZERO_PAGE(address)) { clear_user_highpage(to, address); return; } copy_user_highpage(to, from, address); }
③ ⟺ break_cow
// mm/memory.c /* * We hold the mm semaphore for reading and vma->vm_mm->page_table_lock */ static inline void break_cow(struct vm_area_struct * vma, struct page * new_page, unsigned long address, pte_t *page_table) { flush_page_to_ram(new_page); flush_cache_page(vma, address); establish_pte(vma, address, page_table, pte_mkwrite(pte_mkdirty(mk_pte(new_page, vma->vm_page_prot)))); }
6、页面相关的磁盘 I/O
(1)一般文件读
这里更多的是 I/O 管理而不是 VM 的领域,但是由于它通过页面高速缓存完成操作,所以在这里我们简要讨论一番。虽然 generic_file_write 。在本书中并不讨论,但是实际上它与读操作一样。因此,如果你理解了读是如何发生的,那么写操作的理解也不会对你造成困难。
(a)generic_file_read
这是一个用于任何文件系统从页面高速缓存读入页面的一般文件读函数。对普通 I/O 而言,它负责利用 do_generic_file_read() 和 file_read_actor() 来建立一个 read_descriptor_t 。对直接 I/O 而言,这个函数是 generic_file_direct_IO() 的封装。
// mm/filemap.c /* * This is the "read()" routine for all filesystems * that can use the page cache directly. */ ssize_t generic_file_read(struct file * filp, char * buf, size_t count, loff_t *ppos) { ssize_t retval; if ((ssize_t) count < 0) return -EINVAL; // 这一块有关普通文件I/O。 // 如果这是一个直接I/O,则跳转到 o_direct 标号。 if (filp->f_flags & O_DIRECT) goto o_direct; retval = -EFAULT; // 如果对用户空间页面具有写权限,则继续。 if (access_ok(VERIFY_WRITE, buf, count)) { retval = 0; // 如果count为0,则不进行I/O。 if (count) { read_descriptor_t desc; // 组装一个 read_descriptor_t 结构,file_read_actor()(见 L. 3. 2. 3)要用到。 desc.written = 0; desc.count = count; desc.buf = buf; desc.error = 0; // 进行文件读。 do_generic_file_read(filp, ppos, &desc, file_read_actor); // 从读描述符结构中提取写的字节数。 retval = desc.written; // 如果发生错误,则这里提取这种错误。 if (!retval) retval = desc.error; } } out: // 返回读取的字节数或者发生的错误。 return retval; // 这一块有关直接I/O。它主要负责提取 generic_file_direct_IO() 的参数。 o_direct: { loff_t pos = *ppos, size; // 获取这个 struct file 用到的 address_space。 struct address_space *mapping = filp->f_dentry->d_inode->i_mapping; struct inode *inode = mapping->host; retval = 0; // 如果不再请求I/O,这里跳转到out以避免更新inode的访问次数。 if (!count) goto out; /* skip atime */ down_read(&inode->i_alloc_sem); down(&inode->i_sem); // 获取文件大小。 size = inode->i_size; // 如果当前位置在文件结束之前,则读是安全的,并调用 generic_file_direct_IO()。 if (pos < size) { retval = generic_file_direct_IO(READ, filp, buf, count, pos); // 如果成功读,则这里更新读指针在文件中的当前位置。 if (retval > 0) *ppos = pos + retval; } up(&inode->i_sem); up_read(&inode->i_alloc_sem); // 更新访问次数。 UPDATE_ATIME(filp->f_dentry->d_inode); // 转到out,在那里返回retval。 goto out; } }
(b)do_generic_file_read
这是一般文件读操作的核心部分。它负责页面不在页面高速缓存中时分配页面。如果存在页面时则保证页面是最新的,然后保证设置了合适大小的预读窗口。
/* * This is a generic file read routine, and uses the * inode->i_op->readpage() function for the actual low-level * stuff. * * This is really ugly. But the goto's actually try to clarify some * of the logic when it comes to error handling etc. */ void do_generic_file_read(struct file * filp, loff_t *ppos, read_descriptor_t * desc, read_actor_t actor) { struct address_space *mapping = filp->f_dentry->d_inode->i_mapping; struct inode *inode = mapping->host; unsigned long index, offset; struct page *cached_page; int reada_ok; int error; // 获取该块设备的最大预读窗口大小。 int max_readahead = get_max_readahead(inode); cached_page = NULL; // 计算页面索引,其中包含有当前文件位置指针。 index = *ppos >> PAGE_CACHE_SHIFT; // 计算持有当前文件位置指针的页面内偏移。 offset = *ppos & ~PAGE_CACHE_MASK; /* * If the current position is outside the previous read-ahead window, * we reset the current read-ahead context and set read ahead max to zero * (will be set to just needed value later), * otherwise, we assume that the file accesses are sequential enough to * continue read-ahead. */ // 如注释所言,如果当前文件位置在当前预读窗口以外,则需要重置预读窗口。 // 在这里重置为0,然后在需要的时候由 generic_file_readahead() (见D. 6.1. 3)调整。 if (index > filp->f_raend || index + filp->f_rawin < filp->f_raend) { reada_ok = 0; filp->f_raend = 0; filp->f_ralen = 0; filp->f_ramax = 0; filp->f_rawin = 0; } else { reada_ok = 1; } /* * Adjust the current value of read-ahead max. * If the read operation stay in the first half page, force no readahead. * Otherwise try to increase read ahead max just enough to do the read request. * Then, at least MIN_READAHEAD if read ahead is ok, * and at most MAX_READAHEAD in all cases. */ // 如注释所言,如果在当前页的后半部分,则稍微地调整预读窗口。 if (!index && offset + desc->count <= (PAGE_CACHE_SIZE >> 1)) { filp->f_ramax = 0; } else { unsigned long needed; needed = ((offset + desc->count) >> PAGE_CACHE_SHIFT) + 1; if (filp->f_ramax < needed) filp->f_ramax = needed; if (reada_ok && filp->f_ramax < vm_min_readahead) filp->f_ramax = vm_min_readahead; if (filp->f_ramax > max_readahead) filp->f_ramax = max_readahead; } // 这里的循环遍历每个需要满足读请求的页面。 for (;;) { struct page *page, **hash; unsigned long end_index, nr, ret; // 计算文件的末端在哪个页面。 end_index = inode->i_size >> PAGE_CACHE_SHIFT; // 如果当前索引已经超出末尾,则从这里退出,因为我们正试着读超过文件末 // 尾的地方。 if (index > end_index) break; // 计算nr为在当前页还需要读的字节数。这一块考虑可能用到的文件最后一 // 页,以及页中当前文件位置。 nr = PAGE_CACHE_SIZE; if (index == end_index) { nr = inode->i_size & ~PAGE_CACHE_MASK; if (nr <= offset) break; } nr = nr - offset; /* * Try to find the data in the page cache.. */ // 在页面高速缓存中搜索这一页。 hash = page_hash(mapping, index); spin_lock(&pagecache_lock); page = __find_page_nolock(mapping, index, *hash); // 如果页面不在页面高速缓存中,则跳转到no_cached_page,在那里分配一个页面。 if (!page) goto no_cached_page; // 这一块,在页面高速缓存中已经找到该页面。 found_page: // 引用页面高速缓存中的这一页,以使它不会过早地被释放。 page_cache_get(page); spin_unlock(&pagecache_lock); // 如果页面不是最新的,则转到page_not_up_to_date,在那里利用磁盘中的信 // 息更新页面。 if (!Page_Uptodate(page)) goto page_not_up_to_date; // 利用 generic_file_readahead()(见 D. 6. 1. 3)完成预读文件。 generic_file_readahead(reada_ok, filp, inode, page); // 在这一块,页面已经在页面高速缓存中,且已经准备好由文件读操作函数读。 page_ok: /* If users can be writing to this page using arbitrary * virtual addresses, take care about potential aliasing * before reading the page on the kernel side. */ // 由于其他用户可能在写这个页面,所以调用flush_dcache_page()来保证所有 // 的改变都可见。 if (mapping->i_mmap_shared != NULL) flush_dcache_page(page); /* * Mark the page accessed if we read the * beginning or we just did an lseek. */ // 由于刚才已经访问过该页面,所以这里调用mark_page_accessed() (见J.2.3.1) // 来将其移到activjlist中。 if (!offset || !filp->f_reada) mark_page_accessed(page); /* * Ok, we have the page, and it's up-to-date, so * now we can copy it to user space... * * The actor routine returns how many bytes were actually used.. * NOTE! This may not be the same as how much of a user buffer * we filled up (we may be padding etc), so we can only update * "pos" here (the actor routine has to update the user buffer * pointers and the remaining count). */ // 调用操作函数。在这种情况下,操作函数是file_read_actor() (见L. 2.3. 2),它负责 // 从页面复制字节到用户空间。 ret = actor(desc, page, offset, nr); // 更新文件中的当前偏移。 offset += ret; // 如果有必要,则移动到下一个页面。 index += offset >> PAGE_CACHE_SHIFT; // 更新我们当前正在读的页面中的偏移。记住我们刚才已经进入了文件的下一个页面。 offset &= ~PAGE_CACHE_MASK; // 释放我们对该页的引用。 page_cache_release(page); // 如果还有数据读,这里再次循环以读下一个页面。否则,跳出,因为读操作已经完成。 if (ret == nr && desc->count) continue; break; /* * Ok, the page was not immediately readable, so let's try to read ahead while we're at it.. */ // 在这一块中,被读的页面并不是磁盘上最新的数据信息,调用generic_file_readahead()更 // 新当前页面和预读指针,因为这些都是I/O需要的。 page_not_up_to_date: // 调用generic_file_readahead()(见D. 6. 1. 3),在需要的时候同步当前页面和预读指针。 generic_file_readahead(reada_ok, filp, inode, page); // 如果页面现在是最新的,则转到page_ok,在那里开始复制字节到用户空间。 if (Page_Uptodate(page)) goto page_ok; /* Get exclusive access to the page ... */ // 否则,预读指针上发生了改变,这里对页面上锁以进行排他访问。 lock_page(page); /* Did it get unhashed before we got the lock? */ // 如果在还未获得自旋锁的时候页面已经从页面高速缓存中移除,则这里释放 // 对页面的引用,重新开始。在第二轮中,将分配页面并重新插入到页面高速缓存中。 if (!page->mapping) { UnlockPage(page); page_cache_release(page); continue; } /* Did somebody else fill it already? */ // 如果某个人在我们没有上锁的页面上更新了页面,则这里再次解锁,然后转 // 到page_ok, 开始读字节到用户空间。 if (Page_Uptodate(page)) { UnlockPage(page); goto page_ok; } // 在这一块,预读与由readpage()函数提供的 address_space 页面读同步。 readpage: /* ... and start the actual read. The read will unlock the page. */ // 调用address_space文件系统特定的readpage()函数。在多数情形下,这里最后将 // 调用在 fs/buffer.c() 中声明的 block_read_full_page()函数。 error = mapping->a_ops->readpage(filp, page); // 如果没有发生错误,页面也是最新的,则转到page_ok开始向用户空间读字节。 if (!error) { if (Page_Uptodate(page)) goto page_ok; /* Again, try some read-ahead while waiting for the page to finish.. */ // 否则,调度某个预读,因为我们被强制性地等待I/O。 generic_file_readahead(reada_ok, filp, inode, page); // 等待请求页上的I/O完成。如果成功完成,则转到 page_ok。 wait_on_page(page); if (Page_Uptodate(page)) goto page_ok; // 否则,发生了错误,所以这里设置-EIO返回到用户空间。 error = -EIO; } /* UHHUH! A synchronous read error occurred. Report it */ // 发生了 I/O 错误,记录错误,然后释放对当前页面的引用。这个错误将由 // generic_file_read() (见 D. 6. L 1) 从 read_descriptor_t 结构中获取。 desc->error = error; page_cache_release(page); break; // 在这一块,页面不在页面高速缓存中,所以这里分配一个页面并加入到页面高速缓存中。 // 如果还没有分配一个高速缓存页面,则这里分配一个,并保证在我们睡眠时 // 其他人没有将其插入到页面高速缓存中。 no_cached_page: /* * Ok, it wasn't cached, so we need to create a new * page.. * * We get here with the page cache lock held. */ if (!cached_page) { // 释放 pagecache_lock, 因为 page_cache_alloc() 可能睡眠。 spin_unlock(&pagecache_lock); // 分配一页,如果分配失败则设置-ENOMEM为返回值。 cached_page = page_cache_alloc(mapping); if (!cached_page) { desc->error = -ENOMEM; break; } /* * Somebody may have added the page while we * dropped the page cache lock. Check for that. */ // 再次获得pagecache_lock, 在页面高速缓存中搜索以保证在释放锁的时候其 // 他进程没有将页面插入到页面高速缓存中。 spin_lock(&pagecache_lock); page = __find_page_nolock(mapping, index, *hash); // 如果其他进程已经添加一个合适页面到页面高速缓存中,则这里跳转到 // found_page, 因为我们刚才分配的那一页已经不再需要了。 if (page) goto found_page; } /* * Ok, add the new page to the hash-queues... */ // 否则,在这里将我们刚才分配的那一页加入到页面高速缓存中。 page = cached_page; __add_to_page_cache(page, mapping, index, hash); spin_unlock(&pagecache_lock); // 将页面加入到LRU链表中。 lru_cache_add(page); // 设置cached_page为NULL,因为它现在正在使用中。 cached_page = NULL; // 跳转到readpage以调用将从磁盘读的页面。 goto readpage; } // 更新我们在文件中的位置。 *ppos = ((loff_t) index << PAGE_CACHE_SHIFT) + offset; filp->f_reada = 1; // 如果分配的一页对页面高速缓存已经多余,则不需要搜索,直接在这里释放该页面。 if (cached_page) page_cache_release(cached_page); // 更新文件的访问次数。 UPDATE_ATIME(inode); }
(c)generic_file_readahead
这个函数完成一般文件预读。预读部分是代码中很少见的着重注释的地方。推荐读者去读在 mm/filemap.c 中标记为 “预读上下文" 的注释。
// mm/filemap.c static void generic_file_readahead(int reada_ok, struct file * filp, struct inode * inode, struct page * page) { unsigned long end_index; // 以提供的page为基础获取开始的索引。 unsigned long index = page->index; unsigned long max_ahead, ahead; unsigned long raend; // 获取这个块设备的最大预读指针。 int max_readahead = get_max_readahead(inode); // 获取在文件末端的页面索引。 end_index = inode->i_size >> PAGE_CACHE_SHIFT; // 获取struct file的预读窗口末端。 raend = filp->f_raend; max_ahead = 0; /* * The current page is locked. * If the current position is inside the previous read IO request, do not * try to reread previously read ahead pages. * Otherwise decide or not to read ahead some pages synchronously. * If we are not going to read ahead, set the read ahead context for this * page only. */ // 这一块碰到已经上锁的一页。所以这里必须确定是否暂时停止预读。 // 如果当前页面因为I/O而被上锁,则这里检查当前页面是否在最后一个预读窗口, // 如果是,则没必要再次预读。如果不是,或者前面还没有进行预读,则这里更新预读上下文。 if (PageLocked(page)) { // 首先检查预读在前面是否已经进行过。然后检查当前上锁的页面是否在上一次预 // 读完成的地址之后。第3步检查当前上锁的页面是否在当前预读窗口中。 if (!filp->f_ralen || index >= raend || index + filp->f_rawin < raend) { // 更新预读窗口的末端。 raend = index; // 如果预读窗口的末端不在文件末端之后,则这里设置max_head为预读的最 // 大量,这个将在struct file(filp->f_ramax)中用到。 if (raend < end_index) max_ahead = filp->f_ramax; // 设置预读仅在当前页面发生,有效地停止预读。 filp->f_rawin = 0; filp->f_ralen = 1; if (!max_ahead) { filp->f_raend = index + filp->f_ralen; filp->f_rawin += filp->f_ralen; } } } /* * The current page is not locked. * If we were reading ahead and, * if the current max read ahead size is not zero and, * if the current position is inside the last read-ahead IO request, * it is the moment to try to read ahead asynchronously. * We will later force unplug device in order to force asynchronous read IO. */ // 这是在代码注释方面很少见的情形:使代码尽可能得清晰。基本上,它的意思是说如果当 // 前页面没有因为I/O而上锁,则稍微地扩大预读窗口 ,然后记住预读进展顺利。 else if (reada_ok && filp->f_ramax && raend >= 1 && index <= raend && index + filp->f_ralen >= raend) { /* * Add ONE page to max_ahead in order to try to have about the same IO max size * as synchronous read-ahead (MAX_READAHEAD + 1)*PAGE_CACHE_SIZE. * Compute the position of the last page we have tried to read in order to * begin to read ahead just at the next page. */ raend -= 1; if (raend < end_index) max_ahead = filp->f_ramax + 1; if (max_ahead) { filp->f_rawin = filp->f_ralen; filp->f_ralen = 0; reada_ok = 2; } } /* * Try to read ahead pages. * We hope that ll_rw_blk() plug/unplug, coalescence, requests sort and the * scheduler, will work enough for us to avoid too bad actuals IO requests. */ // 这一块通过对先前读窗口中的每个页面调用 page_cache_read() 进行实际的预读。注意 // 这里ahead是如何在每个预读页面上增加的。 ahead = 0; while (ahead < max_ahead) { ahead ++; if ((raend + ahead) >= end_index) break; if (page_cache_read(filp, raend + ahead) < 0) break; } /* * If we tried to read ahead some pages, * If we tried to read ahead asynchronously, * Try to force unplug of the device in order to start an asynchronous * read IO request. * Update the read-ahead context. * Store the length of the current read-ahead window. * Double the current max read ahead size. * That heuristic avoid to do some large IO for files that are not really * accessed sequentially. */ // 如果预读成功,则这里更新在struct file中的预读字段来标识进度。基本上是预读上下 // 文在增长,但是如果发现预读效率低下,可以由do_generic_file_readahead()重置。 if (ahead) { // 更新在这一遍中预读的页面数 f_ralen 。 filp->f_ralen += ahead; // 更新预读窗口的大小。 filp->f_rawin += filp->f_ralen; // 标记预读的末端。 filp->f_raend = raend + ahead + 1; // 将当前的预读最大大小扩大一倍。 filp->f_ramax += filp->f_ramax; // 不使预读的最大大小超过块设备中的最大预读大小。 if (filp->f_ramax > max_readahead) filp->f_ramax = max_readahead; #ifdef PROFILE_READAHEAD profile_readahead((reada_ok == 2), filp); #endif } return; }
(2)一般文件 mmap
(a)generic_file_mmap
这是由许多 struct files 用作 struct file_operations 的一般 mmap() 函数。它主要负责存在合适的 address_space 函数,并设置 VMA 的使用操作。
// mm/filemap.c /* This is used for a general mmap of a disk file */ int generic_file_mmap(struct file * file, struct vm_area_struct * vma) { // 获取管理被映射文件的 address_space。 struct address_space *mapping = file->f_dentry->d_inode->i_mapping; // 获取该 address_space 的 struct inode。 struct inode *inode = mapping->host; // 如果VMA可以共享和可写,则这里保证存在一个a_ops->writepage()函数。 // 如果没有,则返回-EINVAL。 if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) { if (!mapping->a_ops->writepage) return -EINVAL; } // 保证存在一个 a_ops->readpage() 函数。 if (!mapping->a_ops->readpage) return -ENOEXEC; // 更新inode的访问次数。 UPDATE_ATIME(inode); // 使用generic_file_vm_ops进行文件操作。在 mm/filemap.c 中定义的一般 VM 操 // 作结构,仅提供filemap_nopage() (见D. 6.4. 1)作为它的nopage()函数。这里没有定义其他 // 的回调函数。 vma->vm_ops = &generic_file_vm_ops; return 0; } static struct vm_operations_struct generic_file_vm_ops = { nopage: filemap_nopage, };
(3)一般文件截断
这一节有关截断文件的路径。实际的系统调用 truncate() 由 fs/open.c 中的 sys_truncate() 来实现。在 VM 中的高层函数( vmtruncate() )调用时,文件的目录项信息已经被更新,而且还获得了 inode 的信号量。
(a)vmtruncate
这是一个负责截断文件的 VM 高层函数。当它完成时,所有映射页面的页表项都被截断,解除映射并在可能时回收。
// mm/memory.c /* * Handle all mappings that got truncated by a "truncate()" * system call. * * NOTE! We have to be ready to update the memory sharing * between the file and the memory map for a potential last * incomplete page. Ugly, but necessary. */ // 传入的参数是被截断的 inode, 标记文件新末端的offset。文件以前的长度存放在 // inode->i_size。 int vmtruncate(struct inode * inode, loff_t offset) { unsigned long pgoff; // 获取 inode 的 address_space。 struct address_space *mapping = inode->i_mapping; unsigned long limit; // 如果新文件大小大于旧的大小,则跳转到do_expand,在扩大文件前进行进 // 程极限检查。 if (inode->i_size < offset) goto do_expand; // 在这里收缩文件,所以更新inode->i_size来匹配这种收缩。 inode->i_size = offset; // 上锁自旋锁,保护使用该inode的两个VMA链表。 spin_lock(&mapping->i_shared_lock); // 如果没有VMA映射到该inode,则跳转到out_unlock。在那里文件使用的 // 页面将被 truncate_inode_pages() (D. 6. 3. 6)回收。 if (!mapping->i_mmap && !mapping->i_mmap_shared) goto out_unlock; // 计算作为文件偏移的pgoff,它是截断开始的页面。 pgoff = (offset + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; // 调用vmtruncate_list()(见D. 6. 3. 2)从所有私有映射中截断页面。 if (mapping->i_mmap != NULL) vmtruncate_list(mapping->i_mmap, pgoff); // 从所有的共享映射中截断页面。 if (mapping->i_mmap_shared != NULL) vmtruncate_list(mapping->i_mmap_shared, pgoff); out_unlock: // 解锁保护VMA链表的自旋锁。 spin_unlock(&mapping->i_shared_lock); // 如果在文件的页面高速缓存中存在页面,则调用truncate_inode_pages() 回收页面。 truncate_inode_pages(mapping, offset); // 跳转到out_truncate来调用特定文件系统的truncate()函数,这样磁盘上使用的块 // 将被释放。 goto out_truncate; // 如果扩展了文件,这里保证将不会超过最大文件大小的进程限制,而且宿主 // 文件系统可以支持这种新的文件大小。 do_expand: limit = current->rlim[RLIMIT_FSIZE].rlim_cur; if (limit != RLIM_INFINITY && offset > limit) goto out_sig; if (offset > inode->i_sb->s_maxbytes) goto out; // 如果符合限制,则更新inode大小,并回退来调用特定文件系统的截断函数,用 0 填 // 充扩展的文件大小。 inode->i_size = offset; out_truncate: // 如果文件系统提供一个truncate()函数,则上锁内核,调用它,并再次释放内 // 核锁。没有获取合适锁的文件系统将在文件截断和文件扩展之间阻止这种竞争,因为在写或 // 陷入前必须需要一个大内核锁。 if (inode->i_op && inode->i_op->truncate) { lock_kernel(); inode->i_op->truncate(inode); unlock_kernel(); } // 返回成功。 return 0; out_sig: // 如果文件大小变得过大,则给调用进程发送SIGXFSZ信号并能够返回-EFBIG send_sig(SIGXFSZ, current, 0); out: return -EFBIG; }
(b)vmtruncate_list
这个函数遍历在一个 address_spaces 链表中的所有 VMA ,并在映射正被截断文件的地址内调用 zap_page_range() 。
// mm/memory.c static void vmtruncate_list(struct vm_area_struct *mpnt, unsigned long pgoff) { // 遍历链表中的所有VMA。 do { // 获取该VMA持有的mm_struct。 struct mm_struct *mm = mpnt->vm_mm; // 计算VMA起点、终点和长度。 unsigned long start = mpnt->vm_start; unsigned long end = mpnt->vm_end; unsigned long len = end - start; unsigned long diff; /* mapping wholly truncated? */ // 如果截断了整个VMA.这里调用函数zap_page_range()(见D 6. 3. 3),以整 // 个VMA的起点和长度为参数。 if (mpnt->vm_pgoff >= pgoff) { zap_page_range(mm, start, len); continue; } /* mapping wholly unaffected? */ // 计算以页面为单位的VMA长度。 len = len >> PAGE_SHIFT; // 检查VMA映射的任何区域是否可以被截断。如果这个VMA不受影响则转到下一个VMA。 diff = pgoff - mpnt->vm_pgoff; if (diff >= len) continue; /* Ok, partially affected.. */ // 如果VMA只是部分被截断,则计算要截断区域的起点和以页面为单位的长度。 start += diff << PAGE_SHIFT; len = (len - diff) << PAGE_SHIFT; // 调用zap_page_range() (见D. 6. 3. 3)来取消受影响区域的映射。 zap_page_range(mm, start, len); } while ((mpnt = mpnt->vm_next_share) != NULL); }
(c)zap_page_range
这个函数是遍历页表的顶层函数,它从一个 mm_struct 中取消对特定区域中用户空间的映射。
// mm/memory.c /* * remove user pages in a given range. */ void zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size) { mmu_gather_t *tlb; pgd_t * dir; // 计算销毁的start和end地址。 unsigned long start = address, end = address + size; int freed = 0; // 计算包含起始address的PGD(dir) 。 dir = pgd_offset(mm, address); /* * This is a long-lived spinlock. That's fine. * There's no contention, because the page table * lock only protects against kswapd anyway, and * even if kswapd happened to be looking at this * process we _want_ it to get stuck. */ // 保证起始地址不在末尾地址之后。 if (address >= end) BUG(); // 获取保护页表的自旋锁。这是一个长生命期的锁,一般认为这不是一个好方法,但是 // 在这块前面的注释解释了在这种情形下的可行性。 spin_lock(&mm->page_table_lock); // 清理这片区域的CPU高速缓存。 flush_cache_range(mm, address, end); // tlb_gather_mmu() 记录了正在被改变的MM。最后将调用tlb_remove_page()来取 // 消PTE映射,并将PTE存放在一个struct free_pte_ctx中直到销毁过程结束。这里是为了避 // 免必须不断地因为释放PTE而要刷新TLB。 tlb = tlb_gather_mmu(mm); // 对由于销毁而受影响的每个PMD,这里调用zap_pmd_range()直到到达末端地址。注意这里也传入了 // tlb参数,在后面tlb_remove_page()将用到。 do { freed += zap_pmd_range(tlb, dir, address, end - address); address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; } while (address && (address < end)); /* this will flush any remaining tlb entries */ // tlb_finish_mmu() 释放所有由 tlb_remove_page()解除映射的 PTE,然后刷新 TLB。 // 这样刷新是为了避免TLB刷新风暴,否则需要解除映射的每个PTE。 tlb_finish_mmu(tlb, start, end); /* * Update rss for the mm_struct (not necessarily current->mm) * Notice that rss is an unsigned long. */ // 更新RSS计数。 if (mm->rss > freed) mm->rss -= freed; else mm->rss = 0; // 释放页表锁。 spin_unlock(&mm->page_table_lock); }
(d)zap_pmd_range
这个函数没有加注释。它遍历在请求范围内受影响的 PMD,然后在每个前面调用 zap_pte_range() 。
// mm/memory.c static inline int zap_pmd_range(mmu_gather_t *tlb, pgd_t * dir, unsigned long address, unsigned long size) { pmd_t * pmd; unsigned long end; int freed; // 如果不存在PGD,这里返回 if (pgd_none(*dir)) return 0; // 如果PGD损坏,则表明是错误并返回。 if (pgd_bad(*dir)) { pgd_ERROR(*dir); pgd_clear(dir); return 0; } // 获取起始pmd。 pmd = pmd_offset(dir, address); // 计算销毁的end地址。如果它已经超过了这个PGD的末端,则设置end为PGD的末端。 end = address + size; if (end > ((address + PGDIR_SIZE) & PGDIR_MASK)) end = ((address + PGDIR_SIZE) & PGDIR_MASK); freed = 0; // 遍历该PGD中所有PMD,对每个PMD,这里调用zap_pte_range()(见 D. 6. 3. 5)来取消对PTE的映射。 do { freed += zap_pte_range(tlb, pmd, address, end - address); address = (address + PMD_SIZE) & PMD_MASK; pmd++; } while (address < end); // 返回释放的页面数。 return freed; }
(e)zap_pte_range
这个函数对请求地址范围中的 pmd 中每个 PTE,调用 tlb_remove_page() 。
// mm/memory.c static inline int zap_pte_range(mmu_gather_t *tlb, pmd_t * pmd, unsigned long address, unsigned long size) { unsigned long offset; pte_t * ptep; int freed = 0; // 如果不存在PGD,这里返回。 if (pmd_none(*pmd)) return 0; // 如果PGD损坏,则表明是错误并返回。 if (pmd_bad(*pmd)) { pmd_ERROR(*pmd); pmd_clear(pmd); return 0; } // 获取起始PTE偏移。 ptep = pte_offset(pmd, address); // 将偏移对齐于PMD边界。 offset = address & ~PMD_MASK; // 如果要解除映射的区域大小是以前的PMD边界,在这里指定大小,这样仅这个PMD // 会受到影响。 if (offset + size > PMD_SIZE) size = PMD_SIZE - offset; // 将size对齐于页面边界。 size &= PAGE_MASK; // 遍历区域中所有PTE。 for (offset=0; offset < size; ptep++, offset += PAGE_SIZE) { pte_t pte = *ptep; // 如果不存在PTE,这里继续下一个。 if (pte_none(pte)) continue; if (pte_present(pte)) { // 如果存在PTE,这里调用tlb_remove_page()来取消页面映射。如果页面可 // 回收,则增加freed计数。 struct page *page = pte_page(pte); if (VALID_PAGE(page) && !PageReserved(page)) freed ++; /* This will eventually call __free_pte on the pte. */ tlb_remove_page(tlb, ptep, address + offset); } else { // 如果PTE正在使用中,而页面被换出或者在交换高速缓存中,这里利用free_swap_and_cache() // (见K. 3. 2. 3)释放交换槽和页面。如果在高速缓存中的页面没有在这里计 // 数,它可能被回收,但这不是最重要的。 free_swap_and_cache(pte_to_swp_entry(pte)); pte_clear(ptep); } } // 返回释放的页面数。 return freed; }
(f)truncate_inode_pages
这是一个负责截断所有来自 mapping 中 Istart 之后出现的页面高速缓存中所有页面的高层函数。
// mm/filemap.c /** * truncate_inode_pages - truncate *all* the pages from an offset * @mapping: mapping to truncate * @lstart: offset from with to truncate * * Truncate the page cache at a set offset, removing the pages * that are beyond that offset (and zeroing out partial pages). * If any page is locked we wait for it to become unlocked. */ void truncate_inode_pages(struct address_space * mapping, loff_t lstart) { // 计算作为页面索引的截断的start位置。 unsigned long start = (lstart + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; // 如果是部分截断则作为计算最后一页中偏移的partial. unsigned partial = lstart & (PAGE_CACHE_SIZE - 1); int unlocked; // 上锁页面高速缓存。 spin_lock(&pagecache_lock); // 这里一直循环直到不再调用truncate_list_pages(),它的返回表明找到的任何页都已经回收。 do { // 使用truncate_list_pages() (见D. 6. 3. 7)截断在clean_pages链表中的所有页面。 unlocked = truncate_list_pages(&mapping->clean_pages, start, &partial); // 相似地,截断在dirty_pages链表中的页面。 unlocked |= truncate_list_pages(&mapping->dirty_pages, start, &partial); // 相似地,截断在locked_pages链表中的页面。 unlocked |= truncate_list_pages(&mapping->locked_pages, start, &partial); } while (unlocked); /* Traversed all three lists without dropping the lock */ // 解锁页面高速缓存。 spin_unlock(&pagecache_lock); }
(g)truncate_list_pages
这个函数找到请求链表(head),它是 address_space 的一部分。如果在 start 之后找到页面,它们都将被截断。
// mm/filemap.c static int truncate_list_pages(struct list_head *head, unsigned long start, unsigned *partial) { struct list_head *curr; struct page * page; int unlocked = 0; restart: // 记录链表的开始并循环,直到扫描了整个链表。 curr = head->prev; while (curr != head) { unsigned long offset; // 获取该表项的页面,以及它所表示的文件中的offset。 page = list_entry(curr, struct page, list); offset = page->index; /* Is one of the pages to truncate? */ // 如果当前的页面是在start之后,或者是部分截断的页面,则截断该页面并移到下一个页面。 if ((offset >= start) || (*partial && (offset + 1) == start)) { int failed; // 引用页面并试着对它上锁。 page_cache_get(page); failed = TryLockPage(page); // 从链表中移除页面。 list_del(head); // 如果我们上锁页面,则这里将其添加回链表,在那里它将在下次循环时被跳过。 if (!failed) /* Restart after this page */ list_add_tail(head, curr); else // 如果不是这样,则这里将其加回去,这样它就可以被立即发现。在这个函数后 // 面,将调用wait_on_page()直到页面解锁。 /* Restart on this page */ list_add(head, curr); // 释放页面高速缓存锁。 spin_unlock(&pagecache_lock); // 设置上锁为1表明已经找到了截断错误的该页。这里将强制使用truncate_inode_pages。 // 再次调用这个函数以保证后面不再有页面。这里看似有点多余,实际上是想在找到一 // 个上锁页后重新调用这个函数。但是它以这种实现的方式表明无论页面是否上锁它都会被调用。 unlocked = 1; // 如果我们对页面加了锁,这里截断它。 if (!failed) { // 如果页面是部分地被截断,这里调用truncate_partial_page()(见D. 6. 3. 10), // 以截断开始页面中的偏移为参数。 if (*partial && (offset + 1) == start) { truncate_partial_page(page, *partial); *partial = 0; } else // 如果不是部分截断,则这里调用truncate_complete_page()(见D. 6. 3. 8)来截断整个 // 页面。 truncate_complete_page(page); // 解锁页面。 UnlockPage(page); } else // 如果页面上锁失败,这里调用wait_on_page()等待直到页面可以上锁。 wait_on_page(page); // 释放对页面的引用。如果没有对页面的引用,则这里回收该页面。 page_cache_release(page); // 检查进程是否应该在继续前调用schedule()。这是为了避免在运行CPU时中 // 断一个截断进程。 if (current->need_resched) { __set_current_state(TASK_RUNNING); schedule(); } // 重新获得自旋锁并重新扫描页面以回收。 spin_lock(&pagecache_lock); goto restart; } // 当前的页面应该不能回收,所以这里移到下一个页面。 curr = curr->prev; } // 如果在列表中找到一个待截断的页面,则这里返回1。 return unlocked; }
(h)truncate_complete_page
这个函数截断一整个页面,释放相关的资源并回收页面。
// mm/filemap.c static void truncate_complete_page(struct page *page) { /* Leave it on the LRU if it gets converted into anonymous buffers */ // 如果页面拥有缓冲,则这里调用do_flushpage()(见D. 6. 3. 9)来清理页面相关的所有 // 缓冲。下一行注释清楚地描述了这个问题。 if (!page->buffers || do_flushpage(page, 0)) // 从LRU中删除页面。 lru_cache_del(page); /* * We remove the page from the page cache _after_ we have * destroyed all buffer-cache references to it. Otherwise some * other process might think this inode page is not in the * page cache and creates a buffer-cache alias to it causing * all sorts of fun problems ... */ // 清除脏的和更新页面的标志。 ClearPageDirty(page); ClearPageUptodate(page); // 调用 remove_inode_page()(见J. 1. 2. 1)来从页面高速缓存中删除页面。 remove_inode_page(page); // 撤销页面引用。在truncate_list_pages()释放它的私有引用之后,该页将在后面被回收。 page_cache_release(page); }
(i)do_flushpage
这个函数负责清理页面相关的所有缓冲。
// mm/filemap.c static int do_flushpage(struct page *page, unsigned long offset) { int (*flushpage) (struct page *, unsigned long); // 如果page->mapping提供flushpage()函数,这里调用它。 flushpage = page->mapping->a_ops->flushpage; if (flushpage) return (*flushpage)(page, offset); // 如果没有,则这里调用block_flushpage(),它是一个清理页面相关缓冲的一般函数。 return block_flushpage(page, offset); }
(j)truncate_partial_page
这个函数利用不再使用的高位字节清 0 来部分截断一个页面,然后清理所有的相关缓冲。
// mm/filemap.c static inline void truncate_partial_page(struct page *page, unsigned partial) { // memclear_highpage_flush() 利用0填充地址范围。在这种情形下,它将从partial到 // 页面末端清0。 memclear_highpage_flush(page, partial, PAGE_CACHE_SIZE-partial); // 如果页面拥有任何的相关缓冲,则这里清理包含在截断后区域的数据缓冲。 if (page->buffers) do_flushpage(page, partial); }
(4)从页面高速缓存中读入页面
(a)filemap_nopage
这是许多 VMA 使用的一般 nopage() 函数。这里利用大量的 goto 来循环,所以踉踪起来比较困难,但在这里并没有什么新颖的地方。它主要负责从页面高速缓存中提取错页面或者从磁盘中读入错页面。如果需要,它还可以进行预读。
// mm/filemap.c /* * filemap_nopage() is invoked via the vma operations vector for a * mapped memory region to read in file data during a page fault. * * The goto's are kind of ugly, but this streamlines the normal case of having * it in the page cache, and handles the special cases reasonably without * having a lot of duplicated code. */ struct page * filemap_nopage(struct vm_area_struct * area, unsigned long address, int unused) { // 这一块获取struct file,address_space和inode,这对缺页中断很重要。然后获取陷入需 // 要的文件中起始偏移,以及该VMA末端相应的偏移。偏移是VMA的末端,而不是在预读文 // 件时的页面末端。 int error; // 获取陷入所需的 struct file,address_space 和 inode。 struct file *file = area->vm_file; struct address_space *mapping = file->f_dentry->d_inode->i_mapping; struct inode *inode = mapping->host; struct page *page, **hash; unsigned long size, pgoff, endoff; // 计算pgoff,它是对应于陷入起点的文件中的偏移口 pgoff = ((address - area->vm_start) >> PAGE_CACHE_SHIFT) + area->vm_pgoff; // 计算对应于VMA末端的文件中的偏移口 endoff = ((area->vm_end - area->vm_start) >> PAGE_CACHE_SHIFT) + area->vm_pgoff; retry_all: /* * An external ptracer can access pages that normally aren't * accessible.. */ // 计算文件的大小(以页数计) size = (inode->i_size + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT; // 如果陷入pgoff已经超出了文件末端,则这里不是一个跟踪进程,所以这里返回 NULL。 if ((pgoff >= size) && (area->vm_mm == current->mm)) return NULL; /* The "size" of the file, as far as mmap is concerned, isn't bigger than the mapping */ // 如果VMA映射超出了文件末端,这里设置文件大小为映射的大小。 if (size > endoff) size = endoff; /* * Do we have something in the page cache already? */ // 在页面高速缓存中查找该页。 hash = page_hash(mapping, pgoff); retry_find: page = __find_get_page(mapping, pgoff, hash); // 如果不存在,则跳转到no_cached_page,在那里调用page_cache_read()从后 // 援存储器中读入该页。 if (!page) goto no_cached_page; /* * Ok, found a page in the page cache, now we need to check * that it's up-to-date. */ // 如果页面不是最新,则跳转到page_not_uptodate,在那里页面或者被声明为 // 无效,或者更新页面中的数据。 if (!Page_Uptodate(page)) goto page_not_uptodate; success: /* * Try read-ahead for sequential areas. */ // 如果 VM_SEQ一READ 指明 了映射,则利用 nopage_sequential_readahead() // 对当前陷人的页面进行预陷入。 if (VM_SequentialReadHint(area)) nopage_sequential_readahead(area, pgoff, size); /* * Found the page and have a reference on it, need to check sharing * and possibly copy it over to another page.. */ // 将陷入页面标记为已访问,该页面将被移到active_list。 mark_page_accessed(page); // 因为页面将被添加到进程页表中,所以这里调用flush_page_to_ram(),这样内存对 // 页面的最近存储将肯定可以为用户空间所见。 flush_page_to_ram(page); // 返回陷入页面。 return page; no_cached_page: /* * If the requested offset is within our file, try to read a whole * cluster of pages at once. * * Otherwise, we're off the end of a privately mapped file, * so we need to map a zero page. */ // 如果还没有到达文件的末端以及还未指明随机读,则这里调用read_cluster_nonblocking() // 在这个陷入页面附近的几个页面中进行预陷入。 if ((pgoff < size) && !VM_RandomReadHint(area)) error = read_cluster_nonblocking(file, pgoff, size); else // 如果没有,且文件是被随机访问的,则这里调用page_cache_read() (见D. 6. 4. 2)来 // 读入刚陷入的页面。 error = page_cache_read(file, pgoff); /* * The page we want has now been added to the page cache. * In the unlikely event that someone removed it in the * meantime, we'll just come back here and read it again. */ // 如果没有错误发生,则转到1958行的retry_find,在那里检查以保证返回前 // 页面在页面高速缓存中。 if (error >= 0) goto retry_find; /* * An error return from page_cache_read can result if the * system is low on memory, or a problem occurs while trying * to schedule I/O. */ // 如果由于内存溢出发生错误,这里返回,异常处理程序将相应进行处理。 if (error == -ENOMEM) return NOPAGE_OOM; // 如果不是这个错误,这里返回NULL表明陷入一个不存在的页面,发送SIGBUS给 // 陷入页面。 return NULL; // 在这一块,找到的页面不是最新的,所以检查页面是否被更新过。如果通过,则调用合适 // 的readpage()来重新同步页面。 page_not_uptodate: // 为I/O锁定页面。 lock_page(page); /* Did it get unhashed while we waited for it? */ // 如果页面从映射中移除(可能是因为文件截断),现在处于匿名状态,则这里 // 转到retry_all,在那里试着再次陷人页面。 if (!page->mapping) { UnlockPage(page); page_cache_release(page); goto retry_all; } /* Did somebody else get it up-to-date? */ // 再次检查Uptodate标志以防我们为I/O而锁定页面时页面已经改变。 if (Page_Uptodate(page)) { UnlockPage(page); goto success; } // 调用函数address_space->readpage()来从磁盘调度数据读。 if (!mapping->a_ops->readpage(file, page)) { // 等待I/O完成,如果现在是最新的,则转到success返回页面。如果函数readpage() // 调用失败,则后退到错误恢复路径。 wait_on_page(page); if (Page_Uptodate(page)) goto success; } // 在这条路径上,由于I/O错误,页面不是最新的。这里第2次试着读页面数据,如果失败则返回。 /* * Umm, take care of errors if the page isn't up-to-date. * Try to re-read it _once_. We do this synchronously, * because there really aren't any performance issues here * and we need to check for errors. */ lock_page(page); /* Somebody truncated the page on us? */ // 这与前一块几乎相同。惟一的区别是调用ClearPageError()来清除由前面 // I/O 引起的错误。 if (!page->mapping) { UnlockPage(page); page_cache_release(page); goto retry_all; } /* Somebody else successfully read it in? */ if (Page_Uptodate(page)) { UnlockPage(page); goto success; } ClearPageError(page); if (!mapping->a_ops->readpage(file, page)) { wait_on_page(page); if (Page_Uptodate(page)) goto success; } /* * Things didn't work out. Return zero to tell the * mm layer so, possibly freeing the page cache page first. */ // 如果还是失败,则这里撤销对页面的引用,因为该页面已经没有用了。 page_cache_release(page); // 因为陷入失败所以返回NULL return NULL; }
① ⟺ __find_get_page
// mm/filemap.c /* * a rather lightweight function, finding and getting a reference to a * hashed page atomically. */ struct page * __find_get_page(struct address_space *mapping, unsigned long offset, struct page **hash) { struct page *page; /* * We scan the hash list read-only. Addition to and removal from * the hash-list needs a held write-lock. */ spin_lock(&pagecache_lock); page = __find_page_nolock(mapping, offset, *hash); if (page) page_cache_get(page); spin_unlock(&pagecache_lock); return page; }
② ⟺ __find_page_nolock
// mm/filemap.c static inline struct page * __find_page_nolock(struct address_space *mapping, unsigned long offset, struct page *page) { goto inside; for (;;) { page = page->next_hash; inside: if (!page) goto not_found; if (page->mapping != mapping) continue; if (page->index == offset) break; } not_found: return page; }
③ ⇒ nopage_sequential_readahead
nopage_sequential_readahead 函数
④ ⇒ read_cluster_nonblocking
(b)age_cache_read
如果页面不在页面高速缓存中,则这个函数将与 file 中 offset 相关的页面加入到页面高速缓存中。
// mm/filemap.c static int page_cache_read(struct file * file, unsigned long offset) { // 获取管理文件映射的address_space。 struct address_space *mapping = file->f_dentry->d_inode->i_mapping; // 页面高速缓存是一个哈希表,page_hash()返回的是相应mapping和offset桶中第1个页面。 struct page **hash = page_hash(mapping, offset); struct page *page; // 利用__find_page_nolock()(见J. L4.3)来搜索页面高速缓存。这里基本上是 // 从hash开始遍历链表以确定是否可以找到请求页。 spin_lock(&pagecache_lock); page = __find_page_nolock(mapping, offset, *hash); spin_unlock(&pagecache_lock); // 如果页面已经在页面高速缓存中,则这里返回。 if (page) return 0; // 分配一页并将其插入到页面高速缓存中,page_cache_alloc()使用mapping中含的 // GFP信息从伙伴分配器中分配一个页面。 page = page_cache_alloc(mapping); if (!page) return -ENOMEM; // 利用add_to_page_cache_unique() (见J.L L 2)将页面插入到页面高速缓存中。使用 // 这个函数是因为需要再次检查,以确定在没有获得pagecache_lock自旋锁时页面没有被插入 // 到页面高速缓存中。 if (!add_to_page_cache_unique(page, mapping, offset, hash)) { // 如果分配的页面没有插入到页面高速缓存中,则需要用数据填充。所以调用该 // mapping 的readpage() 函数。这里发生I/O调度,在I/O完成后将解锁页面。 int error = mapping->a_ops->readpage(file, page); page_cache_release(page); return error; } /* * We arrive here in the unlikely event that someone * raced with us and added our page to the cache first. */ // 如果其他进程向页面高速缓存加入页面,则在这里由page_cache_release()释放,因 // 为现在没有该页面的用户。 page_cache_release(page); return 0; }
(5)为 nopage() 进行预读文件
(a)nopage_sequential_readahead
这个函数仅在 VMA 中已经指定了 VM_SEQ_READ 标志时才由 filemap_nopage() 调用。当半个当前预读窗口已经陷入时,将为 I/O 调度下一个预读窗口,前面一个窗口中的页面将被释放。
// mm/filemap.c /* * Read-ahead and flush behind for MADV_SEQUENTIAL areas. Since we are * sure this is sequential access, we don't need a flexible read-ahead * window size -- we can always use a large fixed size window. */ static void nopage_sequential_readahead(struct vm_area_struct * vma, unsigned long pgoff, unsigned long filesize) { unsigned long ra_window; // get_max_readahead()将为在特定inode中驻留的设备块返回先前读窗口的最大大小。 ra_window = get_max_readahead(vma->vm_file->f_dentry->d_inode); // CLUSTER_PAGES是批量换入换出的页面数。宏CLUSTER_OFFSET()将预读 // 窗口与集边界对齐。 ra_window = CLUSTER_OFFSET(ra_window + CLUSTER_PAGES - 1); /* vm_raend is zero if we haven't read ahead in this area yet. */ // 如果还没有发生预读.这里设置预读窗口 ( vm_raend )。 if (vma->vm_raend == 0) vma->vm_raend = vma->vm_pgoff + ra_window; /* * If we've just faulted the page half-way through our window, * then schedule reads for the next window, and release the * pages in the previous window. */ // 如果在预读窗口的过程中发生异常,这里调度下一个预读窗口从磁盘读入数据。并 // 释放当前窗口的前半部分页面, 为不再需要它们。 if ((pgoff + (ra_window >> 1)) == vma->vm_raend) { // 计算下一个预读窗口的start和end,因为我们要利用它们开始I/O。 unsigned long start = vma->vm_pgoff + vma->vm_raend; unsigned long end = start + ra_window; // 如果预读窗口的末端在VMA的末端之后,这里设置end为VMA的末端。 if (end > ((vma->vm_end >> PAGE_SHIFT) + vma->vm_pgoff)) end = (vma->vm_end >> PAGE_SHIFT) + vma->vm_pgoff; // 如果我们在映射的末端,这里仅返回,因为不再需要进行预读。 if (start > end) return; // 调用 read_cluster_nonblocking() (见 D. 6. 5. 2)调度下一个预读窗口 // 读入页面。 while ((start < end) && (start < filesize)) { if (read_cluster_nonblocking(vma->vm_file, start, filesize) < 0) break; start += CLUSTER_PAGES; } // 调用 run_task_queue() I/O。 run_task_queue(&tq_disk); /* if we're far enough past the beginning of this area, recycle pages that are in the previous window. */ // 利用 filemap_sync() 来处理以前预读窗口的页面,因为不再需要它们了。 if (vma->vm_raend > (vma->vm_pgoff + ra_window + ra_window)) { unsigned long window = ra_window << PAGE_SHIFT; end = vma->vm_start + (vma->vm_raend << PAGE_SHIFT); end -= window + window; filemap_sync(vma, end - window, window, MS_INVALIDATE); } // 更新预读窗口的末端地址。 vma->vm_raend += ra_window; } return; }
(b)read_cluster_nonblocking
这个函数调度下一个读入页面的预读窗口。
// mm/filemap.c static int read_cluster_nonblocking(struct file * file, unsigned long offset, unsigned long filesize) { // CLUSTER_PAGES在低端内存系统中是四个页面,在高端内存系统中则是八个页 // 面。这意味着,在有充分内存的x86系统中,在一块中读入32 KB。 unsigned long pages = CLUSTER_PAGES; // CLUSTER_OFFSET()将偏移对齐于块大小边界。 offset = CLUSTER_OFFSET(offset); // 对块中的每一页,调用page_cache_read()(见D. 6. 4. 2)将整块读入页面高速缓存。 while ((pages-- > 0) && (offset < filesize)) { int error = page_cache_read(file, offset); // 如果在预读时发生错误,则这里返回错误。 if (error < 0) return error; offset ++; } // 返回成功。 return 0; }
(6)交换相关的预读
(a)swapin_readahead
这个函数在当前表项陷入一系列的页面。它在 CLUSTER_PAGES 个页面换入后或者找到一个未使用的交换表项后停止。
// mm/memory.c /* * Primitive swap readahead code. We simply read an aligned block of * (1 << page_cluster) entries in the swap area. This method is chosen * because it doesn't cost us any seek time. We also make sure to queue * the 'original' request together with the readahead ones... */ void swapin_readahead(swp_entry_t entry) { int i, num; struct page *new_page; unsigned long offset; /* * Get the number of handles we should do readahead io to. */ // valid_swaphandles()是决定页面换入数目的函数。它在到达第1个空表项或者 // CLUSTER_PAGES 时停止。 num = valid_swaphandles(entry, &offset); // 换入页面。 for (i = 0; i < num; offset++, i++) { /* Ok, do the async read-ahead now */ // 试着利用read_swap_cache_async()(见K. 3. 1. 1)将页面换入到交换高速缓存。 new_page = read_swap_cache_async(SWP_ENTRY(SWP_TYPE(entry), offset)); // 如果页面不能被换入,这里跳出并返回。 if (!new_page) break; // 撤销对发生read_swap_cache_async()页面的引用。 page_cache_release(new_page); } // 返回。 return; }
(b)valid_swaphandles
这个函数决定从交换区起点 offset 开始要预读多少页面。它将预读到下一个未使用的交换槽,但是至多返回 CLUSTER_PAGES 。
// mm/swapfile.c /* * swap_device_lock prevents swap_map being freed. Don't grab an extra * reference on the swaphandle, it doesn't matter if it becomes unused. */ int valid_swaphandles(swp_entry_t entry, unsigned long *offset) { // i设置为CLUSTER_PAGE,与这里的位移操作相对应。 int ret = 0, i = 1 << page_cluster; unsigned long toff; // 获取包含这个 entry 的 swap_info_struct。 struct swap_info_struct *swapdev = SWP_TYPE(entry) + swap_info; // 如果禁止了预读,这里返回。 if (!page_cluster) /* no readahead */ return 0; // 计算toff为entry向下取整到最近的CLUSTER_PAGES大小边界。 toff = (SWP_OFFSET(entry) >> page_cluster) << page_cluster; // 如果toff为0,则将其移到1,因为这是第1个包含交换区新信息的页面。 if (!toff) /* first page is swap header */ toff++, i--; *offset = toff; // 上锁我们将扫描的交换设备。 swap_device_lock(swapdev); // i 初始化为CLUSTER_PAGES,最多循环i次。 do { /* Don't read-ahead past the end of the swap area */ // 如果到达交换区的末端,这是能够进行预读最远的地方。 if (toff >= swapdev->max) break; /* Don't read in free or bad pages */ // 如果到达一个没有使用的表项,这里仅返回,因为它是我们将预读最远的 // 地方。 if (!swapdev->swap_map[toff]) break; // 相似地,如果发现了一个坏表项,这里也返回。 if (swapdev->swap_map[toff] == SWAP_MAP_BAD) break; // 移到下一个槽。 toff++; // 将预读页面数加1。 ret++; } while (--i); // 解锁交换设备。 swap_device_unlock(swapdev); // 返回应该预读的页面数。 return ret; }