2.5 Linux中的分页机制
如前所述,Linux主要采用分页机制来实现虚拟存储器管理。这是因为:
· Linux的分段机制使得所有的进程都使用相同的段寄存器值,这就使得内存管理变得简单,也就是说,所有的进程都使用同样的线性地址空间(0~4G)。
· Linux设计目标之一就是能够把自己移植到绝大多数流行的处理器平台。但是,许多RISC处理器支持的段功能非常有限。
为了保持可移植性,Linux采用三级分页模式而不是两级,这是因为许多处理器(如康柏的Alpha,Sun的UltraSPARC,Intel的Itanium)都采用64位结构的处理器,在这种情况下,两级分页就不适合了,必须采用三级分页。图2.28为三级分页模式,为此,Linux定义了三种类型的页表:
· 总目录PGD(Page Global Directory)
· 中间目录PMD(Page Middle Derectory)
· 页表PT(Page Table)

图2.28 Liunx的三级分页
尽管Linux采用的是三级分页模式,但我们的讨论还是以Intel奔腾处理器的两级分页模式为主,因此,Linux忽略中间目录层,以后,我们把总目录就叫页目录。
2.5.1 与页相关的数据结构及宏的定义
上一节讨论的分页机制是硬件对分页的支持,这是虚拟内存管理的硬件基础。要想使这种硬件机制充分发挥其功能,必须有相应软件的支持,我们来看一下Linux所定义的一些主要数据结构,其分布在include/asm-i386/目录下的page.h,pgtable.h及pgtable-2level.h三个文件中。
1. 表项的定义
如上所述,PGD、PMD及PT表的表项都占4个字节,因此,把它们定义为无符号长整数,分别叫做pgd_t、pmd_t及pte_t(pte 即Page table Entry),在page.h中定义如下:
| typedef struct { unsigned long pte_low; } pte_t; typedef struct { unsigned long pmd; } pmd_t; typedef struct { unsigned long pgd; } pgd_t; typedef struct { unsigned long pgprot; } pgprot_t; |
可以看出,Linux没有把这几个类型直接定义长整数而是定义为一个结构,这是为了让gcc在编译时进行更严格的类型检查。另外,还定义了几个宏来访问这些结构的成分,这也是一种面向对象思想的体现:
| #define pte_val(x) ((x).pte_low) #define pmd_val(x) ((x).pmd) #define pgd_val(x) ((x).pgd) |
从图2.22和图2.24 可以看出,对这些表项应该定义成位段,但内核并没有这样定义,而是定义了一个页面保护结构pgprot_t和一些宏:
| typedef struct { unsigned long pgprot; } pgprot_t; #define pgprot_val(x) ((x).pgprot) |
字段pgprot的值与图2.24页面项的低12位相对应,其中的9位对应0~9位,在pgtalbe.h中定义了对应的宏:
| #define _PAGE_PRESENT 0x001 #define _PAGE_RW 0x002 #define _PAGE_USER 0x004 #define _PAGE_PWT 0x008 #define _PAGE_PCD 0x010 #define _PAGE_ACCESSED 0x020 #define _PAGE_DIRTY 0x040 #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */ #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */ |
在你阅读源代码的过程中你能体会到,把标志位定义为宏而不是位段更有利于编码。
另外,页目录表及页表在pgtable.h中定义如下:
| extern pgd_t swapper_pg_dir[1024]; extern unsigned long pg0[1024]; |
swapper_pg_dir为页目录表,pg0为一临时页表,每个表最多都有1024项。
2.线性地址域的定义
Intel线性地址的结构如图2.29所示:

图2.29 32位的线性地址结构
(1) 偏移量的位数
| #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL << PAGE_SHIFT) #define PTRS_PER_PTE 1024 #define PAGE_MASK (~(PAGE_SIZE-1)) |
其中PAGE_SHIFT宏定义了偏移量的位数为12,因此页大小PAGE_SIZE为212=4096字节; PTRS_PER_PTE为页表的项数;最后PAGE_MASK值定义为0xfffff000,用以屏蔽掉偏移量域的所有位(12位)。
| (2) PGDIR_SHIFT #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 #define PGDIR_SIZE (1UL << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE-1)) |
PGDIR_SHIFT是页表所能映射区域线性地址的位数,它的值为22(12位的偏移量加上10位的页表);PTRS_PER_PGD为页目录目录项数;PGDIR_SIZE为页目录的大小,为222,即4MB;PGDIR_MASK为0xffc00000,用于屏蔽偏移量位与页表域的所有位。
| (3)PMD_SHIFT #define PMD_SHIFT 22 #define PTRS_PER_PMD 1 |
PMD_SHIFT为中间目录表映射的地址位数,其值也为22,但是因为Linux在386中只用了两级页表结构,因此,让其目录项个数为1,这就使得中间目录在指针序列中的位置被保存,以便同样的代码在32位系统和64位系统下都能使用。后面的讨论我们不再提及中间目录。
2.5.2 对页目录及页表的处理
在page.h,pgtable.h及pgtable-2level.h三个文件中还定义有大量的宏,用以对页目录、页表及表项的处理,我们在此介绍一些主要的宏和函数。
1.表项值的确定
|
static inline int pgd_none(pgd_t pgd) { return 0; } #define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE)) |
pgd_none()函数直接返回0,表示尚未为这个页目录建立映射,所以页目录项为空。pgd_present()函数直接返回1,表示映射虽然还没有建立,但页目录所映射的页表肯定存在于内存(即页表必须一直在内存)。
pte_present宏的值为1或0,表示P标志位。如果页表项不为0,但标志位为0,则表示映射已经建立,但所映射的物理页面不在内存。
2. 清相应表的表项:
| #define pgd_clear(xp) do { } while (0) #define pte_clear(xp) do { set_pte(xp, __pte(0)); } while (0) |
pgd_clear宏实际上什么也不做,定义它可能是为了保持编程风格的一致。pte_clear就是把0写到页表表项中。
3.对页表表项标志值进行操作的宏。
这些宏的代码在pgtable.h文件中,表2.1给出宏名及其功能。
表2.1 对页表表项标志值进行操作的宏及其功能

实际上页表的处理是一个复杂的过程,在这里我们仅仅让读者对软硬件如何结合起来有一个初步的认识,有关页表更多的内容我们将在第六章接着讨论。


