页表管理(Page table management)
Linux内核软件架构习惯与分成硬件相关层和硬件无关层。对于页表管理,2.6.10以前(包括2.6.10)在硬件无关层使用了3级页表目录管理的方式,它不管底层硬件是否实现的也是3级的页表管理:
Page Global Directory (PGD)
Page Middle Directory (PMD)
Page Table (PTE)
从2.6.11开始,为了配合64位CPU的体系结构,硬件无关层则使用了4级页表目录管理的方式:
Page Global Directory (PGD)
Page Upper Directory (PUD)
Page Middle Directory (PMD)
Page Table (PTE)
PGD每个条目中指向一个PUD,PUD的每个条目指向一个PMD,PMD的每个条目指向一个PTE,PTE的每个条目指向一个页面(Page)的物理首地址。因此一个线性地址被分为了5个部分,如下图:
PGD,PUD,PMD,PTE中到底有几个条目,不同的CPU体系结构有不同的定义。
虽然硬件无关层是这么设计的,但是底层硬件未必也是这样实现的。如x86体系结构,如果不使用PAE(Physical Address Extension)特性,则硬件底层实现的是2级的页表目录管理,事实上,只有PGD,PTE才是真正有意义的。
页目录(Page directory)
每个进程所代表的上下文数据结构中都有一个指针(mm_struct->pgd),其指向这个进程所使用的PGD的一个页(page frame)。这个页面中包含了一个类型为pgd_t的数组。pgd的载入到CPU的方式完全和体系结构相关。x86,进程的页表地址从mm_struct->pgd载入到CR3寄存器,载入页表地址的同时,会引起TLB(快表,是对页目录,页表缓存的缓冲区)也被强制刷新。事实上,这也是__flush_tlb()函数,实现的机制。
PGD中的每个条目指向一个页(page frame), 这个页是“由类型为pud_t的条目组成的PUD”。 PUD中的每个条目同样指向一个页,这个页是“由类型为pmd_t的条目组成的PMD”。PMD的每个条目指向一个页,这个页是“由类型为pte_t的条目组成的PTE”。PTE的每个条目就指向了真正的数据或指令所在的页面的首地址的了,这也不是100%的,如果所需要的页面被交换到磁盘空间去后,这个条目就包含的内容是在当page fault发生后,传入需要调用的 do_swap_page()函数,找到包含页面数据的交换空间。
将线性地址转换成物理地址,需要将线性地址分成5个部分,其中4个的值是在各级页表中的索引或者也可以看成是偏移(OFFSET),另外一个是数据在页中的偏移。为了分别析出这5个部分,各级页表和页中偏移都拥有特定的几个宏:SHIFT,SIZE和MASK。SHIFT宏表示各级页表或页中偏移所占用的bit数。
MASK的值和线性地址做AND运算,获得一个各级的高位部分,一般用于页面,页表对齐。SIZE宏表示各级所能管理的内存空间的字节数。
MASK和SIZE都是有SHIFT计算得到,如x86体系结构是这样的:
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PAGE_MASK (~ (PAGE_SIZE - 1))
PAGE_SHIFT是线性地址中偏移(offset)的位的位数,x86系统是12位。page的字节数计算很简单:2PAGE_SHIFT(和1<
PMD_SHIFT是线性地址中第三级页表的所占的位数,PMD_SIZE和PMD_MARK是由这个宏计算得到的。
PUD_SHIFT是线性地址中第二级页表的所占的位数,PUD_SIZE和PUD_MARK是由这个宏计算得到的。
PGD_SHIFT是线性地址中第一级页表的所占的位数,PGD_SIZE和PGD_MARK是由这个宏计算得到的。
最后介绍4个重要的宏:PTRS_PER_PGD,PTRS_PER_PUD, PTRS_PER_PMD,PTRS_PER_PTE。它们用于确定每级页表有多少条目。
不使能PAE特性的x86体系结构这几个宏定义如下:
#define PTRS_PER_PGD 1024
#define PTRS_PER_PUD 1 //这种情况下PUD事实不起作用,为了代码的硬件无关性,设置为1。
//在include/asm-generic/pgtable-nopud.h中定义
#define PTRS_PER_PMD 1 //这种情况下PMD事实不起作用,为了代码的硬件无关性,设置为1。
//在include/asm-generic/pgtable-nopmd.h中定义
#define PTRS_PER_PTE 1024
页表条目(Page table entry)
页表的每个条目都是一个声明为数据结构的对象:pgd_t,pud_t,pmd_t和pte_t分别对应PGD,PUD,PMD和PTE。
虽然这些数据结构常常只有一个无符号整数,它们被定义成数据结构有2个原因:第一,类型保护,防止被不合适的方式使用。第二,容易扩展每个条目所占字节的数量,如x86使能PAE,则需要另外加入4位(原书是说4位,但是我觉得应该是错误的,应该是加入了4个字节),以使得能够访问多余4GB的物理内存。
为了保持一些保护位,定义了pgprot_t数据结构,它保存相关的标志,通常会保持在页表条目的低位区域。
为了类型的计算,在文件asm/page_32.h或者asm/page_64.h中定义了5个宏。传入上述的类型,返回相应的数据结构中的部分数值:pte_val(),pmd_val(),pud_val()和pgprot_val(). 相反的操作的计算的宏:__pte(),__pmd(),__pud(),__pgd()和__pgprot()。
条目中的状态位,完全是和体系结构相关的。下面解释一下不使能PAE的x86体系结构下,各个状态位的含义。
没有使能PAE的x86,pte_t数据结构中只有一个32位的整数。每个PTE中的类型为pte_t的指针指向一个页面的首地址,也就是说指向的地址总是页面对齐的。因此,在这个整数中PAGE_SHIFT指定数目的位数,也就是12位,是给页表条目中的状态位。列表如下:
比较费解的是_PAGE_PROTNONE这个状态位,x86的体系结构上并不存在这个状态位,LINUX内核借用了PAT位作为这个来使用。这里还有一个问题如果有PSE位被设置,则PAT位的位置就会使用另外一个位置,幸运的是,LINUX内核不会在用户页面中使用PSE特性。LINUX内核挪用这个位的目的是:确定一个虚拟内存的页面在物理内存中是存在的,但是用户空间的进程不能访问它,如同对一段内存区域调用mprotect() API函数并传入PROT_NONE标志一样。当一段内存区域被要求保护,_PAGE_PRESENT为被清除,_PAGE_PROTNONE位被置一。pte_present()宏会同时检测这2位的设置情况,让kernel能够自己知道对应的PTE是否可用:
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
如果正好是用户空间不能访问的页面,这就相当巧妙了,但是也相当的重要考量。因为硬件状态为_PAGE_PRESENT已经被清除,当试图访问这个页面的时候,会产生一个page fault的异常,LINUX内核强制的保护了页面访问,但是内核还是知道页面是存在的,如果需要交换到磁盘或者进程退出释放页面,能够做出正确的动作。
阅读(5034) | 评论(0) | 转发(1) |