之四:页面映射中的结构体

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

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

一、 页表项pte_t以及相关操作

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

/*
 * These areused 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>中:

/*
 * Certainarchitectures need to do special things when PTEs
 * within apage table are directly modified.  Thus,the following
 * hook ismade available.
 */
#define set_pte(pteptr, pteval) (*(pteptr) =pteval)
/*
 * (pmds arefolded into pgds so this doesnt get actually called,
 * but thedefine 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位可以看做是页面序号。

物理页面起始地址只有高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>中:

#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+((unsignedlong)(((x).pte_low >> PAGE_SHIFT))))

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

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 entryPPro+ */
 
#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))
/*
 * Thefollowing only work if pte_present() is true.
 * Undefinedbehaviour 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时才有效。

二、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 i386is two-level, so we don't really have any
 * PMDdirectory 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虚拟内存管理》):

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的对应关系描述如下:


回到正题,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,感觉这次像是内核自己欺骗自己了。


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值