Linux 内存管理浅析

来源:互联网 发布:怪物猎人XX数据库安卓 编辑:程序博客网 时间:2024/05/18 16:54

(3). 页表简介

前面说了那么多,现在我们可以详细的讨论页表了。因为这部分内容涉及到平台相关性,我将以e6500为例说明。

当虚拟地址转换成实际的物理地址时,我们需要有个实体记录这种转换关系,这个实体就是页表项(Page table entry)。因为物理地址我们是分页来管理的,则每一个物理页我们都需要一个这样的页表项来表示这种转换关系。页表项实体抽象出来,我们可以用一个结构体或类型来表示(实际上是unsigned long类型,对于64bit CPU来说就是64bit)。如果有物理页面我们需要访问,那么就实例化一个这样的页表项。注意这里我们只是按需实例化,并没有全部实例化所有物理地址空间的页表项。所有这样的页表项就组合成了页表(Page table)。

前面我说过,虚拟地址空间没有分页机制,分页只是存在于物理地址空间。在物理地址空间,我们可以通过页面号(Page frame number)来唯一标识一段地址。那么对于虚拟地址来说,其也会自然的对齐到页面大小。也就是说,映射到某个物理页面的一段连续虚拟地址,大小为一个物理页面,它们对应于同一个物理页面。那么,我们建立这样的映射关系时,我们可以舍弃虚拟地址的某些低地址位(如果页面大小4KB,虚拟地址0x80008400和0x80008600,那么我们实际只需要地址0x80008000对应于哪个物理页面就可以了)。

既然规定了物理页面大小,那么虚拟地址也就确定了其最小映射地址bit位。对于页面内部地址bit位,我们称之为页面内部偏移(page offset)。下面这幅图显示了对于虚拟地址bit位的划分。


这里写图片描述


图中,Offset部分,是页面内部地址bit位,如果是4KB页面,则bit位数为12,也即12bit能够表示4KB地址(2^12)。我们知道,虚拟地址位数取决于系统是32bit 还是64bit。因为e6500是一个64bit CPU,自然虚拟地址会有64bit。

在上图,我们还看到有PTE,PMD,PUD,PGD。PTE我们知道是指的页表项,前面已经说过。那么PMD,PUD,PGD表示什么呢?

我们看下,如果没有PMD,PUD,PGD这几个部分,那么PTE部分可能就会有52bit地址空间(以64bit为例,64-12=52)。这是一个非常大的地址范围,最多可能有2^52个PTE。如果我们采用hash表之类的方式存储页表项,当我们需要查询某个PTE时,可能需要搜索非常长的时间。我们知道,查询PTE是非常频繁的操作,这对于系统来说,是不可接受的。后面我们会看到,在e6500上,PTE实际是连续存储在一起的。那么这就要求在2^52 字节的内存空间,我们只能像存储数组元素那样去存储PTE,这对于内存空间来说是不可能的。

基于上面两个原因,我们把虚拟地址bit位分解为PGD,PUD,PMD,PTE这样几个部分。通过页表分级的方法,实现页表项的局部加载。下面这幅图显示了它们的架构。这里我画出了e6500的虚拟地址bit位的划分。具体定义在:arch\powerpc\include\asm\pgtable-ppc64-4k.h

#define PTE_INDEX_SIZE  9#define PMD_INDEX_SIZE  7#define PUD_INDEX_SIZE  9#define PGD_INDEX_SIZE  9

这里写图片描述


  • PGD(Page Global Directory) Table:
    对于每个进程来说(包括用户进程和内核进程),只有一个PGD Table。每个PGD称之为PGD Table中一项(Entry),PGD数量取决于PGD_INDEX_SIZE bit数。每一个PGD保存了下一级表(PUD Table)的虚拟地址。PGD本身存储空间大小为unsigned long类型,64bit系统中,就是8字节大小。这里我们定义了PGD_INDEX_SIZE为9,也即PGD Table存储空间为2^9 x 8字节=4KB,刚好是一页大小。

  • PUD(Page Upper Directory) Table 与 PMD(Page Middle Directory) Table:
    这两级目录是页表结构的中间层。它们的每一项分别保存了下一级表的地址。

  • PTE(Page Table Entry) Table:
    PTE中保存了真正的物理页面的页面号(PFN)。不过由于物理地址空间通常比虚拟地址空间要小,并且最少有12bit会用不到(page offset),所以PTE还可以存储一些页面标志位。在e6500上只有40 bit,则PFN表示最大需要bit数为:40 - 12(4KB页面) = 28 bit。而PTE可以存储64bit。

    下面是e6500 PTE的内容:

    ARPN:是物理页面号,一共有40bit,但实际上只需要低28bit就够了。
    WIMGE:是页面访问控制选项,控制物理页面的访问及写入方式等。
    V:是页表项是否有效标志。
    C:当前页面内容是否被改变,这个标志位用来做我们的通常说的dirty位。
    PS:页面SIZE,对于e6500来说,固定为4KB。
    BAP:基本访问权限控制。

    这些标志位被定义在:arch\powerpc\include\asm\pte-book3e.h。


这里写图片描述


当进程访问虚拟地址时,如果没有在MMU TLB中查找到到对应PTE,则会发生TLB miss(后面会详述)。这时系统软件需要负责查找对应PTE表。查找的次序就是先查找PGD表,根据虚拟地址PGD部分偏移,查找到对应PGD,获得PUD表地址,再根据虚拟地址PUD部分偏移查找到对应PMD表。以此类推,查找到相应的PTE,将PTE更新到MMU TLB中,从而完成地址转换。

有的平台可能不需要用到PUD和PMD这两级,但为了系统兼容性,通常还是有这四级表,只不过PUD和PMD表只有一项。我们看到,通过表的分级我们不需要同时将所有页表项都加载到内存中,减轻了内存负担,但这会增加内存的访问次数,增加查找时间,为了平衡查找时间和内存占用,有的平台会只用两级页表。


对于页表的建立和操作,内核提供了通用函数,但具体实现是平台相关的。下面我列出了一部分这样的函数,它们都定义在arch\powerpc\include\asm\pgalloc-64.h:

  • pgd_alloc,pud_alloc_one,pmd_alloc_one,pte_alloc_one,pud_free,pmd_free,pte_free,pgd_free
    这些函数提供了各级页表的存储空间分配和释放。由于平台不同,这里的函数实现可能会不同。但它们都是系统通用函数,就是说,不管哪个平台,都需要实现这样一个函数以满足系统框架调用,即使函数本身什么也不做。对于一个进程来说,PGD表是在进程建立的时候就分配好了,而且,进程存在期间,会一直存在。

  • pgd_populate,pud_populate,pmd_populate
    这几个函数根据传进来的参数,对相应的表项进行赋值。这几个函数和上面分配、释放函数都定义为内联函数。

  • set_pte_at(定义在:arch\powerpc\mm\pgtable.c)
    这个函数负责对PTE进行设置。因为不同平台对于PTE的要求不一样,这个函数会调用平台相关的函数负责具体写入。