《深入理解Linux内核》-2.4. 硬件分页

分页单元用来把线性地址转换成物理地址。它的一个主要的任务就是根据线性地址的访问权限检查请求的访问类型。如果访问的内存不合法,它会产生一个页面错误异常(参考第4章和第8章)。

出于性能考虑,线性地址被分成固定大小的组,叫做页面;一个页面中连续的线性地址被映射到连续的物理地址上。如此,内核可以对一个页面进行权限控制,而不需要针对其中的每个线性地址。一般情况下,我们说页面时,就是指一个线性地址集合和这些地址包含的数据。

分页单元把所有RAM分成固定长度的页帧(物理页)。每个页帧包含一个页面,也就是页帧的大小和页面大小一样。页帧是主内存的组成部分,因此也是一块存储区域。区分页帧和页面很重要,页面是一块数据,它被存储在任意一个页帧上或者磁盘上。

把线性地址映射到物理地址的数据结构叫做页表;它存储在主存中,并被内核初始化(启用分页单元的情况下)。

从80386开始,所有80x86处理器支持分页;通过设置cr0寄存器的PG标志位可以启用它。当PG=0时,线性地址等于物理地址。

2.4.1. 常规分页

从80386开始,页面的大小为4KB。

32位线性地址被分成三部分:

  • 目录

    最高10位有效位

  • 页表

    中间10位

  • 偏移

    最低12位有效位

线性地址的转换分为两步,每一步基于一种转换表。第一个转换表叫做页目录,第二个叫做页表。

采用这种两级结构的目的是减少每个CPU的页表使用的RAM内存。如果只有一级页表,那么每个CPU的页表将需要2^20个节点(比如,线性地址空间大小是4KB,每个页表节点4字节,则页表总共占用4MB内存),即使进程并没有使用那么多的内存。两级结构的策略是只为进程实际使用的内存区域建立页表,以此减少内存占用。

每个活动的进程都有一个页目录。这些页目录不是一开始就全部分配,而是在进程实际需要的时候才被分配。

当前页目录的物理地址存储在c3寄存器中。线性地址中的目录部分指向页目录节点,这个节点用来定位页表。找到页表后,再使用线性地址中的表字段来定位页表节点,这个节点包含相应页帧的物理地址。偏移字段则决定了数据在页帧中的实际位置(看图2-7)。因为偏移长度是12位,所以每个页面包含4096字节数据。

图2-7. 80x86处理器的分页

这里写图片描述

由于目录和表字段的长度都是10比特,因此它们可以容纳1024个节点,进而,一个页目录可以定位的地址数目为1024 * 1024 * 4096 = 2^32个内存元,这正是32位地址所能表示的。

也目录和页表具有相同的结构。每个节点包含以下字段:

  • Present 标志

    当为1时,页面在主存中;为0时,表示页面不在主存中,该节点剩下的比特位被操作系统用作其他目的。当一个Present为0的页表或者页目录需要做地址转换时,分页单元把线性地址存储在寄存器cr2中,并产生一个异常14:页面错误异常。(参考17章关于Linux如何使用这个字段)

  • 包含页帧物理地址高20有效位的字段

    因为页帧大小为4KB,它的物理地址必须是4096的倍数,因此它的低12有效位总是0。如果这个字段在页目录中,则对应的页帧包含一个页表;如果是页表中的字段,对应的页帧包含一个页面的数据。

  • Accessed 标志

    每当分页单元寻址到对应的页帧时,它被设置为1。这个标志位在操作系统换出页面的时候会用到。分页单元从不清除它,它必须由操作系统来清除。

  • Dirty 标志

    仅页表节点会用到。页帧上有写操作时,它被设置为1。跟Accessed标志一样,Dirty会被操作系统在换出页面的时候用到。分页单元从不清除它,它必须由操作系统来清除。

  • Read/Write 标志

    包含页面或页表的访问权限(读写或者只读)(参考本节之后的“硬件保护结构”一段)。

  • User/Supervisor 标志

    包含需要访问页或者页表的特权级别(参考本节之后的“硬件保护结构”一段)。

  • PCD和PWT 标志

    控制硬件缓存处理页面或者页表的方式(参考本节之后的“硬件缓存”一段)。

  • Page size 标志

    仅对页目录有效。当其为1时,该节点指向一个2MB或者4MB大小的页帧(参考后面段落)。

  • Global 标志

    仅对页表有效。Pentium Pro引入它来防止频繁使用的页面被刷出TLB缓存(参考本章之后的“转换查找缓存(TLB)”一段)。

2.4.2. 扩展分页

从奔腾处理器开始,80x86微处理器引入扩展分页,它允许页帧的大小为4MB,而不是4KB(看图2-8)。扩展分页用来把大的连续线性地址转换成对应的物理地址;这时,内核不需要中间页表,从而节省了内存和TLB节点(参考“块表(TLB)”一段)。

图2-8. 扩展分页

这里写图片描述

扩展分页的启用是通过设置页目录节点的Page Size标志位来的,此时,分页单元把32位线性地址化分成两部分:

  • 目录

    高10位有效位

  • 偏移

    剩下的22位

扩展分页的页目录节点和普通分页的一样,除了以下两点:

  • Page Size标志必须被设置
  • 20位物理地址只有高10位是有效的。这是因为每个物理地址按4MB对齐,所以22个最低有效位总是0

扩展分页和常规分页共存,通过设置cr4寄存器的PSE标志可以启用扩展分页。

2.4.3. 硬件保护体系

分页单元采用一种和分段单元不同的保护体系。80x86处理器中,分段需要四个特权等级,而分页单元只需要两个,它们由之前提到的User/Supervisor标志位控制。当它为0时,页面仅可被内核态进程访问,为1时,页面总是可以被访问。

除此之外,分段系统使用三种访问权限(读、写、执行),而分页系统只使用两种访问权限(读和写)。Read/Write标志位0时,页面只读,否则可以读写。

2.4.4. 常规分页的一个例子

一个简单的例子有利于我们更好的理解常规分页是怎么工作的,假设内核给进程分配0x20000000和0x2003ffff之间的地址空间,包含64个页面。我们不关心页面对应页帧的物理地址;实际上,它们甚至不一定在主存中,这里,我们只关心页表节点的其他字段。

我们首先看线性地址的10个最高有效位,它的值为0x80(128),因此,它指向页目录的第129个节点,这个节点里面包含页表的物理地址(参考图2-9)。如果进程没有分配其他线性地址,页目录剩下的节点全部为0。

图2-9. 分页示例

这里写图片描述

我们假定的地址的中间10位(线性地址中的表字段)的范围从0到0x03f,所以只有前64个页表节点是有效的,剩下的以0填充。

假设我们现在需要读取线性地址0x20021406处的内容,分页单元的处理逻辑如下:

1、目录字段0x80用来选择页目录的第0x80号节点,它指向相应的页表;

2、表字段0x21用来选择页表的第0x21号节点,它指向包含目标页的页帧;

3、最后,偏移字段0x406用来选择页帧中0x406处的字节。

如果页表0x21处节点的Present标志被清除,说明对应页面不在主存中,此时页面单元发出一个页面错误异常。当进程尝试访问0x20000000和0x2003ffff范围之外的地址时,也会导致同样的异常,因为页表中的其他节点都是0,尤其是它们的Present标志位为0。

2.4.5. 物理地址扩展(PAE)分页机制

一个处理器可以支持的最大RAM受连接到地址总线的针脚数限制。老的Intel处理器,从80386到奔腾都使用32位物理地址。理论上这些系统可以使用最多4GB RAM;实际上,由于用户态线性地址空间的要求,内核不能直接定位超过1GB的RAM,下一节“Linux分页”我们会看到为什么。

然而,那些同时运行成千上万个进程的大服务器需要比4GB更多的内存,这迫使Intel扩展32位80x86架构能支持的内存数。

Intel通过把处理器地址针脚从32增加到36来满足这些需求。从高能奔腾(Pentium Pro)开始,所有Intel处理器能够最大寻址2^36=64GB内存。这时候,需要一种新的分页机制来吧32位的线性地址转换成36位物理地址。

对于高能奔腾处理器,Intel引入一种叫做物理地址扩展(PAE)的机制。另一种机制是,页面大小扩展(PSE-36),在奔腾三处理器上被采用,但是Linux没有使用它,我们也不打算在本书介绍。

PAE通过设置cr4寄存器中的PAE标志来启用。页目录的Page Size标志支持更大的页大小(当PAE启用时,是2MB)。

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

  • 64GB内存被分成2^24个页帧,页帧大小保持不变,页表中存储的物理地址从20位扩展到24位。因为PAE页表必须包含12个标志位(偏移)和24个物理地址位,所以页表节点必须从32位扩展到64位。这个相应的导致了一个4KB的PAE页表(一个页帧中的页表)只能包含512个节点而不是1024个节点。
  • 增加了新的一级页表,叫做页目录指针表(PDPT),每个节点64位。(译者注:页目录本来是4KB,一个页帧就能保存,不需要查找页目录,现在增加到四个页目录,需要通过PDPT的方式来定位页目录)
  • cr3寄存器包含27位的页目录指针表基地址。因为PDPT存储在RAM的前4GB内存中,并且按32字节对齐,27位足够表示PDPT的基地址。
  • 当映射一个4KB页面的线性地址时,32位线性地址按如下方式翻译:

    cr3:指向PDPT

    31, 30位:指向PDPT中四个可能的节点

    29-21位:指向页目录中512个中的一个节点

    20-12位: 指向页表中512个中的一个节点

    11-0位:4KB页面偏移

  • 映射2MB页面时,32位线性地址翻译方式如下:

    cr3:指向PDPT

    31, 30位:指向PDPT中四个可能的节点

    29-21位:指向页目录中512个中的一个节点

    20-0位:2MB页面偏移

总而言之,一旦cr3被设置,它可以寻址4GB内存。当我们想要寻得更多地址时,我们必须更改cr3或者PDPT的值。然而,PAE的主要问题是线性地址仍然是32位,内核程序员必须重用相同的线性地址来映射不同的内存。我们会在之后的章节概述Linux在PAE启用时是怎样初始化页表的,“内存大于4GB时的最终内核页表”。显然,PAE没有增大单个进程可用的内存空间,因为它只处理物理地址。此外,只有内核可以修改进程的页表,所以用户态进程不能使用超过4GB的内存。另一方面,PAE却允许内核使用最大64GB的内存,从而有效增加了系统可以运行的进程数。

2.4.6. 64位架构上的分页

通过前文的学习,我们知道32位处理器使用两级页表,但在64位架构上却不适合。我们从理论上来解释为什么:

假设页表大小为4KB,所有线性地址的偏移字段必须为12位,剩下52位给页表和页目录。如果我们只使用64位中的48位(这个限制刚好给我们很舒服的256TB内存)来寻址,剩下的48-12=36位用作页表和页目录字段。如果各分一半,18位,那么每个进程的页表和页目录都拥有2^18个节点,超过256000个节点(总共4MB)。

为了节省内存,所有64位分页系统使用更多的分页层级。层级的数量取决于处理器类型。表2-4总结了一些Linux支持的64位平台分页层次数。

表2-4. 一些64位系统分页层级

这里写图片描述

Linux系统提供一种通用的分页模型,可以支持大部分硬件系统,我们在下一节“Linux分页”会讲到。

2.4.7. 硬件缓存

现代微处理器的时钟频率可以达到几个GB,而RAM(DRAM)芯片的访问时间在几百个时钟周期左右。这意味着当执行需要访问RAM的指令时,CPU会等待。

因此引入硬件缓存来减少CPU和RAM的速度不匹配问题。这种方法基于一种“局部原则”,对程序和数据均有效。“局部原则”认为,由于程序的循环结构和相关的数据往往被打包存储到线性数组,跟最近使用的内存临近的地址有极大可能很快被使用。因此,引入一个更小更快的内存来存放最近使用的代码和数据是有意义的。80x86为此增加了一个新的硬件单元叫做行(line)。它由一些批连续的字节组成,这些数据在较慢的DRAM和较快的用来实现缓存的SRAM之间传输。

这个缓存被分成几个行子集。极端情况下,一个子集一行,缓存可以做到直接映射,也就是内存中的一行总是存储在缓存中相同位置。另一个极端情况,缓存是全相联的,即内存中的任何行可以存储到缓存中的任意位置。但是,大部分的缓存使用N路相联,内存中的任意行可以被存储在缓存中特定N行的任意位置。

如图2-10,缓存单元被插在分页单元和主存之间。它包含一个硬件缓存存储和一个缓存控制器。缓存存储保存实际的数据,缓存控制器存储一些节点,每个节点对应缓存存储中的一行。每个节点包含一个tag和一些描述缓存状态的标志位。tag允许缓存控制器识别映射到缓存中的内存位置。物理地址被分成三部分:高位表示tag,中间对应缓存控制器子索引,低位表示偏移。

图2-10. 处理器硬件缓存

这里写图片描述

当访问一个RAM内存单元时,CPU提取物理地址的子索引字段,并拿高位tag和这个子集中的所有行的tag作比较,匹配到则缓存命中,否则缓存未命中。

缓存命中时,缓存控制器对不同的访问类型有不同的流程。对于读操作,控制器直接从缓存中取出数据并传输给CPU寄存器,不用访问RAM,因而节省了时间。对于写操作,控制器有两种策略:直写和回写。直写就是同时写RAM和缓存,这个过程中禁止其他对缓存的写操作。写回策略效率更高,只有缓存被更新,控制器会在适当的时机把缓存中的内容更新到RAM中,比如CPU接到一个刷新缓存指令或者发生了一个硬件刷新信号(通常是由缓存未命中导致)。

当缓存未命中时,缓存被直接写到内存,必要的情况下,会把RAM中的数据加载到缓存中。

多处理器系统中每个处理器都有一个独立的硬件缓存,因而需要额外的硬件装置保证缓存间的数据同步。如图2-11,每个CPU有自己的局部硬件缓存,但更新操作变得更加耗时:当一个CPU修改自己缓存中的数据是,必须要检查相应的数据是否存在其他CPU的缓存中,如果是,则要通知对应的CPU来更新自己缓存中的值。这一般被叫做缓存窥探。幸运的是,所有这些操作都由硬件来完成,跟内核无关。

图2-11. 双处理器中的缓存

这里写图片描述

缓存技术更新很快。比如,第一代奔腾处理器只有一个缓存芯片叫做一级缓存(L1-cache)。更新一些的CPU添加了更大但稍慢的缓存,叫做二级缓存、三级缓存等等。不同层级的缓存之间的一致性由硬件保证,Linux忽略这些细节,并且假定只有一个缓存。

cr0寄存器的CD标志位用来启用和禁用缓存,NW标志指定缓存使用直写或者写回策略。

另一个奔腾处理器有趣的功能是它允许操作系统为每个页帧关联一个不同的缓存管理策略。这通常由PCD和PWT标志位来完成,但是Linux系统清除了这两个标志位,因此,所有页面都启用缓存,并且使用回写策略。

2.4.8. 快表技术(TLB)

80x86使用TLB来加速线性地址转换。当线性地址第一次被访问时,它对应的物理地址是通过内存中的页表来获取的,然后这个地址被存储在TLB中,因而之后访问相同的地址就不需要再访问内存了,可以从TLB中快速获得。

多处理器系统中,每个CPU一个快表。跟硬件缓存不同的是,快表之间不需要同步,因为不同的进程的线性地址空间时独立的。

当一个CPU的cr3寄存器被修改时,硬件自动清除本地快表的所有节点,因为一个新的页表被使用,而快表还指向老的数据。

猜你喜欢

转载自blog.csdn.net/ybxuwei/article/details/80708768