Linux内存管理:分页机制

Linux内存管理:内存描述之内存节点node

Linux内存管理:内存描述之内存区域zone

Linux内存管理:内存描述之内存页面page

Linux内存管理:内存描述之高端内存

Linux内存管理:分页机制

内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB

目录

1 分页机制

1.1 为什么使用多级页表来完成映射

1.2 32位系统中2级页表

1.3 64位系统中的分页

1.4 Linux中的分页

2 页表

3 Linux分页机制的演变

3.1 Linux的页表实现

3.2 Linux最初的二级页表

3.3 Linux的三级页表

3.4 Linux的四级页表

4 页式管理

4.1 分段机制存在的问题

4.2 分页存储的基本内容

5 分页机制支持

5.1 硬件分页支持

5.2 常规的32bit分页

5.3 物理地址扩展(PAE)分页机制和扩展分页(PSE)

5.4 64位系统中的分页

5.5 硬件保护方案

6 部分总结

6.1 为什么使用多级页表

6.2 x86_32两级页表结构

6.3 线性地址到物理地址的转换

6.4 扩展分页

6.5 页面高速缓存

7 linux的分页机制

7.1 四级分页机制

7.3 不同架构的分页机制

7.4 为什么linux热衷:分页>分段

8 linux中页表处理数据结构

8.1 页表类型定义pgd_t、pmd_t、pud_t和pte_t

8.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t

8.1.2 pgd_t、pmd_t、pud_t和pte_t

8.1.4 xxx_val和__xxx

8.2 页表描述宏

8.2.1 PAGE宏–页表(Page Table)

8.2.2 PMD-Page Middle Directory (页目录)

8.2.3 PUD_SHIFT-页上级目录(Page Upper Directory)

8.2.4 PGDIR_SHIFT-页全局目录(Page Global Directory)

8.3 页表处理函数

8.3.1 查询页表项中任意一个标志的当前值

8.3.2 设置页表项中各标志的值

8.3.3 宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作

8.3.4 简化页表项的创建和撤消

9 线性地址转换

9.1 分页模式下的的线性地址转换

9.1.1 页表

9.1.2 页表项

9.1.3 地址转换过程

9.2 Linux中通过4级页表访问物理内存

相关链接


 

1 分页机制


前面我们讲解了操作系统段式存储管理的主要内容.

  • 32位,在保护方式下,其能够访问的线性地址空间可达4GB,而且允许几乎不受存储空间限制的虚拟存储器程序。虚拟存储器地址空间能够可达64TB。它还提供了复杂的存储管理和硬件辅助的保护机构和增加了支持多任务操作系统的特别优化的指令。实际上,64TB的虚拟地址空间是有磁盘等外部存储器的支持下实现的。在编写程序是可以放在磁盘存储器上,但在执行时,必须把程序加载到物理存储器中。而存储器管理就是要将46位虚拟地址变换成32位物理地址。

  • 将程序分成不同的段进行管理,我们编程访问内存地址时,访问的其实是操作系统抽象给我们的虚拟地址,通过段基址:段偏移的方式访问内存虚拟地址,极大了简化了程序员的编程结构。

  • 通过硬件和操作系统的段式管理机制,实模式下通过段基址左移四位+段内偏移,保护模式下通过段选择子select从段描述符GDT/LDT表中获取到段描述符,然后对基地址和段偏移处理,将虚拟地址转换为线性地址。

如果没有采用存储器分页管理机制,那么我们得到的线性地址就直接对应与物理地址,否则,则需要将线性地址转换为物理地址。

从80386开始,所有的80x86处理器都支持分页,它通过设置CR0寄存器的PG标志启用分页。当PG=0时,线性地址就被解释成物理地址。

在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address).

很显然,这个页表是需要常驻内存的东西, 以应对频繁的查询映射需要(实际上,现代支持VM的处理器都有一个叫TLB的硬件级页表缓存部件,本文不讨论)。

 

1.1 为什么使用多级页表来完成映射


但是为什么要使用多级页表来完成映射呢?

用来将虚拟地址映射到物理地址的数据结构称为页表, 实现两个地址空间的关联最容易的方式是使用数组, 对虚拟地址空间中的每一页, 都分配一个数组项. 该数组指向与之关联的页帧, 但这会引发一个问题, 例如, IA-32体系结构使用4KB大小的页, 在虚拟地址空间为4GB的前提下, 则需要包含100万项的页表. 这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表, 这回导致系统中大量的所有内存都用来保存页表.

设想一个典型的32位的X86系统,它的虚拟内存用户空间(user space)大小为3G, 并且典型的一个页表项(page table entry, pte)大小为4 bytes,每一个页(page)大小为4k bytes。那么这3G空间一共有(3G/4k=)786432个页面,每个页面需要一个pte来保存映射信息,这样一共需要786432个pte!

如何存储这些信息呢?一个直观的做法是用数组来存储,这样每个页能存储(4k/4=)1K个,这样一共需要(786432/1k=)768个连续的物理页面(phsical page)。而且,这只是一个进程,如果要存放所有N个进程,这个数目还要乘上N! 这是个巨大的数目,哪怕内存能提供这样数量的空间,要找到连续768个连续的物理页面在系统运行一段时间后碎片化的情况下,也是不现实的。

为减少页表的大小并容许忽略不需要的区域, 计算机体系结构的涉及会将虚拟地址分成多个部分. 同时虚拟地址空间的大部分们区域都没有使用, 因而页没有关联到页帧, 那么就可以使用功能相同但内存用量少的多的模型: 多级页表

但是新的问题来了, 到底采用几级页表合适呢?

 

1.2 32位系统中2级页表


从80386开始, intel处理器的分页单元是4KB的页, 32位的地址空间被分为3部分

单元描述
页目录表Directory最高10位
页中间表Table中间10位
页内偏移最低12位

即页表被划分为页目录表Directory和页中间表Tabl两个部分

此种情况下, 线性地址的转换分为两步完成.

  • 第一步, 基于两级转换表(页目录表和页中间表), 最终查找到地址所在的页帧

  • 第二步, 基于偏移, 在所在的页帧中查找到对应偏移的物理地址

使用这种二级页表可以有效的减少每个进程页表所需的RAM的数量. 如果使用简单的一级页表, 那将需要高达220220个页表, 假设每项4B, 则共需要占用220∗4B=4MB220∗4B=4MB的RAM来表示每个进程的页表. 当然我们并不需要映射所有的线性地址空间(32位机器上线性地址空间为4GB), 内核通常只为进程实际使用的那些虚拟内存区请求页表来减少内存使用量.

 

1.3 64位系统中的分页


正常来说, 对于32位的系统两级页表已经足够了, 但是对于64位系统的计算机, 这远远不够.

首先假设一个大小为4KB的标准页. 因为1KB覆盖210210个地址的范围, 4KB覆盖212212个地址, 所以offset字段需要12位.

这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory. 如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB, 即可达到256TB的寻址空间). 剩下的48-12=36位被分配给Table和Directory字段. 即使我们现在决定位两个字段各预留18位, 那么每个进程的页目录和页表都包含218218个项, 即超过256000个项.

基于这个原因, 所有64位处理器的硬件分页系统都使用了额外的分页级别. 使用的级别取决于处理器的类型

平台名称页大小寻址所使用的位数分页级别数线性地址分级
alpha8KB43310 + 10 + 10 + 13
ia644KB3939 + 9 + 9 + 12
ppc644KB41310 + 10 + 9 + 12
sh644KB41310 + 10 + 9 + 12
x86_644KB4849 + 9 + 9 + 9 + 12

1.4 Linux中的分页


层次话的页表用于支持对大地址空间快速, 高效的管理. 因此linux内核堆页表进行了分级.

前面我们提到过, 对于32位系统中, 两级页表已经足够了. 但是64位更多数量的分页级别.

为了同时支持适用于32位和64位的系统, Linux采用了通用的分页模型. 在Linux-2.6.10版本中, Linux采用了三级分页模型. 而从2.6.11开始普遍采用了四级分页模型.

目前的内核的内存管理总是嘉定使用四级页表, 而不管底层处理器是否如此.

单元描述
页全局目录Page GlobalDirectory
页上级目录Page Upper Directory
页中间目录Page Middle Directory
页表Page Table
页内偏移Page Offset

Linux不同于其他的操作系统, 它把计算机分成独立层(体系结构无关)/依赖层(体系结构相关)两个层次. 对于页面的映射和管理也是如此. 页表管理分为两个部分, 第一个部分依赖于体系结构, 第二个部分是体系结构无关的. 所有数据结构几乎都定义在特定体系结构的文件中. 这些数据结构的定义可以在头文件arch/对应体系/include/asm/page.harch/对应体系/include/asm/pgtable.h中找到. 但是对于AMD64和IA-32已经统一为一个体系结构. 但是在处理页表方面仍然有很多的区别, 因为相关的定义分为两个不同的文件arch/x86/include/asm/page_32.harch/x86/include/asm/page_64.h, 类似的也有pgtable_xx.h .

 

2 页表


Linux内核通过四级页表将虚拟内存空间分为5个部分(4个页表项用于选择页, 1个索引用来表示页内的偏移). 各个体系结构不仅地址长度不同, 而且地址字拆分的方式也不一定相同. 因此内核使用了宏用于将地址分解为各个分量.

其他内容请参照博主的另外两篇博客, 我就不罗嗦了

深入理解计算机系统-之-内存寻址(五)–页式存储管理, 详细讲解了传统的页式存储管理机制

深入理解计算机系统-之-内存寻址(六)–linux中的分页机制, 详细的讲解了Linux内核分页机制的实现机制

 

3 Linux分页机制的演变


3.1 Linux的页表实现


由于程序存在局部化特征, 这意味着在特定的时间内只有部分内存会被频繁访问,具体点,进程空间中的text段(即程序代码), 堆, 共享库,栈都是固定在进程空间的某个特定部分,这样导致进程空间其实是非常稀疏的, 于是,从硬件层面开始,页表的实现就是采用分级页表的方式,Linux内核当然也这么做。所谓分级简单说就是,把整个进程空间分成区块,区块下面可以再细分,这样在内存中只要常驻某个区块的页表即可,这样可以大量节省内存。

 

3.2 Linux最初的二级页表


Linux最初是在一台i386机器上开发的,这种机器是典型的32位X86架构,支持两级页表

一个32位虚拟地址如上图划分。当在进行地址转换时,

结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd;

从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte;

在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。
从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk。

至于这种做法为什么能节省内存,举个更简单的例子更容易明白。比如要记录16个球场的使用情况,每张纸能记录4个场地的情况。采用4+4+4+4,共4张纸即可记录,但问题是球场使用得很少,有时候一整张纸记录的4个球场都没人使用。于是,采用4 x 4方案,即把16个球场分为4组,同样每张纸刚好能记录4组情况。这样,使用一张纸A来记录4个分组球场情况,当某个球场在使用时,只要额外使用多一张纸B来记录该球场,同时,在A上记录”某球场由纸B在记录”即可。这样在大部分球场使用很少的情况下,只要很少的纸即困记录,当有球场被使用,有需要再用额外的纸来记录,当不用就擦除。这里一个很重要的前提就是:局部性。

 

3.3 Linux的三级页表


当X86引入物理地址扩展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes被扩充到8 bytes,这意味着,每一页现在能存放的pte数目从1024变成512了(4k/8)。相应地,页表层级发生了变化,Linus新增加了一个层级,叫做页中间目录(page middle directory, PMD), 变成:

字段描述位数
cr3指向一个PDPTcrs寄存器存储
PGD指向PDPT中4个项中的一个位31~30
PMD指向页目录中512项中的一个位29~21
PTE指向页表中512项中的一个位20~12
page offset4KB页中的偏移位11~0

实际的page table walk依然类似,只不过多了一级

现在就同时存在2级页表和3级页表,在代码管理上肯定不方便。巧妙的是,Linux采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE。那只使用2级页表(如非PAE的X86)怎么办?

办法是针对使用2级页表的架构,把PMD抽象掉,即虚设一个PMD表项。这样在page table walk过程中,PGD本直接指向PTE的,现在不了,指向一个虚拟的PMD,然后再由PMD指向PTE。这种抽象保持了代码结构的统一。

 

3.4 Linux的四级页表


硬件在发展,3级页表很快又捉襟见肘了,原因是64位CPU出现了, 比如X86_64, 它的硬件是实实在在支持4级页表的。它支持48位的虚拟地址空间1。如下:

字段描述位数
PML4指向一个PDPT位47~39
PGD指向PDPT中4个项中的一个位38~30
PMD指向页目录中512项中的一个位29~21
PTE指向页表中512项中的一个位20~12
page offset4KB页中的偏移位11~0

Linux内核针为使用原来的3级列表(PGD->PMD->PTE),做了折衷。即采用一个唯一的,共享的顶级层次,叫PML4[2]。这个PML4没有编码在地址中,这样就能套用原来的3级列表方案了。不过代价就是,由于只有唯一的PML4, 寻址空间被局限在(239=)512G, 而本来PML4段有9位, 可以支持512个PML4表项的。现在为了使用3级列表方案,只能限制使用一个, 512G的空间很快就又不够用了,解决方案呼之欲出。

在2004年10月,当时的X86_64架构代码的维护者Andi Kleen提交了一个叫做4level page tables for Linux的PATCH系列,为Linux内核带来了4级页表的支持。在他的解决方案中,不出意料地,按照X86_64规范,新增了一个PML4的层级, 在这种解决方案中,X86_64拥一个有512条目的PML4, 512条目的PGD, 512条目的PMD, 512条目的PTE。对于仍使用3级目录的架构来说,它们依然拥有一个虚拟的PML4,相关的代码会在编译时被优化掉。 这样,就把Linux内核的3级列表扩充为4级列表。这系列PATCH工作得不错,不久被纳入Andrew Morton的-mm树接受测试。

不出意外的话,它将在v2.6.11版本中释出。但是,另一个知名开发者Nick Piggin提出了一些看法,他认为Andi的Patch很不错,不过他认为最好还是把PGD作为第一级目录,把新增加的层次放在中间,并给出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他认为Nick不过是玩了改名的游戏,而且他的PATCH经过测试很稳定,快被合并到主线了,不宜再折腾。

不过Linus却表达了对Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。毕竟作为Linux的扛把子,稳定对于Linus来说意义重大。

最终,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主线。在这种方案中,4级页表分别是:PGD -> PUD -> PMD -> PTE。

 

4 页式管理


4.1 分段机制存在的问题


分段,是指将程序所需要的内存空间大小的虚拟空间,通过映射机制映射到某个物理地址空间(映射的操作由硬件完成)。分段映射机制解决了之前操作系统存在的两个问题:

  • (1)地址空间没有隔离

  • (2)程序运行的地址不确定

不过分段方法存在一个严重的问题:内存的使用效率低。

分段的内存映射单位是整个程序;如果内存不足,被换入换出到磁盘的空间都是整个程序的所需空间,这会造成大量的磁盘访问操作,并且严重降低了运行速度.

事实上,很多时候程序运行所需要的数据只是很小的一部分,加入到内存的数据大小可能会很小,并没有必要整体的写入和写出.

分页机制解决了上面分段方法所存在的一个内存使用效率问题;其核心思想是系统为程序执行文件中的第x页分配了内存中的第y页,同时y页会添加到进程虚拟空间地址的映射表中(页表),这样程序就可以通过映射访问到内存页y了。

 

4.2 分页存储的基本内容


分页的基本方法是将地址空间人为地等分成某一个固定大小的页;每一页大小由硬件来决定,或者是由操作系统来决定(如果硬件支持多种大小的页)。目前,以大小为4KB的分页是绝大多数PC操作系统的选择.

  • 逻辑空间等分为页;并从0开始编号

  • 内存空间等分为块,与页面大小相同;从0开始编号

  • 分配内存时,以块为单位将进程中的若干个页分别装入

关于进程分页. 当我们把进程的虚拟地址空间按页来分割,常用的数据和代码会被装在到内存;暂时没用到的是数据和代码则保存在磁盘中,需要用到的时候,再从磁盘中加载到内存中即可.

这里需要了解三个概念:

  1. 虚拟页(VP, Virtual Page),虚拟空间中的页;

  2. 物理页(PP, Physical Page),物理内存中的页;

  3. 磁盘页(DP, Disk Page),磁盘中的页。

虚拟内存的实现需要硬件的支持,从Virtual Address到Physical Address的映射,通过一个叫MMU(Memory Mangement Unit)的部件来完成

 

5 分页机制支持


5.1 硬件分页支持


分页单元(paging unit)把线性地址转换成物理地址。其中的一个关键任务就是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。

  • :为了更高效和更经济的管理内存,线性地址被分为以固定长度为单位的组,成为页。页内部连续的线性地址空间被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和对应的存取权限,而不用指定全部线性地址的存取权限。这里说页,同时指一组线性地址以及这组地址包含的数据

  • 页框:分页单元把所有的 RAM 分成固定长度的页框(page frame)(有时叫做物理页)。每一个页框包含一个页(page),也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放在任何页框或磁盘中。

  • 页表:把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。

 

5.2 常规的32bit分页


常规4KB分页,32位的线性地址被分成3个域

Directory(目录)Table(页表)Offset(偏移量)
最高10位中间10位最低12位

 

 

线性地址的转换分为两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table)。

为什么需要两级呢?
目的在于减少每个进程页表所需的 RAM 的数量。如果使用简单的一级页表,将需要高达220220 个表项来表示每个进程的页表,即时一个进程并不使用所有的地址,二级模式通过职位进程实际使用的那些虚拟内存区请求页表来减少内存容量。

每个活动的进程必须有一个页目录,但是却没有必要马上为所有进程的所有页表都分配 RAM,只有在实际需要一个页表时候才给该页表分配 RAM。

页目录项和页表项有同样的结构,每项都包含下面的字段:

字段描述
Present标志如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。
 包含页框物理地址最高20位的字段。由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12位总为0.如果这个字段指向一个页目录,相应的页框就含有一个页表;如果它指向一个页表,相应的页框就含有一页数据
Accessed标志每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做
Dirty标志只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。
Read/Write标志含有页或页表的存取权限(Read/Write或Read)。
User/Supervisor标志含有访问页或页表所需的特权级。
PCD和PWT标志控制硬件高速缓存处理页或页表的方式。
Page Size标志只应用于页目录项。如果设置为1,则页目录项指的是2MB或4MB页框。
Global标志只应用于页表项。这个标志是在Pentium Pro中引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page Global Enable, PGE)标志置位时这个标志才起作用。

正在使用的页目录的物理地址存放在控制寄存器CR3中。

了解了以上结构之后,我们看看如何从线性地址转换到物理地址的 :

  • 线性地址中的 Directory 字段决定页目录中的目录项,目录项指向适当的页表

  • 线性地址中的 Table 字段又决定页表的页表项,页表项含有页所在页框的物理地址

  • 线性地址中的 Offset 地段决定了页框内的相对位置,由于 offset 为 12 为,所以一页含有 4096 字节的数据

Directory字段和Table字段都是10位长,因此页目录和页表都可以多达1024项。那么一个页目录可以寻址到高达1024*1024*4096=232个存储单元,这和32位地址所期望的一样。

5.3 物理地址扩展(PAE)分页机制和扩展分页(PSE)


处理器所支持的RAM容易受到连接到地址总线上的地址管脚树限制. 早期Intel处理器从80386到Pentium使用32位物理地址.

从理论上讲, 这样的系统可以使用高达2^32=4GB的RAM, 而实际上, 由于用户进程现行地址空间的需要, 4GB的虚拟地址按照1:3的比例划分给内核虚拟地址空间和进程虚拟地址空间. 则内核只能直接对1GB的线性地址空间进行寻址.

然而, 大型服务器需要大于4GB的RAM来同时运行数以钱计的进程, 所以必须扩展32位80x86架构所支持的RAM容量.

Intel通过在它的处理器上把管脚数从32增加到36满足这样的需要, 从Pentinum Pro开始, Intel所有处理器的寻址能力可达到2^36=64GB, 但是只有引入一种新的分页机制才能把32位现行地址转换为36位物理地址才能使用所增加的物理地址.

从Pentinum Pro处理器开始, Intel引入一种叫做物理地址扩展(Physical Address Extension, PAE)的机制.

从Pentium模型开始,80x86微处理器引入了扩展分页(externded paging),也叫页大小扩展[Page Size Extension], 它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这种情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。

但是Linux并没有采用这种机制

正如前面所述,通过设置页目录项的Page Size标志启用扩展分页功能。在这种情况下,分页单元把32位线性地址分成两个字段:
Directory:最高10位。
Offfset:其余22位。

扩展分页和正常分页的页目录项基本相同,除了
* Page Size标志必须被设置。
* 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0。

通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存

Intel为了支持PAE改变了分页机制

  • 64GB的RAM被分成了2^24个页框, 页表项的物理地址字段从20位扩展到了24位. 因为PAE页表项必须包含12个标志位和24个物理地址位, 总数之和为36, 页表项大小从32位扩展到了64位, 结果, 一个4KB的页表项包含512个表项而不是1024个表项

  • 引入一个页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别, 它由4个64位表项组成.

  • cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段. 因为PDPT存放在RAM的前4GB中, 并在32字节(2^5)的倍数上对其, 因此27位足以表示这种表的基地址

  • 当把线性地址映射到4KB的页时(页目录项中的PS标准清0), 32位线性地址将按照如下方式解释

字段描述位数
cr3指向一个PDPTcrs寄存器存储
PGD指向PDPT中4个项中的一个位31~30
PMD指向页目录中512项中的一个位29~21
PTE指向页表中512项中的一个位20~12
page offset4KB页中的偏移位11~0

* 当把现行地址映射到2MB的页时(页目录项中的PS标志置为1), 32位线性地址按照如下方式解释

字段描述位数
cr3指向一个PDPTcrs寄存器存储
PGD指向PDPT中4个项中的一个位31~30
PMD指向页目录中512项中的一个位29~21
page offset2MB页中的偏移位20~0

总之, 一旦cr3被设置, 就可能寻址高达4GB RAM, 如果我们期望堆更多的RAM进行寻址, 就必须在cr3中放置一个新值, 或改变PDPT的内容.

但是PAE的主要问题是线性地址仍然是32位长, 这就需要内核黑客用同一线性地址映射不同的RAM区. 很显然, PAE并没有扩大进程的线性地址空间, 因为它只处理物理地址. 此外, 只有内核能够修改进程的页表, 所以在用户态下运行的程序不可能使用大于4GB的物理地址空间. 另一方面, PAE允许内核使用容量高达64GB的RAM, 从而显著的增加系统中的进程数目

 

5.4 64位系统中的分页


32位处理器普遍采用两级分页。然而两级分页并不适用于采用64位系统的计算机。

原因如下 :

首先假设一个大小为4KB的标准页,4KB覆盖2^12个地址,所以offset字段是12位。如果我们现在决定仅仅使用64位中的48位来寻址(这个限制仍然能是我们自在地拥有256TB的寻址空间!),剩下的48-12=36位被分配给Table和Directory字段。如果我们决定为两个字段个预留18位,那么每个进程的页目录和页表都含有2^18个项,即超过256000个项。

由于这个原因,所有64位处理器的硬件分页系统都使用了额外的分页级别。使用的级别数量取决于处理器的类型。

平台名称页大小寻址使用位数分页级别线性地址分级
alpha8KB43310+10+10+13
ia644KB3939+9+9+12
ppc644KB41310+10+9+12
x86_644KB4849+9+9+9+12

注:ia64是intel的一门高端技术,不与x86_64系统兼容
IA-32e Paging机制下线性地址映射到4KB的页

 

5.5 硬件保护方案

与页和页表相关的特权级只有两个,因为特权由前面“常规分页”一节中所提到的User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。

此外,与段的3种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。

 

6 部分总结


80386 使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。

 

6.1 为什么使用多级页表


假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。

 

6.2 x86_32两级页表结构


两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。

两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10 位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。

  • 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。

  • 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。

  • 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。

  • 第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式

  • 第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。

  • 第5位是访问位,当对页目录项进行访问时,A位=1。

  • 第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。

  • 第9~11位由操作系统专用,Linux也没有做特殊之用。

80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用.

第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1.

4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB.

 

6.3 线性地址到物理地址的转换


  • 1.CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。

  • 2.从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。

  • 3.将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。

 

6.4 扩展分页


从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB

在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。

 

6.5 页面高速缓存


由于在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,为了提高速度,在386中设置一个最近存取页面的高速缓存硬件机制,它 自动保持32项处理器最近使用的页面地址,因此,可以覆盖128K字节的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓存中,如果在, 就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有98%的命中率,也就是说每次访问存储器时,只有2%的情况必须访问两级分页机构。这就大大加快了速度.

 

7 linux的分页机制


7.1 四级分页机制


前面我们提到Linux内核仅使用了较少的分段机制,但是却对分页机制的依赖性很强,其使用一种适合32位和64位结构的通用分页模型,该模型使用四级分页机制,即

  • 页全局目录(Page Global Directory)
  • 页上级目录(Page Upper Directory)
  • 页中间目录(Page Middle Directory)
  • 页表(Page Table)
  • 页全局目录包含若干页上级目录的地址;
  • 页上级目录又依次包含若干页中间目录的地址;
  • 而页中间目录又包含若干页表的地址;
  • 每一个页表项指向一个页框。
    因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。

7.3 不同架构的分页机制


对于不同的体系结构,Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。

对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。

启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。

最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。

 

7.4 为什么linux热衷:分页>分段


那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:

  • 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
  • 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。

每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。

把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。

 

8 linux中页表处理数据结构


8.1 页表类型定义pgd_t、pmd_t、pud_t和pte_t


Linux分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种 数据结构本质上都是无符号长整型unsigned long

Linux为了更严格数据类型检查,将无符号长整型unsigned long分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。

pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。

首先我们查看一下子这些类型是如何定义的

 

8.1.1 pteval_t,pmdval_t,pudval_t,pgdval_t


参照arch/x86/include/asm/pgtable_64_types.h

#ifndef __ASSEMBLY__
#include <linux/types.h>

/*
 * These are used to make use of C type-checking..
 */
typedef unsigned long   pteval_t;
typedef unsigned long   pmdval_t;
typedef unsigned long   pudval_t;
typedef unsigned long   pgdval_t;
typedef unsigned long   pgprotval_t;

typedef struct { pteval_t pte; } pte_t;

#endif  /* !__ASSEMBLY__ */

8.1.2 pgd_t、pmd_t、pud_t和pte_t


参照 /arch/x86/include/asm/pgtable_types.h

typedef struct { pgdval_t pgd; } pgd_t;

static inline pgd_t native_make_pgd(pgdval_t val)
{
        return (pgd_t) { val };
}

static inline pgdval_t native_pgd_val(pgd_t pgd)
{
        return pgd.pgd;
}

static inline pgdval_t pgd_flags(pgd_t pgd)
{
        return native_pgd_val(pgd) & PTE_FLAGS_MASK;
}

#if CONFIG_PGTABLE_LEVELS > 3
typedef struct { pudval_t pud; } pud_t;

static inline pud_t native_make_pud(pmdval_t val)
{
        return (pud_t) { val };
}

static inline pudval_t native_pud_val(pud_t pud)
{
        return pud.pud;
}
#else
#include <asm-generic/pgtable-nopud.h>

static inline pudval_t native_pud_val(pud_t pud)
{
        return native_pgd_val(pud.pgd);
}
#endif

#if CONFIG_PGTABLE_LEVELS > 2
typedef struct { pmdval_t pmd; } pmd_t;

static inline pmd_t native_make_pmd(pmdval_t val)
{
        return (pmd_t) { val };
}

static inline pmdval_t native_pmd_val(pmd_t pmd)
{
        return pmd.pmd;
}
#else
#include <asm-generic/pgtable-nopmd.h>

static inline pmdval_t native_pmd_val(pmd_t pmd)
{
        return native_pgd_val(pmd.pud.pgd);
}
#endif

static inline pudval_t pud_pfn_mask(pud_t pud)
{
        if (native_pud_val(pud) & _PAGE_PSE)
                return PHYSICAL_PUD_PAGE_MASK;
        else
                return PTE_PFN_MASK;
}

static inline pudval_t pud_flags_mask(pud_t pud)
{
        return ~pud_pfn_mask(pud);
}

static inline pudval_t pud_flags(pud_t pud)
{
        return native_pud_val(pud) & pud_flags_mask(pud);
}

static inline pmdval_t pmd_pfn_mask(pmd_t pmd)
{
        if (native_pmd_val(pmd) & _PAGE_PSE)
                return PHYSICAL_PMD_PAGE_MASK;
        else
                return PTE_PFN_MASK;
}

static inline pmdval_t pmd_flags_mask(pmd_t pmd)
{
        return ~pmd_pfn_mask(pmd);
}

static inline pmdval_t pmd_flags(pmd_t pmd)
{
        return native_pmd_val(pmd) & pmd_flags_mask(pmd);
}

static inline pte_t native_make_pte(pteval_t val)
{
        return (pte_t) { .pte = val };
}

static inline pteval_t native_pte_val(pte_t pte)
{
        return pte.pte;
}

static inline pteval_t pte_flags(pte_t pte)
{
        return native_pte_val(pte) & PTE_FLAGS_MASK;
}

8.1.4 xxx_val和__xxx


参照/arch/x86/include/asm/pgtable.h

五个类型转换宏(_ pte、_ pmd、_ pud、_ pgd和__ pgprot)把一个无符号整数转换成所需的类型。

另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。

#define pgd_val(x)      native_pgd_val(x)
#define __pgd(x)        native_make_pgd(x)

#ifndef __PAGETABLE_PUD_FOLDED
#define pud_val(x)      native_pud_val(x)
#define __pud(x)        native_make_pud(x)
#endif

#ifndef __PAGETABLE_PMD_FOLDED
#define pmd_val(x)      native_pmd_val(x)
#define __pmd(x)        native_make_pmd(x)
#endif

#define pte_val(x)      native_pte_val(x)
#define __pte(x)        native_make_pte(x)

这里需要区别指向页表项的指针和页表项所代表的数据。以pgd_t类型为例子,如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。

 

8.2 页表描述宏


参照arch/x86/include/asm/pgtable_64
linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏:

宏字段描述
XXX_SHIFT指定Offset字段的位数
XXX_SIZE页的大小
XXX_MASK用以屏蔽Offset字段的所有位。

我们的四级页表,对应的宏分别由PAGE,PMD,PUD,PGDIR

宏字段前缀描述
PGDIR页全局目录(Page Global Directory)
PUD页上级目录(Page Upper Directory)
PMD页中间目录(Page Middle Directory)
PAGE页表(Page Table)

8.2.1 PAGE宏–页表(Page Table)


字段描述
PAGE_SHIFT指定Offset字段的位数
PAGE_SIZE页的大小
PAGE_MASK用以屏蔽Offset字段的所有位。

定义如下,在/arch/x86/include/asm/page_types.h 文件中

/* PAGE_SHIFT determines the page size */
 #define PAGE_SHIFT      12
 #define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
 #define PAGE_MASK       (~(PAGE_SIZE-1))

当用于80x86处理器时,PAGE_SHIFT返回的值为12。
由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小PAGE_SIZE是212=4096212=4096字节。
PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。

8.2.2 PMD-Page Middle Directory (页目录)


字段描述
PMD_SHIFT指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数
PMD_SIZE用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小
PMD_MASK用于屏蔽Offset字段与Table字段的所有位

当PAE 被禁用时,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位),
PMD_SIZE 产生的值为222 或 4 MB,
PMD_MASK产生的值为 0xffc00000。

相反,当PAE被激活时
PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位),
PMD_SIZE 产生的值为221221 或2 MB
PMD_MASK产生的值为 0xffe00000。

大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。

8.2.3 PUD_SHIFT-页上级目录(Page Upper Directory)


字段描述
PUD_SHIFT确定页上级目录项能映射的区域大小的位数
PUD_SIZE用于计算页全局目录中的一个单独表项所能映射的区域大小。
PUD_MASK用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位

在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。

8.2.4 PGDIR_SHIFT-页全局目录(Page Global Directory)


字段描述
PGDIR_SHIFT确定页全局页目录项能映射的区域大小的位数
PGDIR_SIZE用于计算页全局目录中一个单独表项所能映射区域的大小
PGDIR_MASK用于屏蔽Offset, Table,Middle Air及Upper Air的所有位

当PAE 被禁止时
PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同),
PGDIR_SIZE 产生的值为 222 或 4 MB,
PGDIR_MASK 产生的值为 0xffc00000。

相反,当PAE被激活时
PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air),
PGDIR_SIZE 产生的值为230 或 1 GB
PGDIR_MASK产生的值为0xc0000000

PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD

用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。

 

8.3 页表处理函数


[注意]
以下内容主要参见 深入理解linux内核第二章内存寻址中页表处理

内核还提供了许多宏和函数用于读或修改页表表项:

  • 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。

  • 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。

  • set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。

  • 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。

  • 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。

宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1:

  • 页不在主存中(Present标志被清除)。

  • 页只允许读访问(Read/Write标志被清除)。

  • Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是
    强制设置这些标志)。

pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。

如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。

前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。

这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。

如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。

 

8.3.1 查询页表项中任意一个标志的当前值


下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。

函数名称说明
pte_user( )读 User/Supervisor 标志
pte_read( )读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)
pte_write( )读 Read/Write 标志
pte_exec( )读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)
pte_dirty( )读 Dirty 标志
pte_young( )读 Accessed 标志
pte_file( )读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)

8.3.2 设置页表项中各标志的值


下表列出的另一组函数用于设置页表项中各标志的值

函数名称说明
mk_pte_huge( )设置页表项中的 Page Size 和 Present 标志
pte_wrprotect( )清除 Read/Write 标志
pte_rdprotect( )清除 User/Supervisor 标志
pte_exprotect( )清除 User/Supervisor 标志
pte_mkwrite( )设置 Read/Write 标志
pte_mkread( )设置 User/Supervisor 标志
pte_mkexec( )设置 User/Supervisor 标志
pte_mkclean( )清除 Dirty 标志
pte_mkdirty( )设置 Dirty 标志
pte_mkold( )清除 Accessed 标志(把此页标记为未访问)
pte_mkyoung( )设置 Accessed 标志(把此页标记为访问过)
pte_modify(p,v)把页表项 p 的所有访问权限设置为指定的值
ptep_set_wrprotect()与 pte_wrprotect( ) 类似,但作用于指向页表项的指针
ptep_set_access_flags( )如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数
ptep_mkdirty()与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。
ptep_test_and_clear_dirty( )与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值
ptep_test_and_clear_young( )与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值

8.3.3 宏函数-把一个页地址和一组保护标志组合成页表项,或者执行相反的操作


现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。

宏名称说明
pgd_index(addr)找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)
pgd_offset(mm, addr)接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录
pgd_offset_k(addr)产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr
pgd_page(pgd)通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项
pud_offset(pgd, addr)参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址
pud_page(pud)通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项
pmd_index(addr)产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)
pmd_offset(pud, addr)接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址
pmd_page(pmd)通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项
mk_pte(p,prot)接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项
pte_index(addr)产生线性地址 addr 对应的表项在页表中的索引(相对位置)
pte_offset_kernel(dir,addr)线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用
pte_offset_map(dir, addr)接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射
pte_page( x )返回页表项 x 所引用页的描述符地址
pte_to_pgoff( pte )从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页
pgoff_to_pte(offset )为非线性文件内存映射所在的页创建对应页表项的内容

8.3.4 简化页表项的创建和撤消


下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。

如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。

函数名称说明
pgd_alloc( mm )分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略
pgd_free( pgd)释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录
pud_alloc(mm, pgd, addr)在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址
pud_free(x)在两级或三级分页系统下,这个宏什么也不做
pmd_alloc(mm, pud, addr)定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略
pmd_free(x)该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的
pte_alloc_map(mm, pmd, addr)接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放
pte_alloc_kernel(mm, pmd, addr)如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用
pte_free(pte)释放与页描述符指针 pte 相关的页表
pte_free_kernel(pte)等价于 pte_free( ) ,但由主内核页表使用
clear_page_range(mmu, start,end)从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容

9 线性地址转换


9.1 分页模式下的的线性地址转换


线性地址、页表和页表项线性地址不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图:

在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。

9.1.1 页表


多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

9.1.2 页表项


页表项从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下:

这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。

不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。

9.1.3 地址转换过程


地址转换过程有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下:

  • 1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。

  • 2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。

  • 3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。

  • 4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。

  • 5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。

  • 6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。

  • 7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。

  • 8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。

  • 9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。

  • 10.第五次读取内存得到最终要访问的数据。

整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

9.2 Linux中通过4级页表访问物理内存


linux中每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。

进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd;

PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。

前面我们讲了页表处理的大多数函数信息,在上面我们又讲了线性地址如何转换为物理地址,其实就是不断索引的过程。

通过如下几个函数,不断向下索引,就可以从进程的页表中搜索特定地址对应的页面对象

宏函数说明
pgd_offset根据当前虚拟地址和当前进程的mm_struct获取pgd项
pud_offset参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址
pmd_offset根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)
pte_offset根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)

根据虚拟地址获取物理页的示例代码详见mm/memory.c中的函数follow_page

不同的版本可能有所不同,早起内核中存在follow_page,而后来的内核中被follow_page_mask替代,目前最新的发布4.4中为查找到此函数

我们从早期的linux-3.8的源代码中, 截取的代码如下

/**
 * follow_page - look up a page descriptor from a user-virtual address
 * @vma: vm_area_struct mapping @address
 * @address: virtual address to look up
 * @flags: flags modifying lookup behaviour
 *
 * @flags can have FOLL_ flags set, defined in <linux/mm.h>
 *
 * Returns the mapped (struct page *), %NULL if no mapping exists, or
 * an error pointer if there is a mapping to something not represented
 * by a page descriptor (see also vm_normal_page()).
 */
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
            unsigned int flags)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *ptep, pte;
    spinlock_t *ptl;
    struct page *page;
    struct mm_struct *mm = vma->vm_mm;

    page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
    if (!IS_ERR(page)) {
        BUG_ON(flags & FOLL_GET);
        goto out;
    }

    page = NULL;
    pgd = pgd_offset(mm, address);
    if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
        goto no_page_table;

    pud = pud_offset(pgd, address);
    if (pud_none(*pud))
        goto no_page_table;
    if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
        BUG_ON(flags & FOLL_GET);
        page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
        goto out;
    }
    if (unlikely(pud_bad(*pud)))
        goto no_page_table;

    pmd = pmd_offset(pud, address);
    if (pmd_none(*pmd))
        goto no_page_table;
    if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
        BUG_ON(flags & FOLL_GET);
        page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
        goto out;
    }
    if (pmd_trans_huge(*pmd)) {
        if (flags & FOLL_SPLIT) {
            split_huge_page_pmd(mm, pmd);
            goto split_fallthrough;
        }
        spin_lock(&mm->page_table_lock);
        if (likely(pmd_trans_huge(*pmd))) {
            if (unlikely(pmd_trans_splitting(*pmd))) {
                spin_unlock(&mm->page_table_lock);
                wait_split_huge_page(vma->anon_vma, pmd);
            } else {
                page = follow_trans_huge_pmd(mm, address,
                                 pmd, flags);
                spin_unlock(&mm->page_table_lock);
                goto out;
            }
        } else
            spin_unlock(&mm->page_table_lock);
        /* fall through */
    }
split_fallthrough:
    if (unlikely(pmd_bad(*pmd)))
        goto no_page_table;

    ptep = pte_offset_map_lock(mm, pmd, address, &ptl);

    pte = *ptep;
    if (!pte_present(pte))
        goto no_page;
    if ((flags & FOLL_WRITE) && !pte_write(pte))
        goto unlock;

    page = vm_normal_page(vma, address, pte);
    if (unlikely(!page)) {
        if ((flags & FOLL_DUMP) ||
            !is_zero_pfn(pte_pfn(pte)))
            goto bad_page;
        page = pte_page(pte);
    }

    if (flags & FOLL_GET)
        get_page(page);
    if (flags & FOLL_TOUCH) {
        if ((flags & FOLL_WRITE) &&
            !pte_dirty(pte) && !PageDirty(page))
            set_page_dirty(page);
        /*
         * pte_mkyoung() would be more correct here, but atomic care
         * is needed to avoid losing the dirty bit: it is easier to use
         * mark_page_accessed().
         */
        mark_page_accessed(page);
    }
    if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
        /*
         * The preliminary mapping check is mainly to avoid the
         * pointless overhead of lock_page on the ZERO_PAGE
         * which might bounce very badly if there is contention.
         *
         * If the page is already locked, we don't need to
         * handle it now - vmscan will handle it later if and
         * when it attempts to reclaim the page.
         */
        if (page->mapping && trylock_page(page)) {
            lru_add_drain();  /* push cached pages to LRU */
            /*
             * Because we lock page here and migration is
             * blocked by the pte's page reference, we need
             * only check for file-cache page truncation.
             */
            if (page->mapping)
                mlock_vma_page(page);
            unlock_page(page);
        }
    }
unlock:
    pte_unmap_unlock(ptep, ptl);
out:
    return page;

bad_page:
    pte_unmap_unlock(ptep, ptl);
    return ERR_PTR(-EFAULT);

no_page:
    pte_unmap_unlock(ptep, ptl);
    if (!pte_none(pte))
        return page;

no_page_table:
    /*
     * When core dumping an enormous anonymous area that nobody
     * has touched so far, we don't want to allocate unnecessary pages or
     * page tables.  Return error instead of NULL to skip handle_mm_fault,
     * then get_dump_page() will return NULL to leave a hole in the dump.
     * But we can only make this optimization where a hole would surely
     * be zero-filled if handle_mm_fault() actually did handle it.
     */
    if ((flags & FOLL_DUMP) &&
        (!vma->vm_ops || !vma->vm_ops->fault))
        return ERR_PTR(-EFAULT);
    return page;
}

以上代码可以精简为

unsigned long v2p(int pid unsigned long va)
{
        unsigned long pa = 0;
        struct task_struct *pcb_tmp = NULL;
        pgd_t *pgd_tmp = NULL;
        pud_t *pud_tmp = NULL;
        pmd_t *pmd_tmp = NULL;
        pte_t *pte_tmp = NULL;

        printk(KERN_INFO"PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);
        printk(KERN_INFO"PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
        printk(KERN_INFO"PUD_SHIFT = %d\n",PUD_SHIFT);
        printk(KERN_INFO"PMD_SHIFT = %d\n",PMD_SHIFT);
        printk(KERN_INFO"PAGE_SHIFT = %d\n",PAGE_SHIFT);

        printk(KERN_INFO"PTRS_PER_PGD = %d\n",PTRS_PER_PGD);
        printk(KERN_INFO"PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
        printk(KERN_INFO"PTRS_PER_PMD = %d\n",PTRS_PER_PMD);
        printk(KERN_INFO"PTRS_PER_PTE = %d\n",PTRS_PER_PTE);

        printk(KERN_INFO"PAGE_MASK = 0x%lx\n",PAGE_MASK);

        //if(!(pcb_tmp = find_task_by_pid(pid)))
        if(!(pcb_tmp = findTaskByPid(pid)))
        {
                printk(KERN_INFO"Can't find the task %d .\n",pid);
                return 0;
        }
        printk(KERN_INFO"pgd = 0x%p\n",pcb_tmp->mm->pgd);

        /* 判断给出的地址va是否合法(va&lt;vm_end)*/
        if(!find_vma(pcb_tmp->mm,va))
        {
                printk(KERN_INFO"virt_addr 0x%lx not available.\n",va);
                return 0;
        }

        pgd_tmp = pgd_offset(pcb_tmp->mm,va);
        printk(KERN_INFO"pgd_tmp = 0x%p\n",pgd_tmp);
        printk(KERN_INFO"pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
        if(pgd_none(*pgd_tmp))
        {
                printk(KERN_INFO"Not mapped in pgd.\n");
                return 0;
        }

        pud_tmp = pud_offset(pgd_tmp,va);
        printk(KERN_INFO"pud_tmp = 0x%p\n",pud_tmp);
        printk(KERN_INFO"pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
        if(pud_none(*pud_tmp))
        {
                printk(KERN_INFO"Not mapped in pud.\n");
                return 0;
        }

        pmd_tmp = pmd_offset(pud_tmp,va);
        printk(KERN_INFO"pmd_tmp = 0x%p\n",pmd_tmp);
        printk(KERN_INFO"pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
        if(pmd_none(*pmd_tmp))
        {
                printk(KERN_INFO"Not mapped in pmd.\n");
                return 0;
        }

        /*在这里,把原来的pte_offset_map()改成了pte_offset_kernel*/
        pte_tmp = pte_offset_kernel(pmd_tmp,va);

        printk(KERN_INFO"pte_tmp = 0x%p\n",pte_tmp);
        printk(KERN_INFO"pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
        if(pte_none(*pte_tmp))
        {
                printk(KERN_INFO"Not mapped in pte.\n");
                return 0;
        }
        if(!pte_present(*pte_tmp)){
                printk(KERN_INFO"pte not in RAM.\n");
                return 0;
        }

        pa = (pte_val(*pte_tmp) & PAGE_MASK) | (va & ~PAGE_MASK);
        printk(KERN_INFO"virt_addr 0x%lx in RAM is 0x%lx t .\n",va,pa);
        printk(KERN_INFO"contect in 0x%lx is 0x%lx\n", pa, *(unsigned long *)((char *)pa + PAGE_OFFSET)
}

 

相关链接


我对linux内核四级分页理解

Linux内核4级页表的演进

Linux内存 之 页表

内存管理(四) 页表数据结构

Linux内存管理之我见(二)-页表、页式内存管理机制

Linux分页机制之概述--Linux内存管理(六)

Linux分页机制之分页机制的演变--Linux内存管理(七)

Linux分页机制之分页机制的实现详解--Linux内存管理(八)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页