linux 页表

原文网址:

http://blog.chinaunix.net/uid-26817832-id-3146395.html


 简单来说,讨论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、swapper_pg_dir : Global Page Directory (全局页目录,即最顶层页目录,PGD) 的地址
2、几个名词:页全局目录(PGD);页上级目录(PUD);页中级目录(PMD);页表(PTE)
3、mips 虚拟地址内存空间映射如下:
0x0000 0000 ~ 0x7fff ffff
用户级空间,2GB,要经MMU(TLB)地址翻译。kuseg。可以控制要不要经过缓冲。

0x8000 0000 ~ 0x9fff ffff
kseg0. 这块区域为操作系统内核所占的区域,共512M。使用时,不经过地址翻译,将最高位去掉就线性映射到内存的低512M(不足的就裁剪掉顶部)。但要经过缓冲区过渡。

0xa000 0000 ~ 0xbfff ffff
kseg1. 这块区域为系统初始化所占区域,共512M。使用时,不经过地址翻译,也不经过缓冲区。将最高3位去掉就线性映射到内存的低512M(不足的就裁剪掉顶部)。

0xc000 0000 ~ 0xffff ffff
kseg2. 这块区域也为内核级区域。要经过地址翻译。可以控制要不要经过缓冲。

核代码运行在0x8000000000 ~ x9fffffff的地址空间范围,共0.5G。对内核而言,显然它应该具备访问整个物理RAM的功能,这里即存在如下问题:
  • 内核应首先建立自己的页表,以便内核本身能寻址物理RAM(mips 处理器不存在这个问题,0x8000000000 ~ x9fffffff 转换到0x0000000000 ~ x1fffffff的物理空间是由处理器内部的地址译码部件自动完成的)
  • 内核本身的地址空间只有0.5G,对于RAM大于0.5G时,内核如何寻址?
4、关于第一个问题的进一步解释
在内核arch/mips/kernel/vmlinux.lds
OUTPUT_ARCH(mips)
ENTRY(kernel_entry)
PHDRS {
text PT_LOAD FLAGS(7);
note PT_NOTE FLAGS(4);
}
jiffies = jiffies_64;
SECTIONS
{
. = 0xffffffff80200000;

_text = .;
.text : {
. = ALIGN(8); *(.text.hot) *(.text) *(.ref.text) *(.devinit.text) *(.devexit.text) *(.text.unlikely)
. = ALIGN(8); __sched_text_start = .; *(.sched.text) __sched_text_end = .;
. = ALIGN(8); __lock_text_start = .; *(.spinlock.text) __lock_text_end = .;
. = ALIGN(8); __kprobes_text_start = .; *(.kprobes.text) __kprobes_text_end = .;

*(.text.*)
*(.fixup)
*(.gnu.warning)
....
这个文件最终会以参数 -Xlinker --script -Xlinker vmlinux.lds 的形式传给 gcc,并最终传给链接器 ld 来控制其行为。ld 会将 .text 节的地址链接到 0xFFFFFFFF80200000 处。而pmon会将内核elf文件移到物理地址0x00200000处,代码调用关键如下:
load命令 -> load_elf -> bootread

pc 寄存器中显示的是虚拟地址,cpu根据pc寄存器的值来取指,取值要访存,经过地址译码部件时,自动将0x8000000000 ~ x9fffffff 转换为0x0000000000 ~ x1fffffff(物理地址)。

5、第二个问题的处理

在前面的讨论中,一直存在一个为难题,即内核本身的地址空间只有0.5G(0x800000000-0x9fffffff),对于RAM大于0.5G时,内核如何寻址? 因此显然RAM等于0.5G是一个分界点;另外,对32位的机器而言,通产其线性地址空间为0~4G范围,当RAM大于4G时如何寻址?因此4G又是另一个分界点。

Linux的实现采用固定,永久映射+动态映射的方式来解决上述问题。
X86具体做法是,内核建立0~896MB的内核映射,即将0xc0000000-0xf0000000映射到物理内存前896MB;而对后128MB的线性地址空间(0xf000000~0xffffffff)采用动态映射方法,即根据需要映射到内存896MB以后的任意地址。具体实现机制请参考<<深入理解linux内核>>。

6、pagetable_init 主要完成固定映射和永久映射,将动态映射的页表无效,动态映射(一般是用户态的地址,进程相关的)如上面台运方所说,需要进一步看一下pagetable_init的代码,看看mips固定映射以及永久映射的具体实现。

7、线性地址空间被分成固定长度为单位的组,称为页,页内部连续的线性地址映射到连续的物理地址,这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。注意:页只是一个数据块,可以存放在任何页框或磁盘中。X86处理器中有一个分页单元完成线性地址到物理地址的解释,当然在启用分页单元之前,先由内核对页表进行适当的初始化。

8、每个活动的进程拥有一个页表集和全局页目录,必须分配给它一个全局页目录,不过没有必要为进程的所有页表分配RAM,只有当进程实际需要一个页表时才给该页表分配RAM会更又效率。

9、进程共享的是虚拟地址空间。

你可能感兴趣的:(linux 页表)