上一篇:物理内存初始化(二)
前言
linux 采用独立于底层架构的三级页表,从概念上容易理解,但也意味着不同类型架构的页面之间的差异变得模糊,这种结构不同于其他OS的页表结构,其他os需要管理最下层的物理页面,例如bsd 的 pmap
非3层架构的mmu将模拟这种三级页表结构,例如,不打开PAE功能 的x86 ,只有两级页表,那么二级页表(PMD)将只有一个页,并直接折返指向一级页表(PGD)项,这在编译的时候会自动优化,不幸的是,对于不自动管理cache和TLB的mmu架构,必须使用钩子函数来修改或者清除cache和TLB的内容,即使是空操作,例如x86,3.8 节将讨论这些钩子函数
本章首先介绍
1.页表如何组织,以及三级页表的各自数据类型;
2.介绍虚拟地址是怎么通过各级页表翻译成物理地址的;
3.讨论分析最底层的页表PTE,以及介绍地址中一些硬件有关的特殊位。相关页表操作和属性检测及设置的宏。
4.页表怎样增加项,页面如何分配和释放
5.内核启动阶段的页表如何初始化
6.怎样利用TLB and CPU caches
3.1 页表
linux 的每个进程都有一个指针(mm struct→pgd)指向他的PGD(一级页表的所在物理页地址),该页包含 pgd_t结构的数组, pgd_t数据结构依赖架构,定义在<asm/page.h>,pgd页表的装载依赖于架构,例如x86将 mm struct→pgd复制到 cr3 寄存器,这种方式还有另外一个作用,就是清除TLB,事实上清除TLB的函数__flush_tlb()就是通过这种方式来实现的,__flush_tlb的实现也是依赖架构的。
PGD 页表中的每一个有效项指向 PMD 页表所在的物理页。PMD 页表包含pmd_t结构的数组。PMD 页表的每一个有效项又指向PTE页表,PTE页表是pte_t结构的数组,其中的每一项指向包含实际用户数据的物理页。如果页面已经换出到swap区,pte将保存swap 中页面的入口地址,用来在发生缺页错误时,通过do_swap_page() 来寻找swap区中的页面地址
图3.1
任意一个线性(虚拟)地址将被拆分成三个页表的偏移量和页内偏移,内核提供多组宏来帮助得到各个偏移量。分别为SHIFT, SIZE,MASK ,这个看图就明白了。ARM 中:
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PMD_SHIFT 20
#define PMD_SIZE (1UL << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))
#define PGDIR_SHIFT 20
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
PTRS_PER_x宏定义了各级页表包含的条目个数即数组大小。ARM中定义如下
/*
* entries per page directory level: they are two-level, so
* we don't really have any PMD directory.
*/
#define PTRS_PER_PTE 256
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 4096
ARM 页大小4096,PGD 的页表数目有4096 ,所以pgd 页表大小为16k
而PTE 只有256 ,总共覆盖4G空间,但256页只需1024字节,这不空间浪费吗?
3.2 页表条目
如上所述,PTE PMD 和PGD 页表的每一项对应的分别是pte_t,pmd_t,pgd_t结构。虽然他们通常是一个无符号整数,但定义成结构体的目的有两点,一是类型保护,防止程序中错误使用,二是为了一些特殊情况,例如x86 带PAE的地址中的有额外的4位用来4GiB 以上的寻址.pgprot_t用来表示虚拟地址中的保护位,通常在低位。上述这些结构ARM定义如下
typedef struct { unsigned long pte; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
typedef struct { unsigned long pgprot; } pgprot_t;
下面两组宏用来结构到数值的相互转换
#define pte_val(x) ((x).pte)
#define pmd_val(x) ((x).pmd)
#define pgd_val(x) ((x).pgd)
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )
地址中保护位的位置是依赖硬件的
我们将以非PAE来举例说明地址翻译过程,但其实原理(支持PAE的)是一样的。在无PAE 的x86系统中,pte页表项是32bit 整数,每个pte条目 指向的是一个页的地址。并且所有的页地址都是页对齐的。因此,PAGE_SHIFT 所对应的低12位 (页大小为4096字节),pte 条目中的低12位可以用来保存页表项的状态和标志,下图是一些状态和标志位列表,不同硬件架构的所包含的位和具体含义是不同的。ARM中的标志位如下
PTE_DIRTY:CPU在写操作时会设置该标志位,表示对应页面被写过,为脏页。
PTE_YOUNG:CPU访问该页时会设置该标志位。在页面换出时,如果该标志位置位了,说明该页刚被访问过,页面是young的,不适合把该页换出,同时清除该标志位。
PTE_PRESENT:表示页在内存中。
#define L_PTE_PRESENT (1 << 0)//Page is resident in memory and not swapped out.
#define L_PTE_YOUNG (1 << 1)
#define L_PTE_BUFFERABLE (1 << 2)
#define L_PTE_CACHEABLE (1 << 3)
#define L_PTE_USER (1 << 4) //the page is accessible from userspace
#define L_PTE_WRITE (1 << 5)
#define L_PTE_EXEC (1 << 6)
#define L_PTE_DIRTY (1 << 7)
_PAGE_PROTNONE后面会讨论,其他的自行理解
x86 的Pentium III以及更新的系统中,pte中有一个PAT位( Pentium II 中是保留位),用来表示pte所指向的页面大小,在PGD 条目,这个位叫做 Page Size Extension(PSE),显然,这两个位要联合使用
因为linux 在用户空间页面不会使用 PSE bit ,因此pte项的PAT bit 没有用,用作其他目的。
有可能出现这样的情况:页面需要驻留在内存中,但不能被用户空间的进程访问。例如被 mprotect() 保护的一段的带PROT_NONE标志的区域。当这个区域被保护, PAGE_PRESENT bit被清除,PAGE_PROTNONE bit 将设置。内核通过pte_present() 宏检测这两个位是否有一个存在,从而得知到这个pte是否存在。它只是不能被用户空间访问,这是个隐秘但又重要的点,因为当PAGE_PRESENT bit被清除,访问该页会产生缺页错误,此时linux内核将对其保护并知道该页存在(用户无法访问),如果有进程退出或需要将该页换出
3.3 页表项的使用
下面这些重要的宏定义在<asm/pgtable.h>,用来地址翻译和检验页表条目
pgd_offset 获得对应的PGD页表项虚拟地址
pmd_offset获得对应的PMD页表项虚拟地址,(由于硬件只有两级页表,因此得到的还是PGD页表项虚拟地址)
pte_offset获得对应的PTE页表项虚拟地址
而线性地址的剩余位用来表示页内偏移,如图3.1点击跳转
/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd+pgd_index(addr))
#define pmd_offset(dir, addr) ((pmd_t *)(dir))
/* Find an entry in the third-level page table.. */
#define __pte_offset(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset(dir, addr) ((pte_t *)pmd_page(*(dir)) + __pte_offset(addr))
static inline unsigned long pmd_page(pmd_t pmd)
{
unsigned long ptr;
ptr = pmd_val(pmd) & ~(PTRS_PER_PTE * sizeof(void *) - 1);
ptr += PTRS_PER_PTE * sizeof(void *);
//页表项存放的是物理地址,而程序使用的是虚拟地址,因此需要转换
return __phys_to_virt(ptr);
}
下面一组宏用来判断页表条目是否存在或使用。
pte_none(), pmd_none() and pgd_none() 返回 1如果对应页表条目不存在
pte_present(), pmd_present() and pgd_present() 返回 1 如果对应页表条目存在
pte_clear(), pmd_clear() and pgd_clear() 清除对应的页表条目
pmd_bad() and pgd_bad() 用来检测是否错误(页表项被错误篡改),其检测依赖于硬件架构。实际使用中,最终要的还是判断是否存在以及是否可以访问。
#define pgd_none(pgd) (0)
#define pgd_bad(pgd) (0)
#define pgd_present(pgd) (1)
#define pgd_clear(pgdp) do { } while (0)
#define pmd_none(pmd) (!pmd_val(pmd))
#define pmd_bad(pmd) (pmd_val(pmd) & 2)//?
#define pmd_present(pmd) (pmd_val(pmd))
#define pmd_clear(pmdp) set_pmd(pmdp, __pmd(0))
#define pte_none(pte) (!pte_val(pte))
#define pte_clear(ptep) set_pte((ptep), __pte(0))
#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)
虚拟内存管理中很多地址翻译的代码,下面是 mm/memory.c 中follow_page函数的部分代码,展示了地址翻译的过程
pgd_t *pgd;
pmd_t *pmd;
pte_t *ptep, pte;
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || pgd_bad(*pgd))
goto out;
pmd = pmd_offset(pgd, address);
if (pmd_none(*pmd) || pmd_bad(*pmd))
goto out;
ptep = pte_offset(pmd, address);
if (!ptep)
goto out;
pte = *ptep;
通过三个XXX_offset宏来一步步找到实际的物理页,使用XXX_none 和 XXX_bad宏来保证页表的有效性
下面这组宏检测以及设置页面的权限,权限决定了用户进程对该页的访问权限。例如内核空间是不允许用户进程访问的。
pte_modify 基本没有使用,除了在 mm/mprotect.c 的 change_pte_range 函数
name | 功能 |
---|---|
pte_read | tested the read permissions for an entry |
pte_write | tested the write permissions for an entry |
pte_exec | tested the execute permissions for an entry |
pte_mkread | set the read permissions for an entry |
pte_mkwrite | set the write permissions for an entry |
pte_mkexec | set the execute permissions for an entry |
pte_rdprotect | clear the read permissions for an entry |
pte_wrprotect | clear the write permissions for an entry |
pte_exprotect | clear the execute permissions for an entry |
pte_modify | modified the permissions for an entry |
最后一组宏测试和设置页表项的状态位,在linux只有两个重要的状态位,分别是dirty bit 和 accessed bit.
name | 功能 |
---|---|
pte_dirty | check the dirty bit |
pte_young | check the accessed bit |
pte mkdirty | set the dirty bits, |
pte_mkyoung | set the accessed bits, |
pte_mkclean | clear the dirty bits, |
pte_mkold | clear the accessed bits, |