date: 2014-09-10 19:09

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

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

1 页表项pte_t以及相关操作

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

/*

* 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定义在中:

/*

* 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位可以看做是页面序号。

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

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

#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的起始地址加上对应的下标,即得到对应元素的地址了。

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

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

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

#define pte_none(x)(!(x).pte_low)

#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“名存实亡”的方案,我们将相关细节集中展示在这里:

/* PAGE_SHIFT determines the page size */

#define PAGE_SHIFT 12

#define PAGE_SIZE(1UL << PAGE_SHIFT)

#define PAGE_MASK(~(PAGE_SIZE-1))

/*

* traditional i386 two-level paging structure:

*/

#define PGDIR_SHIFT22

#define PTRS_PER_PGD1024

/*

* the i386 is two-level, so we don't really have any

* PMD directory physically.

*/

#define PMD_SHIFT22

#define PTRS_PER_PMD1

#define PTRS_PER_PTE1024

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

c语言 结构体映射,内存管理之4:页面映射中的结构体相关推荐

  1. 内存编程 c语言 c,C语言编程入门之内存管理

    本篇教程探讨了C语言编程入门之内存管理,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入. < 自动变量与静态变量 auto自动变量 auto是默认的关键字,如实际中int a ...

  2. [转载] Java内存管理-你真的理解Java中的数据类型吗(十)

    参考链接: Java中的字符串类String 1 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 推荐阅读 第一季 0.Java的线程安全.单例模式.JVM内存结构等知识 ...

  3. Go语言设计与实现 -- 内存管理器

    不同的编程语言选择不同的方式管理内存,本节会介绍Go语言内存分配器. Go内存分配的设计思想是: 内存分配算法采用Google的TCMalloc算法,每个线程都会自行维护一个独立的内存池,进行内存分配 ...

  4. C 语言编程 — 堆栈与内存管理

    目录 文章目录 目录 前文列表 栈(Stack)和堆(Heap) 栈 堆 内存管理 动态分配内存 重新调整内存的大小和释放内存 malloc 函数详解 memset 初始化内存数据 前文列表 < ...

  5. Linux内存管理 (2)页表的映射过程

    专题:Linux内存管理专题 关键词:swapper_pd_dir.ARM PGD/PTE.Linux PGD/PTE.pgd_offset_k. Linux下的页表映射分为两种,一是Linux自身的 ...

  6. nginx源码分析—内存池结构ngx_pool_t及内存管理

    本博客( http://blog.csdn.net/livelylittlefish)贴出作者(阿波)相关研究.学习内容所做的笔记,欢迎广大朋友指正! Content 0.序 1.内存池结构 1.1 ...

  7. JVM运行时结构、Java内存管理、JVM实例、HotSpot VM对象的创建、内存布局和访问定位

    1.JVM运行时结构 Java 运行时数据区域有程序计数器.Java虚拟机栈.本地方法栈.Java堆和方法区.其中前三个线程私有,随线程生而生,线程灭而灭:后面两个是线程间共享. 1.1 程序计数器 ...

  8. nginx源码分析—内存池结构ngx_pool_t及内存管理(精辟)

    Content 0.序 1.内存池结构 1.1 ngx_pool_t结构 1.2其他相关结构 1.3 ngx_pool_t的逻辑结构 2.内存池操作 2.1创建内存池 2.2销毁内存池 2.3重置内存 ...

  9. 学了指针没学动态内存一切都白搭!C语言基础教程之内存管理

    本文将讲解 C 中的动态内存管理.C 语言为内存的分配和管理提供了几个函数.这些函数可以在<stdlib.h>头文件中找到. 在内存中动态地分配 num 个长度为 size 的连续空间,并 ...

最新文章

  1. 关于虚函数的两个例子
  2. Shell中NR/NF的意义
  3. FZU 2150 Fire Game bfs
  4. 测试驱动开发 测试前移_为什么测试驱动的开发有用?
  5. [游戏服务器]第一章:多人聊天室-服务端
  6. Mysql 数据库字符集转换及版本升级/降级的详细教程
  7. mysql 5.5 性能测试,MySQL 5.5迁移到5.7的性能问题排查案例
  8. 【报告分享】2021中国智能驾驶核心软件产业研究报告:软件定义,数据驱动.pdf(附下载链接)...
  9. This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
  10. 数据分析之数据预处理、分析建模、可视化
  11. C++ 学习笔记(22) Builder Pattern
  12. 统计学三种相关系数【pearson、spearman、kendall】
  13. ipython和pycharm的区别_ipython和pycharm有什么区别
  14. jenkins API 使用postman调用
  15. 小米实习生测试开发笔试题
  16. Lync Server 2010不同规模拓扑图详解
  17. 后疫情时代,VR购物—零售业的硬核破局之道
  18. newifi安装php,新路由(newifi)登录地址安装设置步骤
  19. flv地址解析下载方法归纳总结
  20. 安装windows_build_tools一直处于still waiting for installer log file ...中

热门文章

  1. gdb 查看,执行汇编代码
  2. 用adb命令启动停止Android程序
  3. Native wifi API使用
  4. C 语言内存区域分配(进程的各个段)详解
  5. Android--DPAD键的事件处理
  6. ef mysql自动更新_EF Core中怎么实现自动更新实体的属性值到数据库
  7. php过滤特殊字符mysql攻击_php – MySQL在特殊字符处切断字符串
  8. java opencv 调用摄像头_opencv C++实现调用摄像头动态识别人脸
  9. html中投影效果图,利用CSS3(box shadow)制作边框投影
  10. 记一次lua打包环境导致的coredump