简单来说,讨论linux页表就是讨论linux进程的的页表:linux页表的创建与更新都包含于进程的创建与更新中。当前的linux内核采用的是写时复制方法,在创建一个linux进程时,完全复制父进程的页表,并且将父子进程的页表均置为写保护(即写地址的时候会产生缺页异常等)。那么父子进程谁向地址空间写数据时,产生缺页异常,分配新的页,并将两个页均置为可写,按照这种方式父子进程的地址空间渐渐变得不同。
按照上面的分析, 只需要讨论第一个进程页表初始化,进程创建时页表的拷贝,以及缺页异常时页表的更新即可。
1.init_task进程页表的初始化
init_task的地址空间是init_mm, init_mm在内核初始化的时候就赋值给了current->active_mm. init_mm的初始化页表是swapper_pg_dir,在mips架构中swapper_pg_dir初始化在函数pagetable_init中,初始化关系是
swapper_pg_dir -> invalide_pmd_table -> invalide_pte_table 或
swapper_pg_dir -> invalide_pte_table.
即在init_mm中,页表指向的全部是invalide_pte_table。
2.创建进程时页表的拷贝
进程创建一般调用的是do_fork函数,按照如下调用关系:
do_fork->copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range
找到copy_page_range函数,这个函数便是负责页表的拷贝,函数核心代码如下:
874 do {
875 next = pgd_addr_end(addr, end);
876 if (pgd_none_or_clear_bad(src_pgd))
877 continue;
878 if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
879 vma, addr, next))) {
880 ret = -ENOMEM;
881 break;
882 }
883 } while (dst_pgd++, src_pgd++, addr = next, addr != end);
copy_pud_range便是拷贝pud表,copy_pud_range调用copy_pmd_range, copy_pmd_range调用copy_pte_range,以此完成对三级页表的复制。需要注意的是在copy_pte_range调用的copy_one_pte中有如下代码:
694 if (is_cow_mapping(vm_flags)) {
695 ptep_set_wrprotect(src_mm, addr, src_pte);
696 pte = pte_wrprotect(pte);
697 }
这里便是判断如果采用的是写时复制,便将父子页均置为写保护,即会产生如下所示的缺页异常。
3.缺页异常时页表的更新
由页表的初始化可以看到,init_mm的页表全指向无效页表,然而普通的进程中不可能页表均指向无效项,因此肯定拥有一个不断扩充页表的机制,这个机制是通过缺页异常实现的。
以mips为例,mips的缺页异常最终会调用do_page_fault,do_page_fault调用handle_mm_fault,handle_mm_fault是公共代码,一般所有的缺页异常均会调用handle_mm_fault的核心代码如下:
3217 pud = pud_alloc(mm, pgd, address);
3218 if (!pud)
3219 return VM_FAULT_OOM;
3220 pmd = pmd_alloc(mm, pud, address);
3221 if (!pmd)
3222 return VM_FAULT_OOM;
3223 pte = pte_alloc_map(mm, pmd, address);
3224 if (!pte)
3225 return VM_FAULT_OOM;
其中pud_alloc代码如下:
1056 static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
1057 {
1058 return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
1059 NULL: pud_offset(pgd, address);
1060 }
其中pgd_none用于判断pgd是否为invalide,如果是可调用__pud_alloc,如果不是获得其地址继续查。
pmd_alloc函数和pte_alloc_map函数类似。
因此可以看出,在缺页异常中,会按照地址一次查三张页表,如果页表为invalide,比如invalide_pmd_table或invalide_pte_table,则会分配一个新的页表项取代invalide的页表项。这便是页表扩充的机制。
需要注意的是handle_mm_fault最终会调用handle_pte_fault,在handle_pte_fault函数中有如下代码:
3171 if (flags & FAULT_FLAG_WRITE) {
3172 if (!pte_write(entry))
3173 return do_wp_page(mm, vma, address,
3174 pte, pmd, ptl, entry);
3175 entry = pte_mkdirty(entry);
3176 }
即在缺页异常中如果遇到写保护会调用do_wp_page,这里面会处理上面所说的写时复制中父子进程区分的问题。
如上三个部分便是linux页表的大体处理框架
按照上面的分析, 只需要讨论第一个进程页表初始化,进程创建时页表的拷贝,以及缺页异常时页表的更新即可。
1.init_task进程页表的初始化
init_task的地址空间是init_mm, init_mm在内核初始化的时候就赋值给了current->active_mm. init_mm的初始化页表是swapper_pg_dir,在mips架构中swapper_pg_dir初始化在函数pagetable_init中,初始化关系是
swapper_pg_dir -> invalide_pmd_table -> invalide_pte_table 或
swapper_pg_dir -> invalide_pte_table.
即在init_mm中,页表指向的全部是invalide_pte_table。
2.创建进程时页表的拷贝
进程创建一般调用的是do_fork函数,按照如下调用关系:
do_fork->copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range
找到copy_page_range函数,这个函数便是负责页表的拷贝,函数核心代码如下:
874 do {
875 next = pgd_addr_end(addr, end);
876 if (pgd_none_or_clear_bad(src_pgd))
877 continue;
878 if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
879 vma, addr, next))) {
880 ret = -ENOMEM;
881 break;
882 }
883 } while (dst_pgd++, src_pgd++, addr = next, addr != end);
copy_pud_range便是拷贝pud表,copy_pud_range调用copy_pmd_range, copy_pmd_range调用copy_pte_range,以此完成对三级页表的复制。需要注意的是在copy_pte_range调用的copy_one_pte中有如下代码:
694 if (is_cow_mapping(vm_flags)) {
695 ptep_set_wrprotect(src_mm, addr, src_pte);
696 pte = pte_wrprotect(pte);
697 }
这里便是判断如果采用的是写时复制,便将父子页均置为写保护,即会产生如下所示的缺页异常。
3.缺页异常时页表的更新
由页表的初始化可以看到,init_mm的页表全指向无效页表,然而普通的进程中不可能页表均指向无效项,因此肯定拥有一个不断扩充页表的机制,这个机制是通过缺页异常实现的。
以mips为例,mips的缺页异常最终会调用do_page_fault,do_page_fault调用handle_mm_fault,handle_mm_fault是公共代码,一般所有的缺页异常均会调用handle_mm_fault的核心代码如下:
3217 pud = pud_alloc(mm, pgd, address);
3218 if (!pud)
3219 return VM_FAULT_OOM;
3220 pmd = pmd_alloc(mm, pud, address);
3221 if (!pmd)
3222 return VM_FAULT_OOM;
3223 pte = pte_alloc_map(mm, pmd, address);
3224 if (!pte)
3225 return VM_FAULT_OOM;
其中pud_alloc代码如下:
1056 static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
1057 {
1058 return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
1059 NULL: pud_offset(pgd, address);
1060 }
其中pgd_none用于判断pgd是否为invalide,如果是可调用__pud_alloc,如果不是获得其地址继续查。
pmd_alloc函数和pte_alloc_map函数类似。
因此可以看出,在缺页异常中,会按照地址一次查三张页表,如果页表为invalide,比如invalide_pmd_table或invalide_pte_table,则会分配一个新的页表项取代invalide的页表项。这便是页表扩充的机制。
需要注意的是handle_mm_fault最终会调用handle_pte_fault,在handle_pte_fault函数中有如下代码:
3171 if (flags & FAULT_FLAG_WRITE) {
3172 if (!pte_write(entry))
3173 return do_wp_page(mm, vma, address,
3174 pte, pmd, ptl, entry);
3175 entry = pte_mkdirty(entry);
3176 }
即在缺页异常中如果遇到写保护会调用do_wp_page,这里面会处理上面所说的写时复制中父子进程区分的问题。
如上三个部分便是linux页表的大体处理框架
三级页表结构示意图[zz]
图3.3 Linux的三级页表结构
图3.3 Linux的三级页表结构
Linux总是假定处理器有三级页表。每个页表通过所包含的下级页表的页面框号来访问。图3.3给出了虚拟地址是如何分割成多个域的,每个域提供了 某个指定页表的偏移。为了将虚拟地址转换成物理地址,处理器必须得到每个域的值。这个过程将持续三次直到对应于虚拟地址的物理页面框号被找到。最后再使用 虚拟地址中的最后一个域,得到了页面中数据的地址。
为了实现跨平台运行,Linux提供了一系列转换宏使得核心可以访问特定进程的页表。这样核心无需知道 页表入口的结构以及它们的排列方式。
这种策略相当成功,无论在具有三级页表结构的Alpha AXP还是两级页表的Intel X86处理器中,Linux总是使 用相同的页表操纵代码。