首页
社区
课程
招聘
[原创][原创] Windows 7 x64 虚拟内存管理 第三部分笔记
2022-3-5 12:10 21291

[原创][原创] Windows 7 x64 虚拟内存管理 第三部分笔记

2022-3-5 12:10
21291

内存管理基础

1. 虚拟地址空间基础

1.1 基本区域

虚拟地址空间可以被划分为4个基本区域:

  • 用户模式区域,从0到0x7FF‘FFFFFFFF。这是一个8TB大小的进程地址空间,被用于可执行文件,Dlls和其他功能的内存,比如说栈和堆。
  • 未使用的地址范围:0x800‘00000000 到 0x00007FFF’FFFFFFFF。因为Windows限制了用户模式的地址范围到8TB,所以这个区域不使用。
  • 无效的地址范围,因为处理器要求地址必须是规范的,而0x00008000'00000000 to 0xFFFF7FFFF’FFFFFFF违反了规范。规范的地址要求47-63位要么是全0,要么是全1。
  • 0xFFFF8000'00000000 到 OxFFFFFFFF'FFFFFFFF 用于内核代码和一些数据结构。我们称为系统区域。

每一个进程的用户模式地址空间都是私有的,这意味着每个进程的虚拟地址范围内映射着不同的物理页。当然存在API,在访问权限足够的情况下,能跨进程访问用户地址空间。虚拟内存管理器(VMM)的基本行为就是为每一个进程创建私有用户范围。

 

系统区域的大多数区域,在所有进程里是共享的。因为所有的虚拟地址映射的都是同一个物理页。这条规则也有例外,我们将在后面更详细地研究它们。

1.2 分页结构区域

VMM必须能够修改处理器的分页结构。然而,这带来了一个有趣的问题,处理器指令只能使用虚拟地址,即没有办法通过直接指定物理地址来更新一个内存位置的内容。因此访问PML4PDPT等的唯一方法是将它们也映射到虚拟地址上。

1.2.1 PML4 自动项(auto-entry)

这个问题的解决方案比它看起来要简单,而且相当有趣,就是以一种非常特殊的方式使用一个PML4e0x1ed处的项存储PML4自身的PFN。我们将称这个条目为PML4自动项,现在我们将看到他的作用。

 

让我们先回忆一下,一个PML4e映射了512GBVA空间,那么就会有这样一个范围大小的空间为这个特殊目的而保留。为了确定这个PML4e所对应的地址范围,我们必须记住,PML4的索引被存储到VA的第39-47位;当这个索引被设置为0x1ed时,对应的虚拟地址范围如下所示:

 

 

图10显示了VA的39-47位设置成了0x1ED,因为第47位是1,所以48-63也必须为1.那么0-38位可以从0...0到1...1。最后的结果就是这样:

 

 

36-39位的十六进制位是从8到F,x代表可以从全0到全1。最终的范围就是

 

0xFFFFF680'00000000 -- 0xFFFFF6FF'FFFFFFFF

 

这个地址范围的VA在处理器上的地址转换是这样的:使用0x1ed作为索引取到PML4e,然后使用这个项的PFN定位PDPT。等等!这个PFN是指向PML4自身,也就是说PML4被当作PDPT使用了。这就意味着以下几点:

  • 从虚拟地址的30-38位得到的index,然后从PML4里索引后指向的是一个PDPT,因为此时得到的实际上是PML4e
  • 从虚拟地址的21-29位得到的index,从PDPT索引得到PDPTe,指向PD
  • 接着20-12位得到的index索引到的就是PTVA到物理页的映射完成后,我们就看到了页表的内容。

访问0xFFFFF680'00000000得到的PTE虚拟地址的第一个字节。PTEs是8字节大小的,因此下一个PTE的虚拟地址就是0xFFFFF680'00000008,以此类推。

1.2.2 VA和他的PTE地址的关系

虽然知道一个给定VAPTE在512GB区域内的某个地方,但最好是把它的实际位置缩小一点,VA和它的PTE的地址之间有一个非常简单的关系。下面我们先看一个VA的组成部分:

 

 

如果我们进行下面的操作会怎么样呢?

  • VA中的所有索引位向右移9位
  • 最左边未使用的位全部设置为1
  • PML4 index替换成 PML4 自动项的 index
  • 将0-2位置0

得到了下面的结果:

 

 

填入auto-entry后改变了其余索引的意义,PDPT index现在填的是PML4index

 

这样,原来的PML4索引仍然在向PML4索引(PDPT index字段得到的新值),并为原来的VA选择PDPTPDPT索引仍然在向PDPT索引,并为原来的VA选择PD,依此类推,直到映射原来VAPT被映射为物理页。换句话说,给定一个VA,这种转换给了我们另一个VA,在这个VA上我们 "看到 "了用于映射我们原来的VAPT

 

最后,页面中的偏移量是PT index,后面有三个比特设置为0,这就得到了
offset = (PT index) x 8
由于PTE的大小为8字节,这就是原来的VAPTEPT内的偏移。

 

由上述步骤产生的VA是原来的VAPTE所在虚拟地址。

 

注意,我们必须将0-2位设置为0,因为在移位之后,12位宽的偏移字段包含了3-11位的PT index,而0-2位保留了原偏移字段的剩余部分,即其最左边的三个位,这三个位是没有意义的:它们只是给出了一个从PTE起始地址的随机偏移。我们还必须将第48-63位设置为1,因为当我们将第39-47位设置为auto-entry index时,第47位是1。

 

现在假设要更新我们刚刚计算出来的PTE地址,则有以下步骤

  • 左移9位
  • 根据移位完成后的第47位设置48-63位。

我们得到了原来的VA,不过只是offset部分被设置成了0。换句话说,给定一个PTE虚拟地址,这个操作可以获得PTE映射的虚拟页的第一个字节的地址。

1.2.3 PDEs,PDPTEs,PML4Es的虚拟地址

能访问PTEs了,但是其他的表呢?实际上,同样的思路,反复利用PML4 auto-entry就行了。

 

比方说这样一个VA

 

 

这次我们将PML4 indexPDPT index域设置成auto-entry index,这样实际上PD index索引的就是PDPTPT index索引的就是PD。这种地址被用于将一个页面目录映射到虚拟地址空间,我们在这个虚拟页面内的地址上就可以 "看到 "PDEs

 

因此我们可以计算出PDE的范围:

 

开始地址分析:

 

​ bits 63-48 = 1

 

​ bits 39-47 = 1ed (auto-entry index)

 

​ bits 30-38 = 1ed (auto-entry index)

 

​ bits 0-29 = 0

 

结束地址分析:

 

​ 和上面一样,不过 0-29位全为1。

 

因此 PDE开始地址 0xFFFFF6FB'40000000 结束地址:0xFFFFF6FB'80000000

 

请注意,上面我们将范围后的第一个字节称为结束地址。这样结束地址就是0xFFFFF6FB'80000000,而不是0xFFFFF6FB'7FFFFFFF。

 

可以发现这个地址范围在之前计算出来的PTE里。也就是说他把实际的PTE范围分成了两个区域,中间的范围是拿给PDE映射的,而不是PTE

 

VA和他的PDE的虚拟地址关系和他的PTE的计算逻辑一致。将VA地址的索引位向右移动18位,移出来两个空位后,把auto-entry index添进去,再将地址规范化。

 

如果逆向操作得出来的新VA的话,我们就能得到映射该PDE的起始虚拟地址。当然,这个区域不再是一个单一的页面,一个PDE映射了512个PTE,因此它覆盖了512个页面,即2MB

 

同样的道理,PDPTEs的虚拟地址就是左边填3个auto-entry index项。

 

PDPTE的起始地址就是 0xFFFFF6FB'7DA00000

 

PDPTE的结束地址就是 0xFFFFF6FB'7DC00000

 

对于一个VA,要得到他的PDPTE的话,就得移除三个index空位,分别将auto-entry index填入PML4,PDPT,PD

 

最后,如果四个索引位都设置成auto-index的话,那么会得到一个4kb的范围。

 

PML4E 起始地址 0xFFFFF6FB'7DBED000

 

PML4E 结束地址 0xFFFFF6FB'7DBEE000

 

比如,PML4 auto-entryindex是0x1e8。他的offset=0x1e8 x 8 = 0xf68。那么他的VA就是0XFFFFF6FB7DBEDF68。

 

下图展示了各种分页结构区域:

 

 

WinDbg自带了方便的!pte扩展命令,它可以计算映射虚拟地址的PxEsVA,所以我们不必手动计算:只要输入!pte 后面跟个虚拟地址。

1.2.4 分页结构区域保护

分页结构区域必须只能被内核模式代码访问,也就是执行在CPL = 0 的代码,这是怎么实现的呢?

 

下面是一个有效用户模式地址执行!pte后的输出结果:

1
2
3
4
5
1: kd> !pte 0x771d153a
                                           VA 00000000771d153a
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001DC0    PTE at FFFFF680003B8E88
contains 02C00001D10D3867  contains 02D00001D17D4867  contains 01900001D13E2867  contains 25100001F66BF025
pfn 1d10d3    ---DA--UWEV  pfn 1d17d4    ---DA--UWEV  pfn 1d13e2    ---DA--UWEV  pfn 1f66bf    ----A--UREV

可以看到每个PxE都带U。也就是说PML4EPDPTEPDEPTE都设置了U/S位。U/S就是第二位,输出的结果中最右边都是7,也就是第2位是置1的。

 

可以与内核地址进行对比

1
2
3
4
5
1: kd> !pte fffff880`0236e6b0
                                           VA fffff8800236e6b0
PXE at FFFFF6FB7DBEDF88    PPE at FFFFF6FB7DBF1000    PDE at FFFFF6FB7E200088    PTE at FFFFF6FC40011B70
contains 000000022D004863  contains 000000022D003863  contains 00000001D8AD7863  contains 80000001D322E963
pfn 22d004    ---DA--KWEV  pfn 22d003    ---DA--KWEV  pfn 1d8ad7    ---DA--KWEV  pfn 1d322e    -G-DA--KW-V

每一个PxE都带K,因为每一项的值都以3结尾,这告诉我们这个VA是内核模式才能访问。

 

虚拟地址0x771d153a的PTE虚拟地址是FFFFF680003B8E88。让我们看下这个地址的映射情况:

1
2
3
4
5
1: kd> !pte FFFFF6FB40001DC0
                                           VA fffff680003b8000
PXE at FFFFF6FB7DBEDF68    PPE at FFFFF6FB7DBED000    PDE at FFFFF6FB7DA00008    PTE at FFFFF6FB40001DC0
contains 00000001D372E863  contains 02C00001D10D3867  contains 02D00001D17D4867  contains 01900001D13E2867
pfn 1d372e    ---DA--KWEV  pfn 1d10d3    ---DA--UWEV  pfn 1d17d4    ---DA--UWEV  pfn 1d13e2    ---DA--UWEV

输出的结果显示他的PTE地址是FFFFF6FB40001DC0,里面的内容是01900001D13E2867。

 

结果表明FFFFF680003B8E88所在的页面的PTE就是0x771d153a的PDE。由于0x771d153a是一个用户模式地址,它的PDE有一个U位,所以我们从上面的输出看到,这个系统页的PTE以7结尾,"U"就被调试器输出了。

 

但这不意味着可以从用户模式访问PDEPDPTE,你会发现PML4EU/S位置0的。这就意味着还是访问不到PDEPDPTE

 

通常来说,一个VA的各个PxEs有着不同的保护是不常见的,一般U/S位要么全set,要么全clear。但是这在分页结构区域里的地址无法避免出现这种情况。

1.2.5 !pte命令注意事项

让我们再看一次 !pte 0x771d153a的输出结果

1
2
3
4
5
1: kd> !pte 0x771d153a
                                           VA 00000000771d153a
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001DC0    PTE at FFFFF680003B8E88
contains 02C00001D10D3867  contains 02D00001D17D4867  contains 01900001D13E2867  contains 25100001F66BF025
pfn 1d10d3    ---DA--UWEV  pfn 1d17d4    ---DA--UWEV  pfn 1d13e2    ---DA--UWEV  pfn 1f66bf    ----A--UREV

他的PTE映射在FFFFF680003B8E88虚拟地址上。然后我们用这个地址来执行!pte

1
2
3
4
5
1: kd> !pte FFFFF680003B8E88
                                           VA 00000000771d1000
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00008    PDE at FFFFF6FB40001DC0    PTE at FFFFF680003B8E88
contains 02C00001D10D3867  contains 02D00001D17D4867  contains 01900001D13E2867  contains 25100001F66BF025
pfn 1d10d3    ---DA--UWEV  pfn 1d17d4    ---DA--UWEV  pfn 1d13e2    ---DA--UWEV  pfn 1f66bf    ----A--UREV

这并不是我们期望的结果:他和0x77d153a输出的一样。其实是这样的,!pte 指令在判断输入的地址属于分页结构区域时,它以为我们想看到的是它所映射的地址。

 

因此,我们就要使用上一级分页结构的地址,也就是!pte 然后跟上 PDE的地址。这样就可以看到PDE映射的PTE了,也就是页表。

1
2
3
4
5
1: kd> !pte FFFFF6FB40001DC0
                                           VA fffff680003b8000
PXE at FFFFF6FB7DBEDF68    PPE at FFFFF6FB7DBED000    PDE at FFFFF6FB7DA00008    PTE at FFFFF6FB40001DC0
contains 00000001D372E863  contains 02C00001D10D3867  contains 02D00001D17D4867  contains 01900001D13E2867
pfn 1d372e    ---DA--KWEV  pfn 1d10d3    ---DA--UWEV  pfn 1d17d4    ---DA--UWEV  pfn 1d13e2    ---DA--UWEV

现在看到的就是分页表地址的映射了。这有时会让人感到困惑,但只要记住处在分页结构区域的虚拟地址,我们必须使用!pte时,要使用上一层分页结构的地址。

 

谨记:PML4E,PDPTE,PDE,PTE这四个都是映射到分页结构区域的,所以这里的地址,一定要用上一级的查看。

1.3 系统地址范围

这一节将讲述为内核保留的虚拟地址范围,同时定义一些基本概念,为本书第五部分节省一些细节。

1.3.1 系统地址从哪里开始?

在32位Windows系统上,内核地址的开始地址简单的定义在0x8000000或者0xC0000000上。换句话说,要么一分为二,要么内核1GB,用户模式代码3GB。

 

但是64位下,这件事情就复杂起来了。

 

首先,规范的地址要求虚拟地址空间在0xFFFF8000'00000000 到 0xFFFFFFFF'FFFFFFFF,也就是128TB的虚拟地址。但是这不意味着操作系统从这个高地址空间开始。因为分页结构的地址范围最低是0xFFFFF680'00000000。这就意味着0xFFFF8000'00000000 - 0xFFFFF67F'FFFFFFFF地址空间是不使用的,即118.5TB地址范围不使用。

 

本书其余部分将使用系统范围代表系统使用的高地址范围。

 

某些数据结构不能存储在从系统范围最大地址是,也就是顶部开始低于8TB的地址上,关于这点以后会解释为什么。我们只想指出,系统8TB的内存并不能全部用于所有用途。虽然有很多资料说系统范围的大小为8TB。然而,这也是不正确的。8TB开始的地址是0xFFFFF800'000000,但Windows在0xFFFFF680'000000-0xFFFFF7FF'FFFFFFF的范围内使用了额外的1.5TB用于映射分页结构,所以总的系统范围应该是9.5TB大小。

1.3.2 主系统区域

我们可以进一步细分系统地址范围,用WinDbg查看高范围的PML4内容,其PML4E开始于0xFFFFF6FB'7DBED800,结束于0xFFFFF6FB'7DBEDFF8。我们可以看到

  • 0xFFFF8000'00000000 到 0xFFFFF680'00000000(不包含这个地址) 也就是半闭半开区间 [0x0xFFFF8000'00000000,0xFFFFF680'00000000)PML4Es 是没有使用的。
  • 0xFFFFF680'00000000 - 0xFFFFF700'00000000范围内的地址的PML4E都是auto-entry
  • auto-entry以外的其他PML4E各自映射出512GB的区域

下图描述了一个PML4表所映射的高范围。我们将在本书第五部分更详细地研究这里介绍的各个区域。

 

 

现在我们没必要关心各个区域的意义。让我们先通过看看不同进程的PML4s,得出两个事实。

 

两个阴影区域分别对应了一个PML4E。他们为不同的进程存储不同的值。这意味着每个进程都有在这些区域的都有自己的私有拷贝,尽管它们属于系统地址范围。第一个区域是分页结构区域。每个进程都应该映射它自己的分页结构,或者换句话说,每个进程的PML4E存储PML4PFN

 

第二个区域比较神秘,我们后面会看到Windows如何使用它的。这块区域暂对进程来说也是私有的。不过只是会话内存在同一个会话中的进程是一样的,不同的会话进程不一样,因为每个会话都会有自己的私有拷贝。

 

其他的PML4Es,每个地址空间在相应的区域映射了相同的物理页。为了实现这一点,理论上并不严格需要有相同的PML4Es:每个进程都可以有不同的PML4Es,即使指向了不同的PDPTsPDsPTs,但是PTE最终指向相同的物理页就行了。但实际上Windows系统通过将这些地址空间的PML4Es设置为同样的值,这样就重用了PDPTs,那么对于每个地址空间的分页结构的整个层次是一样的。这给我们带来两个好处。最明显的是,我们节省了物理内存;更微妙和更重要的是,当我们在这些范围内映射或取消映射一个共享的物理页时,我们只需要更新一个PTE。如果我们有多个页表指向同一个共享的系统页,在映射和取消映射时我们就得挨个更新PTEs。要做到这一点,我们还得遍历所有正在运行的进程的分页结构,这是非常耗时的。同样的道理也适用于PTPDPDPT,它们在这些地址空间中也是重用的。

 

我们还可以猜测,一个会话内的PML4s结构也是一样的:设置为0的条目永远不会被使用,共享区域的PML4e总是指向同一个PDPT;否则我们又要为所有共享的会话地址空间更新所有的PML4s,这明显是不可能的。

 

高地址范围是128TB大小,由PML4的一半表项映射,也就是由256个PML4E。这些项中只有少数几个,即19个被Windows使用,对应于地址空间的上面部分的9.5TB,所以还有很多未使用的虚拟地址空间。

 

最后,在内核中,有一个名为MmSystemRangeStart的静态变量,其名称似乎是不言自明的。然而,这个变量存储了以下值:

1
2
3
4
5
6
7
8
9
1: kd> dq nt!MmSystemRangeStart
fffff800`046c50e8  ffff0800`00000000 00000000`0023ffff
fffff800`046c50f8  00000007`00000006 00000340`00000000
fffff800`046c5108  49fbeac5`c042287f fffffa80`61154660
fffff800`046c5118  00000000`00000000 00ee09e4`1100020f
fffff800`046c5128  00000000`00000000 00000000`00000000
fffff800`046c5138  00000000`00000000 fffff880`00961000
fffff800`046c5148  00000000`00000000 00000000`00004000
fffff800`046c5158  fffff800`04a0c7b0 00000000`00000000

这个值的48-63位为是1,但是47位是0。32位的Windows 2000是,这个值通常是0x80000000或者0xc0000000。这个地址是规范的,说明现在这个变量是不被使用的。

2 NUMA系统简介

2.1 什么是NUMA系统

NUMANon Uniform Memory Access的缩写,中文名称是非统一内存访问。这种系统的处理器访问不同区域的内存会使用不同的方式。

 

例如,考虑一个包含两个处理器的系统,一个是P0,一个是P1,每个处理器都包括一个片上存储器控制器。

 

每个处理器将通过其控制器与一些RAM芯片紧密相连。如果P0想访问连接到P1的内存,它必须使用处理器间的总线,而对于连接到自己控制器的内存,访问路径则要短得多。内存延迟的变化取决于内存相对于处理器的位置。

 

在现代处理器上,通常在一个芯片上有多个核心,其中包括内存控制器。这些核心实际上就是独立的处理器,它们都有相同的内存访问路径,对连接到片上控制器的RAM的访问速度较快,对其他内存的访问速度较慢。这样一组处理器和最接近它们的存储器被称为节点。

 

系统内存仍然被组织成所有处理器都可以访问的单一地址空间,因此,从功能上讲,NUMA系统的工作方式类似于模拟多处理(SMP)系统。这意味着,某个物理地址可以访问属于节点0的字节,而另一个地址可以引用节点1的内存。通常情况下,一个物理页面完全属于一个节点。

 

一个有两个以上节点的系统在不同的节点之间可以有不同的访问延迟。例如,节点0访问属于节点1的内存比访问属于节点2的内存快,等等,这取决于处理器间总线的工作方式。

 

使用NUMA以提高系统性能。例如,最好将一个进程安排在属于一个节点的处理器上,并将其虚拟地址映射到同一节点的页面上。如果在首选的节点上没有可用的页,而访问时间在各节点之间变化,那么最好尝试从访问时间较短的节点分配内存,也称为最近的节点。

2.2 Windows对NUMA的支持

在分析内存管理的一些细节时,我们将看到NUMA是如何在VMM中得到支持的。因此,现在介绍一下关于WindowsNUMA支持的一些基本概念。

 

VMM初始化时会执行MiComputeNumaCosts函数确定物理页面在系统中的节点之间的分布,以便VMM "知道 "哪些页面属于哪个节点。它还确定了节点之间的距离,并建立了系统的逻辑表示。这样,当VMM需要从一个特定的节点分配一个页面时,它知道哪些页面属于它,以及这些页面是否可用。如果该节点上没有可用的页面,它就尝试找到最近的一个,以此类推,直到找到一个可用的页面。简而言之,VMM试图分配最近的可用页面。

 

Windows调度程序为每个线程分配一个理想的处理器,并试图在大部分时间内让每个线程都在这个处理器上运行;当VMM代表一个线程分配物理页时,它从线程的_KTHREAD中提取这个处理器索引,用来访问处理器的_KPRCB结构;从那里,它可以找到处理器所属节点的物理页列表,并执行上述的分配顺序。

 

VMM和线程调度器合作,试图让线程在特定的节点上运行,并分配属于同一节点的物理页。

 

如果还有其他空闲的处理器,一个线程可以在其理想的处理器以外的处理器上运行。这是有道理的,因为这意味着Windows更倾向于利用空闲的处理器,而不是让线程等待。当这种情况发生时,VMM必须向线程提供一个物理页面,它仍然按照前面描述的顺序寻找可用的页面,从理想处理器的节点开始。一个线程暂时在某个非理想处理器上运行的事实并不意味着它的物理页是从这个特定处理器的节点上分配的。线程调度器会尽快将线程返回其理想的处理器,所以最好总是从理想的节点(理想处理器的节点)获取内存页。

3 缓存和页面着色

在深入VMM内部原理时,我们需要先定义页面着色的概念。为了解释清楚这个概念,我们还需要理解处理器的缓存是怎么工作的。

3.1 缓存基础

下面是一个实际的例子,在英特尔Core2 Duo移动处理器上的二级缓存,它有以下特点(很快就会解释)。

 

大小: 4MB

 

通道: 16

 

缓存行:64 bytes

 

现在假设处理器想访问下面的物理地址:

1
2
3
bit #         3333 3322 2222 2222 1111 1111 11
              5432 1098 7654 3210 9876 5432 1098 7654 3210
0x781f423 = 0000 0000 0111 1000 0001 1111 0100 0010 0011

我们将地址表示为36位的二进制数字,因为这个型号的处理器将地址大小限制为36位。我们现在要看看处理器如何在缓存中寻找这个地址。

 

首先,高速缓存行是指数据是以固定大小的连续字节块从内存加载到高速缓存中。缓存行的第一个字节的地址必须是缓存行大小的倍数。当处理器需要在一个任意的地址上加载一个或多个字节到高速缓存中时,它会加载整个高速缓存行(或多行),其中包括所需的字节。如果被访问的字节块跨越了高速缓存行的边界,则可以加载多于一行。

 

很明显,缓存的组织方式必须能够知道一个缓存的字节是从哪个地址被加载的。然而,由于一行中的字节是连续的,所以缓存组织的方式是只记录一行中第一个字节的地址,我们称之为行地址。其他字节的地址是由行地址和行内的字节偏移量简单计算出来的。

 

在下文中,我们还将使用术语内存行来表示内存中以行大小的倍数开始的字节块,也就是说,一个内存块可以被加载到一个缓存行中并完全填充。

 

对于我们正在考虑的处理器,缓存行的大小是64字节。由于行地址必须是64的倍数,因此对于这样的地址,从64位地址中的0-5位总是0。对于一般的地址来说,如果在高速缓存中发现了行的偏移量,它们只是一个行偏移量;并不是在高速缓存中用来定位含有所需数据的所在的行。

 

处理器会使用接下来的12位,即第6-17位,在一组高速缓冲存储器块的数组中寻找所需的行。每个数组被称为一个通道,在我们的例子中,我们有16个这样的通道。处理器使用第6-17位作为通道的索引,定位16个数据块,每个通道一个。总的来说,这16个数据块被称为集合。从第6-17位提取的特定索引值就是索引这个集合。

 

这个集合中的每个数据块都保存着一个缓存行的内容,这个缓存行可能就是处理器正在寻找的那个,位6-17等于01 11110100 00的行;因此,它可以指的是所需的行,也可以指的是这些位的数值相同的另一个行,也就是还不确定是否是被缓存。这意味着我们需要更多的数据来知道我们的地址是否被缓存,以及进入哪个数据块中。为了解决这个问题,处理器将行地址的第18-35位存储在数据块中,连同缓存行的内容(记住,在这个特定的处理器上,物理地址被限制为36位,所以我们对高于第35位的位不感兴趣)。这一组比特位被称为标记(tag)。假设我们的地址内容确实被缓存到了其中一个缓存通道中。我们可以用下图来表示通道的内容。

 

 

这个集合中的其他数据块根据不同的标记(tag)存储来自不同地址的内容。同样有趣的是,处理器可以同时将存储在16个数据块中的所有标记与来自我们地址的标记进行比较,而不需要迭代这些数据块,因此确认缓存的速度会非常快。

 

但是,为什么要有多个通道,也就是说,为什么要有数据块的集合?因为,在单一方式下,两个具有相同索引的内存行,即行地址的第6-17位具有相同的值,不可能同时出现在缓存中:这个特定的索引只有一个 位置,一个指向这个空位的新行将把前一个从缓存中移除。有了16个通道,当所有的通道都在使用时,处理器可以存储多达16个具有相同索引的行,并采取移除最近最久未使用的(LRU)算法进行移除。

 

缓存中的所有项并不总是包含有效的内存数据。一个特定的项可能根本就没有被使用过,因为不存在带有其索引的地址,或者其内容可能已经失效了。当处理器检测到另一个处理器正在写入缓存数据的地址时,通常会发生缓存行的失效。在这种情况下意味着缓存中的副本已经不是最新的了,所以检测到的处理器将其数据块仅仅标记为无效(一个处理器不会重新加载缓存行,直到它正在执行的代码试图实际访问同一个物理地址)。

 

这种结构的高速缓存被称为集合关联型高速缓存,而只有一种方式的高速缓存则是直接映射高速缓存。

 

理想的高速缓存是一个完全关联的高速缓存,也就是说,它允许一个高速缓存行存储在任何地方,而不使用部分的地址作为索引。有了这样的高速缓存,处理器只需要在整个高速缓存满了的时候,而不是在其索引的16个可能的块都被使用的时候,才需要换掉一个行,而且换掉的可以是整个高速缓存的LRU行,而不仅仅是16个块。问题是,随着缓存大小的增加,实现一个完全关联的缓存会变得更加复杂,而且在今天的二级缓存中使用的兆字节大小的缓存,实际上是不可能的。不过处理器确实有一个完全关联的高速缓存,被命名为Translation lookaside buffer (TLB),我们将在本书后面讨论它,但它只用于一个专门的目的,而且非常小(少于100个项)。

 

用于索引通道的比特数可以从缓存大小、缓存行大小和通道的数量中计算出来。在我们的例子中。
缓存大小 = 4MB

 

缓存中的总行数 = 4MB/64 Bytes = 65536

 

每个通道的行数 = 65536/16 = 4096

 

每个通道有4096项,因此需要12个位的索引值。

3.2 页面着色

物理内存页的颜色是一个数字,它标识了该页的地址可以被缓存到哪些集合中。具有相同颜色的页面被缓存在相同的集合中,相同颜色的页面在缓存的16种通道中寻找空间时就会相互竞争。

 

一个页面的起始地址在4kB边界上对齐,因此0-11位被设置为0。 例如,包含上一节所考虑的地址的页面(0x781f423)开始于0x781f000;页面中第一行的集合索引是这个地址的第6-17位的值。

 

 

在下文中,我们将把页面首行的集合索引称为页面集合索引。因此,我们的页面集合索引是01 1111 0000 00。

 

在4kB页面和每条缓存行64字节的情况下,一个页面跨越了64个相邻的行或64个从页面集合索引开始的连续索引值。

 

每一个具有相同页面集合索引的页面都将被缓存在用于这个页面的64个页面集合中,因此我们可以将页面颜色定义为页面集合索引。然而,由于第一行是页对齐的,它的0-11位被设置为0,所以页面集合索引的低6位总是0。

 

 

因此我们定义页面集合索引的左边6位时页面颜色,或者是页面起始地址的12-17位。我们的页面颜色=011111b=0x1f,所有这种颜色的页面将被缓存在相同的缓存集中,索引范围从011111000000b到011111111111b

 

但是为什么页面的颜色很重要呢?因为不同页面内的两个地址的颜色相同,最终会出现在同一个缓存集中,也就是拥有相同的缓存索引,而另一方面,来自不同颜色页面的地址则保证会占据不同的缓存集。当VMM分配物理页时,它牢牢记住了这一点。例如,当为一个进程的地址空间分配一个新的物理页时,VMM试图找到一个可用的物理页,其颜色等于分配给同一进程的最后一个页的颜色加1。每个地址空间的颜色计数器在每次分配时都会被更新,以跟踪最后分配的页面的颜色。这样,VMM最大限度地减少了单一地址空间的物理页相互竞争缓存集的机会,当该地址空间是当前的时候,可能会把对方从缓存中挤出来。

 

由于缓存有16个通道,它最多可以存储16种相同颜色的页面。但以现在的内存大小,一个进程可以分配几十万个物理页面,所以将它们均匀地分布在所有可能的颜色上对提高性能是很重要的。

 

哪些地址位实际包含在页面颜色中,取决于缓存的特性(到目前为止我们看到的例子是针对英特尔酷睿2双核移动处理器)。仅仅是缓存行的大小就决定了集合索引的最低有效位,也就是从第几位开始,例如,64字节,行的开始就是第6位。行的大小、缓存的大小和通道的数量决定了每个通道有多少个项,从而决定了集合索引的位数。颜色位从第12位开始延伸到集合索引的最高有效位,因为页面大小为4kB

 

例如,英特尔酷睿i7 Q720移动处理器的L3缓存具有以下特点:

 

大小:6MB

 

通道数:12

 

缓存行:64 bytes

 

集合索引就应该是13位。

 

总的缓存行就是 6MB/64 bytes=98,304

 

每个通道的行数:98304/12=8,192

 

因此,需要13位来索引到8,192个项数。集合索引将由地址的第6-18位组成,而页面颜色位将是第12-18位,总共7位。

 

Windows使用存储在_KPRCB.SecondaryColorMask成员来过滤页面的颜色位。为了计算一个页面的颜色,VMMPFN与掩码相与。

 

在Core 2 Duo处理器中,掩码被设置为0x3f,它过滤了PFN的最右边的6位,即物理地址的第12-17位(记得PFN=物理地址右移12位)。

 

在Core i7处理器上,掩码被设置为0x7f,占了集合索引中的额外位。

3.3 页面着色和其他缓存的对比

我们一直在考虑酷睿2双核处理器的二级缓存和酷睿i7的三级缓存,但其他缓存呢?

 

一般来说,离处理器较近的缓存,如LI缓存,或者在Core i7的情况下,L2缓存,要比上一级的缓存小。只要所有的缓存都有相同的行大小,通常情况下,为最大的缓存分配连续颜色的页面会给小的缓存带来同样的好处。例如,考虑Core i7的二级缓存。

 

大小: 256kb

 

通道数:8

 

缓存行:64 bytes

 

总的缓存行 256kb/64bytes=4,096

 

每个通道的行数:4096/8=512

 

集合索引位数:9位

 

集合索引所在位:6-14

 

与该缓存有关的页面颜色位是第12-14位。

 

我们已经看到,对于这个处理器,VMM将页面的颜色计算为第12-18位的值,这与较大的L3缓存的特性是一致的。我们还知道,VMM试图通过选择具有连续颜色的页面来优化缓存中的页面位置。这意味着一个地址空间页面的物理地址位将如下表所示

 

 

 

该表按照分配的顺序列出了各页,假设第一个使用的颜色值为0,但这并不重要。我们可以看到,L2和L3缓存的颜色都是连续的,这是因为L2的颜色是L3的颜色的一个子集,而且最低有效位是一样的。当然,较小的L2会出现轮转的情况,所以在这个缓存开始有相同集合索引的行之前,需要页面就少。
综上所述,我们可以概括我们的推理:在一个由任意多级组成的缓存层次中,只要它们有相同的行大小,那么所有级别的最低有效颜色位都是一样的,以这种方式选择页面颜色可以优化页面在所有缓存中的位置。

3.4 一级缓存

最后一个说明是关于LI高速缓存的。工作集页面的物理地址对内存内容在这个缓存中的位置没有影响,因为有一个令人惊讶的细节。这个缓存不是用物理地址的位来索引的,而是用虚拟地址来索引的,而tag位仍然来自物理地址。

 

这似乎令人惊讶,因为虚拟地址被映射到物理地址,这取决于CR3的指向和分页结构的内容,因此与虚拟地址相关的项只有在映射不改变的情况下才是正确的。

 

考虑以下情况:虚拟地址VA1被映射到物理地址PA1PA1的内容被缓存在L1中。之后,虚拟到物理的映射被改变,所以VA1现在映射到PA2。如果我们使用VA1索引到LI的通道,我们找到缓存PA1的数据块,但是我们真正的需要的是PA2的内存内容。我们如何检测出缓存内容是错误的物理地址呢?

 

如果L1缓存的大小足够小,这个问题实际上是个假命题。例如,考虑Core 2 Duo Mobile和Core i7 Mobile的L1缓存,其特点如下。

 

大小:32kb

 

通道:8

 

缓存行:64bytes

 

总的缓存行 32kb/64bytes=512

 

每个通道行数: 512/8=64

 

集合索引位位数:6位

 

集合索引所在位:6-11

 

集合索引位完全包含在页偏移位中。这些位的值在物理地址和虚拟地址中都是一样的,因为虚拟到物理的映射只翻译了12-48位。第0-11位是进入页面的偏移量,在地址转换中是不变的,因此集合索引是不变的,如果我们从虚拟地址或物理地址中提取它,是没有区别的。

 

另一种情况是,缓存通道的大小小于或等于一个页面的大小;例如:

 

大小:32kb

 

通道:8

 

通道大小:4kb

 

每一个通道都存储一个内存页,并且集合索引不延伸到被翻译的虚拟地址的部分。

 

然而,有一些LI缓存的通道大于单页,例如Core i7的L1指令缓存。

 

大小:32kb

 

通道:4

 

通道大小: 8kb

 

缓存行:64bytes

 

总的缓存行 32kb/64bytes=512

 

每个通道行数: 512/4=128

 

集合索引位位数:7位

 

集合索引所在位:6-12

 

这个缓存只有4条通道,所以通道大小是8kB,横跨两个内存页。集合索引包含12位,在虚拟地址和物理地址之间可以改变。如果在数据被缓存后,虚拟到物理的映射发生了变化,这意味着通过虚拟地址索引并不能保证为所需的物理地址选择一个集合。

 

这个问题的一个可能的解决方案是在tag中包括PFN的所有位,即12-36位。这样,当一个tag与物理地址相匹配时,我们就可以确定缓存数据块存储的是正确的内存页的数据;同时,所选择的数据块与所需物理地址页面中的偏移量相对应,因为正如我们所说,偏移量不会随着虚拟到物理的映射而改变。综上所述,找到的数据块就是正确的页面和正确的偏移量,因此也就是期望的物理地址。

 

另一个解决方案是让处理器在CR3变化时刷新L1缓存,在分页结构项变化时刷新L1条目。记住,当软件修改分页结构项时,它必须通过使相应的TLB项无效来通知处理器,所以处理器才能知道虚拟到物理的映射已经改变。由于L1很小而且很快,刷新它是可以接受的。

 

但为什么要费尽心思使用虚拟地址呢?因为物理地址只有在处理器翻译了虚拟地址之后才能知道,因此,如果使用前者,在翻译完成之前就不能开始缓存查询。这意味着更高的缓存延迟,对于必须非常快的L1缓存来说是不可接受的。通过使用虚拟地址来索引,处理器可以在地址转换的同时选择缓存集;的确,最终需要物理地址来比较tag,但是现在转换和选择集合可以同时进行。另一方面,更大、更慢的缓存,如Core 2 Duo中的L2和Core i7中的L3,会有更高的延迟,并在指令执行的后期参与其中,此时物理地址已经可用。因此,这些缓存可以进行物理索引。

 

L1这样的缓存被描述为虚拟索引,物理标记;其他类型的缓存被说成是物理索引,物理标记。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞4
打赏
分享
最新回复 (2)
雪    币: 4195
活跃值: (5634)
能力值: ( LV7,RANK:116 )
在线值:
发帖
回帖
粉丝
VirtualCC 2022-3-22 16:07
2
0
----------------------------          ----9---   ---9----  ---9- --  ----9----    ------12-----
0xFFFFF70000000000 = 1111111111111111 111101110 000000000  000000000  000000000    000000000000
0xFFFFF6FB7DBEE000 = 11111111 11111111 11110110 11111011  01111101101 111101110    000000000000

0: kd> !pte 0xFFFFF70000000000
                                           VA fffff70000000000
PXE at FFFFF6FB7DBEDF70    PPE at FFFFF6FB7DBEE000    PDE at FFFFF6FB7DC00000    PTE at FFFFF6FB80000000
contains 0000000154F70863  contains 0000000154871863  contains 000000015876A863  contains 805000015776B863
pfn 154f70    ---DA--KWEV  pfn 154871    ---DA--KWEV  pfn 15876a    ---DA--KWEV  pfn 15776b    ---DA--KW-V

有一点需要注意的是,计算VA的PXE的VA时是通过移位来挪出空位,然后才填充auto-entry项的,第五部分会用到逆推。

雪    币: 1809
活跃值: (4065)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Grav1ty 2022-3-22 16:34
3
0
顶,正在学相关内容
游客
登录 | 注册 方可回帖
返回