You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Table walk unit:对于TLB entry不仅仅包含物理和虚拟地址,也包含内存属性、cache的配置、访问权限、地址空间id(address space id, ASID),虚拟机id(VMID)。如果cpu请求的地址没用命中,接着table walk unit就会被调用,用于在内存中查找请求地址。最新的请求地址会被更新到TLB内部便于重新利用。
当1=1,可缓存的, the Cacheable, Inner Write-Through Read-Allocate No Write-Allocate, Outer Write-Through Read-Allocate No Write-Allocate Outer Shareable attribute is used.
在一个使用虚拟化的系统,arm允许hypervisor去复写操作系统的默认内存类型。在第一阶段MMU被关闭的的状态下,当HCR_EL2.DC位使能数据cache之后对于非安全的EL0/1的一些访问,默认的内存类型是normal,non-shareable,inner write-back, read and write allocate, outer wirte-back read and write allocate。这个可能在cache已经被配置为hypervisor的场景下是有用的。
MSR TTBR0_EL1, X0 // Set TTBR0MSR TTBR1_EL1, X1 // Set TTBR1MSR TCR_EL1, X2 // Set TCRISB // The ISB forces these changes to be // seen before the MMU is enabled.MRS X0, SCTLR_EL1 // Read System Control Register // configuration data ORR X0, X0, #1 // Set [M] bit and enable the MMU.MSR SCTLR_EL1, X0 // Write System Control Register // configuration data ISB // The ISB forces these changes to be // seen by the next instruction
14_ARMv8_内存管理(二)-ARM的MMU设计
1 页表结构
谈及页表结构,或者我们从互联网的资源上去查,就会有个误区,无论是现代大多数的处理器,或者linux内核里面,页表的结构设计是分层设计的,也就是多级页表(multiple-level page table),实际上并不是这样的,在不同的处理器上面也采用不同的页表充当内存管理的枢纽。这里我们引用操作系统概念内部提出的三种页表结构:分层页表(multiple-level page table),哈希页表(hashed page table)。
1.1 分层页表(multiple-level page table)
现代计算机系统多为32位或者64位(最大逻辑寻址空间2^32或者2^64),假设一个页面大小为4KB (2^12)大小,32位的页表就需要有(2^20)条目,如果每个条目占4个字节,那么整个页表就占了2^22。页表是和进程绑定的概念,每个进程都该有一张页表,试想一下,如果每个进程启动光页表就占了2^22大小,这会造成很大的资源浪费。因此,有人提出了分层页表(multiple-leve page table)的概念,用以节约页表的空间。
我们先研究一下两层页表结构:
最左边是第一级页表(outer page table),一级页表负责存储的是二级页表每个页的首地址,二级页表中存的是在内存中的地址。因此,在硬件上面,我们需要一级页表的页码数据p1,还需要二级页表的数据p2,还需要二级页表的偏移数据d,有这三个数据就可以拿到想要访问的数据了。
采用这种结构地址是由外向内的,这种方案称为向前映射页表(forward-mapped page table)。
从进程ELF的角度看,ELF会对代码的段合并,然后按照页帧的大小进行分页处理,这个时候虚拟内存会进行编址,这个就是大名鼎鼎的VMA了。我们来看一下vma的规律(这个不一定代表arm架构,只是一种思路)。最高位(红色)表示第一级页表的位置p1,中间(绿色)表示二级页表的位置p2,最低位(蓝色)表示偏移d。
这里需要注意的是:
现在我们计算一下二级页表可以容纳多少内存:
d = log (page size)
p1 >= log (# entries in level-1 table)
p2 >= log (# entries in level-2 table)
# entry in level-1 table
*# entry in level 2 table
*`` page size`2.2 哈希页表(hashed page table)
我这里说一下hash表,之前学过算法和数据结构但对这个哈希表已经就饭吃了。我们加密算法中hash是算摘要的,无论输入的数据有多长,就会输出一串固定长度的十六进制的字符串数据,而字符串的长度可以是md5、sha224、sha256这些hash算法决定的。所以我们利用hash这个特性,一个固定的input快速的输出一个固定的output1。但是hash运算可能会存在碰撞的问题2。hashed page table就是利用hash元函数,输入不同的地址,然后得到一个唯一的输出然后取长度模,这样就可以快速的查到值了。
处理大于32位地址空间的常用方法是使用哈希页表(hashed page table),采用虚拟页码作为哈希值,哈希页表PTE是一个链表结构。如图所示3:
2 ARM架构的MMU页表
操作系统概念给我们提供了一个理论基础,所有的架构设计都无法脱离这些最基础的思想,因此在ARM架构上面也能体现出这种思想。ARMv7和ARMv8架构使用了分层页表设计(multi-level page table),ARMv7架构因为是32位的架构处理器,因此需要二级页表就可以满足了4;但是ARMv8架构最高支持到64位,因此在64位的模式下,ARMv8使用了四级页表,这里需要注意的是,ARMv8只有64位执行状态下才会使用4级页表,而在32位下又有另一种配置,除此之外,ARMv8不同的异常等级,使用的MMU页表形态也大有不同5,我们后面展开来讲。
2.1 ARMv7的页表设计
ARMv7是一个32位的处理器架构,因此虚拟内存有4GB大小的寻址空间。ARMv7采用的是分层页表设计,并且分了两层。在ARMv7架构上面将页表分为L1和L2。在L1页表包含了所有的4GB的虚拟地址空间(4096条32位长度的记录PTE(page table entry)),因此每一条记录(PTE)可以代表1Mb的虚拟内存6。在CPU虚拟地址(virtual address)编址上,高12位(31:20)表示L2的入口地址p1,中8位(19:12)表示L2的入口地址p2,低12位(11:0)表示二级页表的偏移d。
这里需要注意的是,armv7架构在经典版本中,都是二级页表设计但是大物理内存地址扩展LPAE(large physical address extension)的MMU设计有三级页表7。在armv7-a早起的是实现,如cortex-A8和cortex-A9就不支持LPAE。LPAE是在编译阶段设定的,默认的armv7的配置不能默认的去使能,否则legacy版本的armv7就不能使用了。到了ARMv8架构上,LPAE已经被默认使能。
2.2 ARMv8的MMU设计
2.2.1 设计要素
组成元素
MMU在ARM架构上不只是页表设计,还包含其他机制,一个完成的MMU,包含TLB、table walk unit、cache、memory还有memory的TLB。
映射规则
那么虚拟地址和物理地址的映射规则如何呢?
空间分离
kernel和application的虚拟地址空间分离。在操作系统运行中,是允许多个应用程序或者任务并发运行的,每一个进程都有一个自己独立的应用空间转换表,但是kernel space作为一个进程的上下文,从一个进程切换到另一个进程。然而,在kernel space中有大量的经常使用的指令和数据,因此在设计上处理器会把这部分空间地址固定映射,这也是操作系统概念提到的通用做法。armv8-a在架构层面需要做出这样的支持8。
首先,通过TTBR0_EL1和TTBR1_EL1分别指定基地址;其次,通过地址的一些高位来判断cpu是选择TTBR0的地址还是TTBR1的地址,如果虚拟地址的高位都是0,则选择TTBR0,反过来这些位如果是1,那么就选择TTBR1的地址。在armv8-a上面虚拟地址的高16位来做这项判定。
aarch64的执行模式,取指令和数据访问的虚拟地址都是64位的,其中16已经被占据用以区分是kernel或者userspace的映射,只剩下48位的寻址能力用来映射两个区域到物理内存上面,如图所示,高16位指示kernel space还是userspace,低48位指定的是物理空间,这样的话kernel 和 user可以共用这块物理地址,而在虚拟内存上是分离的。这里还需要注意的是,EL2和EL3是hypervisor和secure monitor的异常模式,不存在kernel space的概念,只有TTBR0,没有TTBR1,这就导致了EL2/3下面,前16位被浪费掉,只能寻址48bit的物理内存。* 注:48位地址空间可以输出的寻址范围256TB,在现在这个时代已经大的不可想象了。 **
高16位和低48位的这样的分割,实际上也是可以控制的,我们通过TCR_EL1寄存器(Translation control register)可以控制有效位的检查。
这个寄存器中含包TCR_EL1包含长T0SZ[5:0]和T1SZ[5:0],用来控制输入地址的的最大值。
IPS字段用来配置地址转换后的物理地址的最大值。
TGx用来配置TTBRx页表的粒度大小。
我们可以配置用于第一个索引的转换表的等级。一个完整的转换流程需要三到四级的页表。我们不需要完全是实现每一级页表。用于第一个索引的页表也就是,被TxSZ的大小决定页表的级数。
多处理器
那么我们场景更复杂一点,在SMP对称多处理器背景下,如何完成同一块物理内存的映射,他们不打架吗?
关于这个问题,在stackoverflow上面有激烈的讨论9,我也没有看出一个所以然。
cpu0和cpu1是共享一份页表的,当其中一个cpu修改了页表,就必须让另一个cpu知道,这里又个非常重要的多核同步TLB机制,BBM(Break-Before-Make),用以保持TLB的coherency。
但是这个机制还有个问题,页表被多个cpu进行了共享,在刷新BBM的时候,这些cpu可能正在运行,所以这个就造成了,TLB的caching可能会破坏一致性(一个 处理器可能看见一个错误的数据或者一个被覆盖的数据)或者TLB冲突会产生10。这个我们后面在讨论。
内存属性11
ARMv8-a体系架构处理器主要提供两种内存属性,普通类型的内存(Normal memory)和设备类型的内存(Device memory)。arm提出这两个memory models主要是为了管理和组织内存的行为,因此就在内存上面引入了很多属性,其实这也是跟随操作系统的概念为操作系统而服务的。如图所示,在操作系统中会提出外部io的映射、有内核的数据和指令、有应用程序,所以要对这些OS的模组使用的内存进行一些属性描述。
那么这些内存属性和我们的MMU有什么关系呢?我们MMU页表上面有对这部分的描述。
而且由于arm的页表是多级页表,也具备多级页表之间的属性继承。
normal memory
普通类型内存实现的是弱一致性(weakly ordered)内存模型,没有额外的约束,可以提高内存的访问性能。通常代码段、数据段等这些内存段会被放在普通的内存里面。普通能可以让处理器做很多有话,比如分支预测、数据预测、高速缓存行预取和填充、乱序和加载等硬件优化。在OS里面,kernel data,kernel指令,用户空间的数据和指令都是普通类型内存。
所以在normal memory中存在re-ordering的序的问题,这个部分我们在barrier的专题来讲解,反正要记住,barrier的作用域仅仅是normal memory。
device memory
在OS内部,设备类型的内存主要是peripherals的映射,包含内存映射IO。设备类型内存会有很多的限制,设备类型内存都是严格按照指令顺序执行的。通常设备内存留给设备来访问。若系统中所有的内存都设置为设备内存就会有很大的副作用。例如:
设备内存无法缓存,这是因为有很少的可能性让cache来访问外围器件。
指令不应该被标记为设备内存,arm推荐设备区域总是被标记为不可执行的,否则处理器可能会从设备内存取指,这个对一些读敏感的设备造成一些挑战,比如FIFO设备。
在设备内存的世界也具备一些属性:
对于内存属性,提供如下四种:
内存属性的描述,是在MAIR_ELx寄存器中。(memory attribute indirection register)。MAIR被分为8段,每一段都可以用于描述不同的内存属性。
2.2.2 ARMv8的页表设计
页表种类
这里有四个类型的页表:
这里的每一个页表都是独立的,都有自己的配置和页表实体。
aarch64执行状态的MMU支持单一阶段的页表转换,也支持虚拟化扩展中的两个阶段的页表转换。单一阶段的页表转换,直接把虚拟地址(VA)转换为物理地址(PA)。然而在虚拟化扩展的两个阶段的页表转换中,如图所示10,在阶段1先把虚拟地址转换为中间物理地址(Intermediate physical address, IPA),第二阶段的时候,IPA转换为PA,我们把第一个阶段叫做 guest translation,转换过来的IPA被OS认为就是物理地址空间,第二阶段叫做host translation,这部分是由hypervisor来控制一系列的转换。
在aarch32的执行状态下,继承armv7的页表格式来运行32位的内存的应用。
页表映射
单页表映射
当一个处理器带着一个虚拟地址发起一次数据访问或者取指令,MMU硬件会把虚拟地址转译成物理地址。对于一个在n位地址的空间的虚拟地址,那么他的高64-n比特必须是0或者1,否则这个地址的访问会触发错误。这个原因我们在要素中已经说明,高N位表示的是kernel space或者user space。
我们来研究一下armv8-a架构的页表映射过程,这里假设只有一级页表,并且使用64kb的物理帧页力度,42位的虚拟地址空间。
多级页表映射
实际上,如此简单的页表严重限制了地址空间的划分。取而代之的方案就是使用多级页表转换:
aarch64最高可以支持4级页表,在一些x86_64体系架构中也支持125级页表,如图所示13作为一个5级页表的设计例子在x86_64架构上面。最终页表的支持的级数是和处理器架构、操作系统内核有关的。
安全与非安全
从安全和非安全的角度来看,arm定义了两个物理地址空间,安全地址空间和非安全地址空间。在理论上,两个地址空间应该是相互平行和独立,一个系统也应该被设计出隔离两个空间的属性。然而在大部分真实的系统中,指示把安全和非安全作为一个访问属性,而非两个真实的隔离的空间,所以安全和非安全的概念是逻辑上的,而不是物理上的。我们设定,normal world只能够访问被标记为非安全的空间,而secure world有着更高的权限,既可以访问安全的空间,又可以访问非安全空间。实现这个控制,是在页表上做的手脚。
在安全世界和非安全世界也有cache一致性的问题。例如,由于安全地址0x8000和非安全地址0x8000是逻辑上不同的地址,但是他们可能会在cache中共存。有一个理想的手段就是禁止安全和非安全的互相访问,但实际上我们只要禁止非安全向安全的访问就可以。为了避免安全世界的冲突,必须使用非安全访问非安全的空间,在安全的页表中加上非安全的映射。
多重虚拟地址空间
任何时候,只有一个虚拟地址空间被使用(在当前的安全状态异常等级)。然而,在概念上,由于有三个不同的TTBRs,因此就有三个不同的平行的虚拟空间(EL0/1,EL1和EL3)
在一个系统里面也可能有安全和非安全的EL1/0,当切换world的时候,安全监视器需要保存和恢复这些寄存器。
MMU关闭之后
当MMU被使能之后,MMU就可以进行页表控制。相反,当MMU被屏蔽,比如在arm复位之后的很短的时间,内存处于默认的状态。
默认状态时,arm用于取指的类型是一个带着被SCTRL_ELx.1控制的可缓存的属性:
在一个使用虚拟化的系统,arm允许hypervisor去复写操作系统的默认内存类型。在第一阶段MMU被关闭的的状态下,当HCR_EL2.DC位使能数据cache之后对于非安全的EL0/1的一些访问,默认的内存类型是normal,non-shareable,inner write-back, read and write allocate, outer wirte-back read and write allocate。这个可能在cache已经被配置为hypervisor的场景下是有用的。
写系统寄存器来控制MMU是一些上下文切换的事件,在这里并没有ordering的要求对于写系统寄存器。上下文切换的事件不会被保证能被观察到,除非产生一个上下文的同步事件。
页表格式
上面的页表由于也是分层页表,所以在设计格式上面肯定有不一样的地方,在aarch64的执行状态下,独有的长描述符转页表格式(long descriptor translation table format);armv7和aarch32的执行状态,有对应的短描述符转页表格式(short descriptor translation table format)与前面提到的大物理地址扩展(LPAE)。ARMv8体系结构,支持4KB, 16KB, 64KB三种帧面粒度。
AArch64的描述符格式
描述符格式在EL0/1/2/3都有涉及,比较特殊的就是一级和四级的页表格式,L0~L3页表描述符格式完全不一样。L0~L2的页表比较类似(前三级)
L0~L2的页表项可以分为三类,这个页表的类型是通过最低1位来指示的:
简单的说,在这个等级,如果有下一级页表的存在,那么就启动页类型页表,如果没有下一级的存在了,那么这一级就启动块类型页表。
页表颗粒度
我们在上面的要素中提到,armv8-a体系架构的页表的颗粒度有三种选择,4K/16K/64K。那么如果不同颗粒度大小的页表在页表的格式体现上也是很有影响的。这个作用只做用到最后一级页表上面。
4KB
在4KB granule,硬件会使用4-level的页表,48bit的地址对这个配置是最好的选择,输出地址对应bit[47:12]
16KB
在16KB granule,硬件会使用4-level的页表,输出地址对应bit[47:14]
64KB
在64KB granule,硬件会使用3-level的页表,输出地址对应bit[47:16]。
页表属性
共享性与缓存性:指的是页面是否使能了高速缓冲及高速缓冲的范围。只有普通内存可以使能高速缓存,通过页表项attrIndx[2:0]来配置。另外还可以指定缓存属性是属于内部共享还是外部共享。内部共享指的是只能是cpu内部的成员才能访问的,外部的共享是核和核之间的共享。
访问权限:访问权限控制的是页面的可读性、可写性权限。通过AP[2:1]来控制。
需要注意的是,AP[1]为1的时候,非特权和特权模式具有相同的访问权限,这就有个问题了。特权模式下内核态是可以访问用户态的任意空间的。攻击者可以在内核态任意访问用户态的代码。为了修复这个漏洞,ARM v8.1 架构里面新增了PAN(特权禁止访问)的设计,在PSTATE的中新增了一位PAN。当内核态访问用户态内存的时候会触发一个访问权限异常,从而限制在内核态恶意访问用户态的内存。
执行权限:页表属性通过PXN字段以及XN/UXN字段来设置CPU是否对这个页面具有执行权限。
访问标记位: 页面有个属性是访问标记,AF(Access Flag),用来指示页面是不是被处理器访问过。操作系统中的页面回收依赖这个特性。
全局和进程特有TLB:nG字段,用来设置TLB的类型。TLB的表项分成全局和进程特有的。当nG为1的时候,表示页面对应的TLB时进程特有的,反之,TLB是全局的。
脏位(dirty state)
3 问题回顾
Q1: 为什么页表要设计成为多级页表?直接使用一级页表是否可行?多级页表又引入了什么问题?
现代处理器架构大部分超32位的,我们以32为例子,32位寻址空间是4GB,如果我们只使用一级页表,每一个进程创建的时候,都会创建一个非常大的页表。因此我们引入多级页表,第一级页表存放第二级页表的索引,次级页表按需创建,只有在进程需要的时候才去创建次级页表,大大节约了空间。但问题是,增加了页表层级,索引次数就会上升,正常我访问一个内存如果一级页表我只索引一次,如果多级页表,还要拿着次级页表的索引再去查地址,这就导致了效率减少50%,试想,如此频繁的进程和内存交换数据的过程,就会产生很大的效率浪费。目前主流的方法是引入TLB机制,快表查询,预加载一部分次级页表的地址在TLB中。
REF
Footnotes
j - 数据结构实验:哈希表_一看就懂的数据结构基础「哈希表」 ↩
哈希碰撞 ↩
Hash, Don't Cache (the Page Table) ↩
ARM® Cortex™ -A Series Version: 4.0 Programmer’s Guide ↩
ARM® Cortex® -A Series Version: 1.0 Programmer’s Guide for ARMv8-A ↩
SUDONULL - Virtual memory in ARMv7 ↩
Linusw - ARM32 Page Tables ↩
Armv8-A Address Translation ↩
Stackoverflow - Do multi-core CPUs share the MMU and page tables ↩
XPDS16: Keeping coherency on ARM - Julien Grall, ARM ↩ ↩2
Learn the architecture: AArch64 memory model ↩
x86-specific Documentation » 22. x86_64 Support » 22.4. 5-level paging ↩
Intel 5-level paging ↩
The text was updated successfully, but these errors were encountered: