x86分页如何工作?

这个问题是为了填补关于这个问题的好的免费信息的真空。

我相信一个好的答案可以适合于一个大的答案,或者至less在几个答案中。

主要目标是为初学者提供足够的信息,以便他们可以自己学习手册,并能够理解与分页相关的基本操作系统概念。

build议的准则:

  • 答案应该是初学者友好的:
    • 具体,但可能简化的例子是非常重要的
    • 所示概念的应用是受欢迎的
  • 引用有用的资源是好的
  • 欢迎使用操作系统使用分页function的小型离线
  • PAE和PSE的解释是受欢迎的
  • 欢迎使用x86_64进行小型离线

相关的问题和为什么我认为他们不是骗局:

  • x86页表如何工作? :标题与这个问题几乎是一样的,但是身体询问有关cr3和TLB的具体问题。 这个问题是这个问题的一个子集。

  • x86虚拟化是如何工作的 :body只要求来源。

这个答案的版本有一个很好的TOC和更多的内容 。

我会纠正任何报告的错误。 如果你想做大的修改或添加一个缺失的方面,让他们自己的答案得到应有的代表。 小编辑可以直接在中合并。

示例代码

最小的例子: https : //github.com/cirosantilli/x86-bare-metal-examples/blob/5c672f73884a487414b3e21bd9e579c67cd77621/paging.S

和编程中的其他一切一样,真正理解这一点的唯一方法就是用最小的例子来玩。

是什么让这个“硬”的主题是最小的例子是很大的,因为你需要做自己的小型操作系统。

英特尔手册

尽pipe不可能在没有示例的情况下理解,但应尽快熟悉手册。

英特尔在“ 英特尔手册第三卷:系统编程指南 – 325384-056US 2015年 4 月第4章”“寻呼”中描述了寻呼。

特别有趣的是图4-4“CR3和页面结构条目的格式与32位寻呼”,给出了关键的数据结构。

MMU

分页由CPU的内存pipe理单元 (MMU)部分完成。 像许多其他产品一样(例如x87协处理器 , APIC ),早期这种芯片就是由单独的芯片组成的,后来被集成到了CPU中。 但是这个词仍然被使用。

普遍事实

逻辑地址是在“常规”用户地址码(例如mov eax, [rsi]rsi的内容)中使用的存储器地址。

首先分割将它们转换成线性地址,然后分页然后将线性地址转换成物理地址。

 (logical) ------------------> (linear) ------------> (physical) segmentation paging 

大多数情况下,我们可以将物理地址视为实际RAM硬件内存单元的索引,但这不是100%正确,因为:

  • 内存映射I / O区域
  • 多通道内存

分页只能在保护模式下使用。 在保护模式下使用分页是可选的。 如果cr0寄存器的PG位置位,则寻址开启。

分页与分页

寻呼和分段之间的一个主要区别是:

  • 分页将RAM分成称为页面的相同大小的块
  • 分段将内存分成任意大小的块

这是分页的主要优点,因为相同大小的块使事情更易于pipe理。

分页变得越来越stream行,支持分段的x86-64以64位模式下降,这是新软件的主要操作模式,它只存在于模拟IA32的兼容模式中。

应用

分页用于在现代操作系统上实现虚拟地址空间。 使用虚拟地址,操作系统可以在一个RAM上安装两个或多个并行进程,其方式如下:

  • 这两个程序都需要对另一个一无所知
  • 这两个程序的内存可以根据需要增长和缩小
  • 程序之间的切换非常快
  • 一个程序永远不能访问另一个进程的内存

分页之后的历史分页,大部分被现代操作系统(如Linux)中的虚拟内存所替代,因为pipe理固定大小的页面内存块而不是可变长度段更容易。

硬件实现

像保护模式下的分段(修改段寄存器触发GDT或LDT的加载)一样,分页硬件使用内存中的数据结构来完成其工作(页表,页面目录等)。

这些数据结构的格式是由硬件固定 ,但由操作系统来正确设置和pipe理RAM上的这些数据结构,并告诉硬件在哪里(通过cr3 )find它们。

其他一些体系结构几乎完全由软件来完成分页,所以TLB未命中运行一个OS提供的函数来遍历页表,并将新的映射插入到TLB中。 这使得页表格式被操作系统所select,但是使得硬件不可能将页面散步与其他指令的乱序执行重叠,这是x86能够实现的 。

例如:简化的单层分页scheme

这是x86分层结构的简化版本如何实现虚拟内存空间的一个例子。

页表

操作系统可以给他们以下的页面表格:

操作系统给进程1的页表:

 RAM location physical address present ----------------- ----------------- -------- PT1 + 0 * L 0x00001 1 PT1 + 1 * L 0x00000 1 PT1 + 2 * L 0x00003 1 PT1 + 3 * L 0 ... ... PT1 + 0xFFFFF * L 0x00005 1 

操作系统给予进程2的页表:

 RAM location physical address present ----------------- ----------------- -------- PT2 + 0 * L 0x0000A 1 PT2 + 1 * L 0x0000B 1 PT2 + 2 * L 0 PT2 + 3 * L 0x00003 1 ... ... ... PT2 + 0xFFFFF * L 0x00004 1 

哪里:

  • PT1PT2 :表1和表2在RAM上的初始位置。

    示例值: 0x00000000

    决定这些价值的是操作系统。

  • L :页表项的长度。

  • present :表示该页面存在于内存中。

页表位于RAM上。 他们可以例如位于:

 --------------> 0xFFFFFFFF --------------> PT1 + 0xFFFFF * L Page Table 1 --------------> PT1 --------------> PT2 + 0xFFFFF * L Page Table 2 --------------> PT2 --------------> 0x0 

两个页表的RAM的初始位置是任意的,并由OS控制。 这是由操作系统,以确保它们不重叠!

每个进程都不能直接访问任何页表,尽pipe它可以向操作系统发出请求,使页表被修改,例如请求更大的堆栈或堆段。

一个页面是一个4KB(12位)的块,并且由于地址有32位,因此只需要20位(20 + 12 = 32,因此,以hex表示的5个字符)来标识每个页面。 该值由硬件修复。

页表条目

一个页面表是一个页面表的条目表!

表项的确切格式由硬件固定。

在这个简化的例子中,页表条目只包含两个字段:

 bits function ----- ----------------------------------------- 20 physical address of the start of the page 1 present flag 

所以在这个例子中,硬件devise者可以selectL = 21

大多数真实的页面表项都有其他字段。

因为内存可以按字节而不是按位来寻址,所以在21字节alignment是不切实际的。 因此,在这种情况下,即使只需要21位,硬件devise人员也可能会selectL = 32来提高访问速度,并将剩余位保留下来供以后使用。 在x86上L的实际值是32位。

单层scheme中的地址转换

一旦页表已由OS设置,线性和物理地址之间的地址转换由硬件完成。

当OS想要激活进程1时,它将cr3设置为PT1 ,即进程1的表的开始。

如果进程1想要访问线性地址0x00000001 ,则寻呼硬件电路自动为OS执行以下操作:

  • 将线性地址分成两部分:

     | page (20 bits) | offset (12 bits) | 

    所以在这种情况下,我们会有:

    • 页面= 0x00000
    • 偏移量= 0x001
  • 查看页表1,因为cr3指向它。

  • 看条目0x00000因为那是页面部分。

    硬件知道这个入口位于RAM地址PT1 + 0 * L = PT1

  • 因为它存在,访问是有效的

  • 通过页表,页码0x00000的位置是0x00001 * 4K = 0x00001000

  • find我们只需要添加偏移量的最终物理地址:

      00001 000 + 00000 001 ----------- 00001 001 

    因为00001是在表格上查询的页面的物理地址, 001是偏移量。

    如名称所示,偏移总是简单地添加页面的物理地址。

  • 硬件会在该物理位置获取内存。

以同样的方式,下面的翻译将发生在过程1中:

 linear physical --------- --------- 00000 002 00001 002 00000 003 00001 003 00000 FFF 00001 FFF 00001 000 00000 000 00001 001 00000 001 00001 FFF 00000 FFF 00002 000 00002 000 FFFFF 000 00005 000 

例如,当访问地址00001000 ,页面部分是00001 ,硬件知道它的页表条目位于RAM地址: PT1 + 1 * L1因为页面部分),那就是它在哪里寻找它。

当操作系统要切换到进程2时,只需要将cr3指向页面2.就是这么简单!

现在下面的翻译将会发生在过程2中:

 linear physical --------- --------- 00000 002 00001 002 00000 003 00001 003 00000 FFF 00001 FFF 00001 000 00000 000 00001 001 00000 001 00001 FFF 00000 FFF 00003 000 00003 000 FFFFF 000 00004 000 

相同的线性地址转换为不同进程的不同物理地址 ,仅取决于cr3内的值。

通过这种方式,每个程序可以预期其数据从0开始并以FFFFFFFF结束,而不用担心确切的物理地址。

页面错误

如果进程1尝试访问不存在的页面内的地址,该怎么办?

硬件通过页面错误exception通知软件。

然后通常由操作系统来注册exception处理程序来决定要做什么。

访问不在桌面上的页面可能是编程错误:

 int is[1]; is[2] = 1; 

但是可能有些情况是可以接受的,例如在Linux中:

  • 该程序想要增加它的堆栈。

    它只是试图访问给定的可能范围内的某个字节,如果操作系统很高兴,它会将该页面添加到进程地址空间。

  • 该页面被交换到磁盘。

    操作系统将需要做一些工作背后的过程回页面回到内存。

    操作系统可以根据页表项的其余部分的内容发现这种情况,因为如果当前标志清楚,则页表项的其他条目完全留给操作系统去执行。

    在Linux上,例如当present = 0时:

    • 如果页表项的所有字段都是0,则表示无效的地址。

    • 否则,该页面已被交换到磁盘,并且这些字段的实际值将对页面在磁盘上的位置进行编码。

在任何情况下,操作系统都需要知道生成页面错误的地址是否能够处理该问题。 这就是为什么每当出现页面错误时,好的IA32开发人员都会将cr2的值设置为该地址。 然后,exception处理程序可以查看cr2来获取地址。

简化

简化到使这个例子更容易理解的现实:

  • 所有真正的寻呼电路都使用多级寻呼来节省空间,但是这显示出简单的单级scheme。

  • 页表只包含两个字段:一个20位地址和一个1位存在标志。

    真正的页面表总共包含12个字段,因此其他的function被省略。

例如:多级分页scheme

单级分页scheme的问题在于,它占用的RAM过多:4G / 4K = 每个进程1M条目。 如果每个条目的长度是4个字节,那么每个进程将会产生4M,即使对于台式计算机也是如此: ps -A | wc -l ps -A | wc -l说我现在正在运行244个进程,所以这需要大约1GB的内存!

出于这个原因,x86开发人员决定使用降低RAM使用率的多级scheme。

这个系统的缺点是有一个稍高的访问时间。

在用于没有PAE的32位处理器的简单3级分页scheme中,32个地址位分成如下:

 | directory (10 bits) | table (10 bits) | offset (12 bits) | 

每个进程必须有一个且只有一个页面目录与之相关联,所以它将包含至less2^10 = 1K页的目录条目,比单级scheme所要求的最小1M目录好得多。

页表只能根据操作系统的需要进行分配。 每个页表有2^10 = 1K页的目录条目

页面目录包含…页面目录条目! 除了指向页表的RAM地址而不是表的物理地址之外,页目录条目与页表条目相同。 由于这些地址只有20位宽,页表必须在4KB页的开头。

cr3现在指向当前进程的页面目录的RAM而不是页表的位置。

页表条目根本不会改变单层scheme。

页表从单级scheme改变,因为:

  • 每个进程最多可以有1K个页面表,每个页面一个目录项。
  • 每个页表包含1K条目而不是1M条目。

在前两个级别使用10位(而不是12 | 8 | 12 )的原因是每个页表项都是4个字节长。 那么页面目录和页面表格的2 ^ 10条目将很好地适合于4Kb页面。 这意味着为此目的分配和释放页面更快,更简单。

地址转换多层次scheme

操作系统给予进程1的页面目录:

 RAM location physical address present --------------- ----------------- -------- PD1 + 0 * L 0x10000 1 PD1 + 1 * L 0 PD1 + 2 * L 0x80000 1 PD1 + 3 * L 0 ... ... PD1 + 0x3FF * L 0 

PT1 = 0x100000000x10000 * 4K)处由OS给予进程1的页表:

 RAM location physical address present --------------- ----------------- -------- PT1 + 0 * L 0x00001 1 PT1 + 1 * L 0 PT1 + 2 * L 0x0000D 1 ... ... PT1 + 0x3FF * L 0x00005 1 

PT2 = 0x800000000x80000 * 4K)处由OS给予进程1的页表:

 RAM location physical address present --------------- ----------------- -------- PT2 + 0 * L 0x0000A 1 PT2 + 1 * L 0x0000C 1 PT2 + 2 * L 0 ... ... PT2 + 0x3FF * L 0x00003 1 

哪里:

  • PD1 :RAM上的进程1的页目录的初始位置。
  • PT1PT2 :在RAM上的页面表1和页面表2的初始位置。

所以在这个例子中,页面目录和页面表可以存储在RAM中,如下所示:

 ----------------> 0xFFFFFFFF ----------------> PT2 + 0x3FF * L Page Table 1 ----------------> PT2 ----------------> PD1 + 0x3FF * L Page Directory 1 ----------------> PD1 ----------------> PT1 + 0x3FF * L Page Table 2 ----------------> PT1 ----------------> 0x0 

让我们一步一步翻译线性地址0x00801004

我们假设cr3 = PD1 ,也就是它指向刚描述的页面目录。

在二进制中,线性地址是:

 0 0 8 0 1 0 0 4 0000 0000 1000 0000 0001 0000 0000 0100 

分组为10 | 10 | 12 10 | 10 | 12 10 | 10 | 12给出:

 0000000010 0000000001 000000000100 0x2 0x1 0x4 

这使:

  • 页目录条目= 0x2
  • 页表项= 0x1
  • 偏移量= 0x4

所以硬件查找页面目录的条目2。

页面目录表表示页表位于0x80000 * 4K = 0x80000000 。 这是该进程的第一个RAM访问。

由于页表项是0x1 ,所以硬件在0x80000000处查看页表的项1,这告诉它物理页位于地址0x0000C * 4K = 0x0000C000 。 这是该进程的第二个RAM访问。

最后,分页硬件增加偏移量,最后的地址是0x0000C004

其他翻译地址的例子是:

 linear 10 10 12 split physical -------- --------------- ---------- 00000001 000 000 001 00001001 00001001 000 001 001 page fault 003FF001 000 3FF 001 00005001 00400000 001 000 000 page fault 00800001 002 000 001 0000A001 00801008 002 001 008 0000C008 00802008 002 002 008 page fault 00B00001 003 000 000 page fault 

如果页面目录条目或页表项不存在,就会发生页面错误。

如果操作系统要同时运行另一个进程,则会为第二个进程提供一个单独的页面目录,并将该目录链接到单独的页表。

64位架构

对于当前的RAM大小,64位仍然是太多地址,所以大多数架构将使用较less的位。

x86_64使用48位(256 TiB),传统模式的PAE已经允许52位地址(4 PiB)。

这48位中的12位已经为偏移保留了,这留下了36位。

如果采取2级的方法,最好的分割将是两个18位的级别。

但是,这意味着页面目录将有2^18 = 256K条目,这将需要太多的RAM:靠近32位体系结构的单级分页!

因此,64位体系结构创build了更高的页面级别,通常为3或4。

x86_64在9 | 9 | 9 | 12使用4个级别 9 | 9 | 9 | 12 9 | 9 | 9 | 12scheme,所以上层只需要2^9更高层次的条目。

PAE

物理地址扩展。

使用32位,只能寻址4GB的RAM。

这开始成为大型服务器的限制,因此英特尔将PAE机制引入了Pentium Pro。

为了解决这个问题,英特尔增加了4条新的地址线,这样就可以解决64GB的问题。

如果PAE处于打开状态,页表结构也会被改变。 它的改变的确切方式取决于天气PSE是打开还是closures。

PAE通过cr4PAE位打开和closures。

即使总共可寻址内存为64GB,个别进程仍然只能使用高达4GB。 但操作系统可以在不同的4GB块上放置不同的进程。

PSE

页面大小的扩展。

允许页面长度为4M(如果PAE打开,则为2M),而不是4K。

PSE通过cr4PAE位打开和closures。

PAE和PSE页面表格scheme

如果PAE和PSE都处于活动状态,则使用不同的分页级别scheme:

  • 没有PAE,也没有PSE: 10 | 10 | 12 10 | 10 | 12

  • 没有PAE和PSE: 10 | 22 10 | 22

    22是4Mb页面内的偏移量,因为22位地址为4Mb。

  • PAE和没有PSE: 2 | 9 | 9 | 12 2 | 9 | 9 | 12

    9的两倍而不是10的devise原因是,现在条目已经不能适应32位,它们被20个地址位和12个有意义的或保留的标志位填满。

    原因是20位不足以代表页表的地址:现在需要24位,因为处理器中增加了4条额外的电线。

    因此,devise者决定将入口大小增加到64位,并且使它们适合单个页面表,有必要将入口数量减less到2 ^ 9而不是2 ^ 10。

    起始2是一个称为页目录指针表(PDPT)的新页面级别,因为它指向页目录并填充32位线性地址。 PDPT也是64位宽。

    现在, cr3指向PDPT,它必须位于第一个4GB的内存上,并且在32位倍数上alignment以提高寻址效率。 这意味着现在cr3有27个有意义的位,而不是32:2 ^ 27的20:2 ^ 5来完成第一个4GB的2 ^ 32。

  • PAE和PSE: 2 | 9 | 21 2 | 9 | 21

    devise师决定保留一个9位宽的字段,使其适合一个页面。

    这留下23位。 留下2为PDPT保持统一的PAE情况下没有PSE离开21偏移,这意味着页面是2M宽而不是4M。

TLB

翻译前瞻缓冲区(TLB)是寻呼地址的caching。

由于它是一个caching,它共享了CPUcaching的许多devise问题,如关联性级别。

本节将描述一个带有4个单一地址条目的简化的全关联TLB。 请注意,像其他caching一样,真正的TLB通常不是完全关联的。

基本操作

线性和物理地址之间的转换发生后,它被存储在TLB上。 例如,一个4条目的TLB以下列状态开始:

  valid linear physical ------ ------- --------- > 0 00000 00000 0 00000 00000 0 00000 00000 0 00000 00000 

>表示要被replace的当前条目。

并且在页面线性地址00003被转换为物理地址00005 ,TLB变成:

  valid linear physical ------ ------- --------- 1 00003 00005 > 0 00000 00000 0 00000 00000 0 00000 00000 

0000700009的第二次翻译00007 ,它变成:

  valid linear physical ------ ------- --------- 1 00003 00005 1 00007 00009 > 0 00000 00000 0 00000 00000 

现在,如果00003需要重新翻译,硬件首先查找TLB,并用单个RAM访问00003 --> 00005查找其地址。

当然, 00000不在TLB中,因为没有有效的条目包含00000作为关键字。

replace政策

当TLB填满时,旧地址被覆盖。 就像CPUcaching一样,replace策略是一个潜在的复杂操作,但是一个简单合理的启发就是删除最近最less使用的条目(LRU)。

有了LRU,从状态开始:

  valid linear physical ------ ------- --------- > 1 00003 00005 1 00007 00009 1 00009 00001 1 0000B 00003 

0000D -> 0000A会给:

  valid linear physical ------ ------- --------- 1 0000D 0000A > 1 00007 00009 1 00009 00001 1 0000B 00003 

CAM

使用TLB使得翻译更快,因为初始翻译每个TLB级别需要一次访问,这意味着在简单的32位scheme上是2,而在64位体系结构上是3或4。

TLB通常被实现为称为内容可寻址存储器(CAM)的昂贵types的RAM。 CAM在硬件上实现一个关联映射,即给定一个键(线性地址)的结构,检索一个值。

映射也可以在RAM地址上实现,但是CAM映射可能需要比RAM映射less得多的条目。

例如,一张地图,其中:

  • 密钥和值都有20位(简单分页scheme的情况)
  • 每次最多需要存储4个值

可以存储在TLB中有4个条目:

 linear physical ------- --------- 00000 00001 00001 00010 00010 00011 FFFFF 00000 

不过,要用RAM来实现这个, 需要有2 ^ 20个地址

 linear physical ------- --------- 00000 00001 00001 00010 00010 00011 ... (from 00011 to FFFFE) FFFFF 00000 

这比使用TLB更昂贵。

使条目无效

cr3发生变化时,所有TLB条目都将失效,因为将要使用新进程的新页表,因此旧条目不可能有任何意义。

x86还提供了显式使单个TLB条目无效的invlpg指令。 其他体系结构提供了更多的指示来使TLB条目无效,例如使给定范围内的所有条目无效。

有些x86 CPU超越了x86规范的要求,并且提供了比它保证的更一致性, 在修改页表项和使用它时,它还没有被caching在TLB中 。 显然,Windows 9x依赖于正确性,但现代AMD CPU不提供连贯的页面散步。 尽pipe英特尔CPU必须检测出错误的猜测才行。 利用这一点可能是一个坏主意,因为可能没有太多的收获,并且很容易引起微妙的时序敏感问题,这些问题很难debugging。

Linux内核使用情况

Linux内核广泛使用了x86的分页function,以允许使用小数据碎片的快速进程切换。

v4.2 ,查看arch/x86/

  • include/asm/pgtable*
  • include/asm/page*
  • mm/pgtable*
  • mm/page*

似乎没有定义结构来表示页面,只有macros: include/asm/page_types.h特别有趣。 摘抄:

 #define _PAGE_BIT_PRESENT 0 /* is present */ #define _PAGE_BIT_RW 1 /* writeable */ #define _PAGE_BIT_USER 2 /* userspace addressable */ #define _PAGE_BIT_PWT 3 /* page write through */ 

arch/x86/include/uapi/asm/processor-flags.h定义了CR0 ,特别是PG位的位置:

 #define X86_CR0_PG_BIT 31 /* Paging */ 

参考书目

自由:

  • rutgers-pxk-416章节“内存pipe理:讲义”

    较旧的操作系统使用的内存组织技术的良好历史回顾。

非自由:

  • bovet05章节“内存寻址”

    x86内存寻址的合理介绍。 缺less一些好的和简单的例子。

这是一个非常简短的高级答案:

x86处理器以几种可能的模式之一运行(粗略地说:真实的,受保护的,64位)。 每种模式可以使用几种可能的存储器寻址模式之一(但不是每种模式都可以使用每种模式),即:实模式寻址,分段寻址和平坦线性寻址。

在现代世界中,只有被保护或64位模式的平面线性寻址是相关的,并且两种模式基本相同,主要区别在于机器字的大小以及因此可寻址的存储量。

现在,存储器寻址模式赋予机器指令的存储器操作数的含义(诸如mov DWORD PTR [eax], 25 ,其将值为25的32位(又名dword )整数存储到其地址被存储在eax 32位寄存器)。 在扁平线性寻址中, eax这个数字允许运行在从零到最大值(在本例中为2 32 – 1)的单个连续范围内。

但是,平面线性寻址可以分页不分页 。 没有分页,地址直接指物理内存。 通过分页,处理器的内存pipe理单元(MMU)将所需地址(现称为虚拟地址 )透明地送入查找机制,即所谓的页表 ,并获得一个新的值,该值被解释为物理地址。 原来的操作现在在物理内存中的这个新的翻译地址上运行,即使用户只能看到虚拟地址。

分页的主要好处是页表由操作系统pipe理。 因此操作系统可以任意修改和replace页表,比如“切换任务”。 它可以保留一整套页表,每个“进程”对应一个表,并且每当它决定一个特定的进程将在给定的CPU上运行时,它将该进程的页表加载到该CPU的MMU中(每个CPU都有它自己的一组页表)。 其结果是,每个进程看到自己的虚拟地址空间,看起来是相同的,无论哪个物理页面是免费的,当操作系统不得不为它分配内存。 它从来不知道任何其他进程的内存,因为它不能直接访问物理内存。

页表是嵌套的树状数据结构,存储在正常的存储器中,由OS编写,但由硬件直接读取,所以格式是固定的。 通过设置一个特殊的CPU控制寄存器指向顶层表,它们被“加载”到MMU中。 CPU使用称为TLB的高速caching来记住查找,因此对于相同的几个页面的重复访问比分散的访问快得多,这是因为TLB未命中原因以及通常的数据caching原因。 即使在TLB中没有caching的情况下,通常也会看到术语“TLB条目”用于引用页表项。

如果您担心某个进程可能会禁用分页或尝试修改页表:这是不允许的,因为x86实现了特权级别 (称为“环形”),并且用户代码的执行级别太低,它修改CPU的页表。