内存管理之4:页面映射中的结构体

date: 2014-09-10 19:09linux

备注:本文中引用的内核代码的版本是2.4.0。数组

在前面的文章中,咱们介绍了linux页式内存管理,讲到了页面目录PGD、中间目录PMD以及页表PT,本文来看下内核中对应的结构体定义。函数

1 页表项pte_t以及相关操做

PGD、PMD以及PT分别是由pgd_t(页面目录项)、pmd_t(中检目录项)以及pte_t(页表项)构成的数组,这些表项(虽然只有32位)被定义成结构体,定义在<asm/page.h>中:this

/*
 * These are used to make use of C type-checking..
 */
#if CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
typedef struct { unsigned long long pmd; } pmd_t;
typedef struct { unsigned long long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32)) 
#else
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)
#endif
#define PTE_MASK PAGE_MASK

typedef struct { unsigned long pgprot; } pgprot_t;

#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) } )

可见,当采用32位地址时,pgd_t、pmd_t和pte_t就是无符号整形数。为何要定义成结构体呢?一方面是为了方便后续的扩展;另外一方面就像面向对象中的封装同样,这里也是一种封装,并定义了相关的“访问器”函数。以pte_t为例,经过pte_val宏来访问结构体中成员,另外经过set_pte来设置结构体。宏set_pte定义在<asm/pgtable-2level.h>中:设计

/*
 * Certain architectures need to do special things when PTEs 
 * within a page table are directly modified. Thus, the following 
 * hook is made available.
 */ 
#define set_pte(pteptr, pteval) (_(pteptr) = pteval)
/* 
 * (pmds are folded into pgds so this doesnt get actually called,
 * but the define is needed for a generic inline function.)
 */
#define set_pmd(pmdptr, pmdval) (_(pmdptr) = pmdval)
#define set_pgd(pgdptr, pgdval) (_(pgdptr) = pgdval)

前面咱们讲过,物理页面是以4K为边界对齐的,意味着每一个物理页面的起始地址(固然是物理地址)的低12都为0,只有高20位是有效的。内核中有一个物理页面Page的数组mem_map,每一个物理页面对应mem_map数组中的一个元素,而数组的下标就是物理页面的序号。物理页面在数组mem_map中按起始地址顺序存放,所以咱们能够根据页面序号获得页面的起始地址,很简单,将页面序号乘以4K(即左移12位)就能够获得页面的起始地址。从这个意义上来说,物理页面起始地址的高20位能够看作是页面序号。3d

物理页面起始地址只有高20位是有效的,那么做为指向物理页面起始地址的页表项pte_t,做为指针只须要它的高20位,因此pte_t中的低12位就挪做他用,用来表示页面的状态信息和访问权限。但在页表项pte_t结构的定义中,并无以位域的方式体现出来,内核为此单独定义了一个用来表示页面保护的结构pgprot_t,它的定义也在上面的代码中,而且内核也位置定义了“访问器”函数。指针

虽然pgprot_t结构被独立出来了,但一个页面对应的页面保护信息仍然保存在页表项pte_t的低12位中,这里只是为了程序设计的方便单独为页面保护信息抽象出一个结构体。咱们能够根据物理页面的起始地址以及页面保护结构pgprot_t拼凑出一个页表项pte_t,内核中__mk_pte宏就是用来干这件事的,宏定义在<asm/pgtable-2level.h>中:code

#define __mk_pte(page\_nr,pgprot) \__pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))

将物理页面的序号左移12位获得页面起始地址的高20位,而后位或上低12位的页面保护结构就可获得物理页面对应的页表项pte_t了;那么反过来,从pte_t获得对应的物理页面的Page结构也是瓜熟蒂落的了,在同一文件中定义了pte_page宏:对象

#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))

数组mem_map的起始地址加上对应的下标,即获得对应元素的地址了。blog

pgprot_t结构被定义成了一个无符号整形数,但有效的只有其低12位,与pte_t中的低12位对应,其中9位是标志位,表示页面的当前状态和访问权限(具体含义参考《x86页式内存管理》),这些标志位在<asm/pgtable.h>中定义以下:

#define _PAGE_BIT\_PRESENT 0
#define \_PAGE_BIT_RW 1
#define _PAGE_BIT\_USER 2
#define \_PAGE_BIT_PWT 3
#define _PAGE_BIT\_PCD 4
#define \_PAGE_BIT_ACCESSED 5
#define _PAGE_BIT\_DIRTY 6
#define \_PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page, Pentium+, if present.. */
#define \_PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */

#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+ */

#define _PAGE_PROTNONE 0x080 /* If not present */

利用这些标志位,咱们就能够判断处对应页面的状态,相关的宏定义以下:

<asm/pgtable-2level.h>
    #define pte_none(x)		(!(x).pte_low)
    
    <asm/pgtable.h>
    
    #define pte_present(x)	((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
    /*
     * The following only work if pte_present() is true.
     * Undefined behaviour if not..
     */
    static inline int pte_read(pte_t pte)		{ return (pte).pte_low & _PAGE_USER; }
    static inline int pte_exec(pte_t pte)		{ return (pte).pte_low & _PAGE_USER; }
    static inline int pte_dirty(pte_t pte)		{ return (pte).pte_low & _PAGE_DIRTY; }
    static inline int pte_young(pte_t pte)		{ return (pte).pte_low & _PAGE_ACCESSED; }
    static inline int pte_write(pte_t pte)		{ return (pte).pte_low & _PAGE_RW; }

对内核来讲,当页面表项的内容为空(即值为0)表示还没有为对应的虚存页面创建映射。回想下逻辑地址映射的过程:利用逻辑地址的高10位在目录表中查找到对应的目录项,此目录项指向一个页表,再利用逻辑地址的中间10位在页表中查找对应的页面表项。按道理说,该页表项应该指向物理页面的起始地址(物理地址),但如今页面表项的值为0,即说明对应的虚存页面还没有映射到某个物理页面上。内核用pte_none宏来检测这种状况。

若是页面表项pte_t非空,但P(Present)位为0,则表示映射已经创建,但对应的物理页面不在内存中(已经换出到交换设备上了)。内核用pte_present宏来判断pte_t对应的物理页面是否在内存中。

pte_read等宏检查pte_t中的相关位是否置1,从而获得页面的相关状态和权限。固然这些只有当P位为1时才有效。

2 MASK && SIZE

前一篇文章提到,在将linux三层页式映射模型落实到intel的两层页式映射之上时,内核(2.4.0版本)采用让中间目录PMD“名不副实”的方案,咱们将相关细节集中展现在这里:

<asm/page.h>
    
    /* PAGE_SHIFT determines the page size */
    #define PAGE_SHIFT 12
    #define PAGE_SIZE	(1UL << PAGE_SHIFT)
    #define PAGE_MASK	(~(PAGE_SIZE-1))
    
    
    <asm/pgtable-2level.h>
    
    /*
     * traditional i386 two-level paging structure:
     */
    #define PGDIR_SHIFT	22
    #define PTRS_PER_PGD	1024
    
    /*
     * the i386 is two-level, so we don't really have any
     * PMD directory physically.
     */
    #define PMD_SHIFT	22
    #define PTRS_PER_PMD	1
    #define PTRS_PER_PTE	1024
    
    
    <asm/pgtable.h>
    
    #define PMD_SIZE	(1UL << PMD_SHIFT)
    #define PMD_MASK	(~(PMD_SIZE-1))
    #define PGDIR_SIZE 	(1UL << PGDIR_SHIFT)
    #define PGDIR_MASK 	(~(PGDIR_SIZE-1))

PGDIR_SHIFT、PMD_SHIFT和PAGE_SHIFT分别表示虚拟地址(即通过段式映射后的线性地址,后文对线性地址和虚拟地址不做区分,认为它们是同一个东西)中页面目录位段、中间目录位段以及页表位段的划分状况,示意以下(偷图自《深刻理解linux虚拟内存管理》):

shift

PGDIR_SIZE、PMD_SIZE和PAGE_SIZE分别根据PGDIR_SHIFT、PMD_SHIFT和PAGE_SHIFT来定义,分别表示一个目录项(pgd_t)、一个中间目录项(pmd_t)和一个页面表项(pte_t)所能“领衔”的地址空间的大小。好比一个页面表项指向一个物理页面,它所能“领衔”的地址空间的大小就是4K(1 << 12)。而一个目录项指向1个页表(两层映射的场景下),一个页表共有1024个页面表项,表明1024个页面,每一个页面4K,故一个目录项表明4M(1 << 22)的空间

PGD_MASK、PMD_MASK和PAGE_MASK分别由PGDIR_SIZE、PMD_SIZE和PAGE_SIZE来定义。分别表示虚拟地址中页面目录位段、中间目录位段以及页表位段的掩码。将虚拟地址与这些掩码相位与,便可获得对应的位段。好比目录位段为虚拟地址的高10位,那么目录位段的掩码PGD_MASK应该是高10位为1,低22位全为0。MASK与SIZE的对应关系描述以下:

mask

回到正题,2.4.0内核是如何让中间目录项PMD名不副实的呢?

在上面的代码中,内核将PGD_SHIFT定义为22,PTRS_PER_PGD定义为1024。毫无疑义,虚拟地址的高10位用做页面目录位段,故PGD_SHIFT定义为22。页面目录表PGD中有1024个目录项,这与页面目录位段共有10bit相对应,这就是PTRS_PER_PGD的含义。而PMD_SHIFT也被定义成22,对应的PTRS_PER_PMD被定义成1。显然这是内核玩的花招,让PMD位段在虚拟地址中占0个bit,还“装模做样”的定义了中间目标表中中间目录的个数为1个。

须要指出,linux的三层映射只是软件设计上的概念,表示一种抽象。而在intel上,两层页面映射是由MMU硬件完成的,只要咱们设置好了CR3寄存器,MMU硬件自动帮咱们完成页面映射(查页面目录表找到对应的目录项,该目录项指向一个页表,再从页表中找到对应的页面表项等等,全程不须要CPU的参入),它压根儿就不认PMD(只认PGD和PT),更不在意内核耍了什么花招让PMD“名不副实”,让PMD“名不副实”只是软件上的诉求,只是为了“套上”linux的“三层映射”模型。以前都是内核欺骗CPU,感受此次像是内核本身欺骗本身了。

相关文章
相关标签/搜索