(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)

Arm32 MMU 页表项属性

本篇文章接上一篇 (番外一)Arm32 中虚拟地址机制分析(Arm cortex-A系列 MMU工作机制分析) ,在上一篇文章中根据手册(《Arm Cortex-A Series Programmer’s Guide》)简单分析了Arm32中多级页表的简单原理,即 MMU 如何通过虚拟地址转换表将 CPU 发出的虚拟地址访问指令定位到具体的物理地址,本篇主要深入了解一下每一个页表项的属性对物理内存的访问的影响,首先贴出 Arm32 一级页表与二级页表结构图:

一级页表
(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)
二级页表
(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)
可以看到每个页表项中不仅包含了下级页表或者物理地址的映射地址(除了无效页表项),而且包含了诸如:TEX S SBZ AP 等属性位,这些属性位影响着 CPU 对内存的访问动作,下边先对 Arm32 对内存种类分配的定义进行简单分析,然后再引入 Arm32 如何通过属性来实现这些分类


Arm32 中内存的种类

借用手册中的话:早期的 Arm 架构允许开发人员通过简单的配置来指定 CPU 对内存页的访问动作,包括是否允许在某些内存地址上启用 cache 或者 写缓冲(write buffer)。但是对现如今越来越复杂的系统而言,这些简单的配置机制已经无法满足上层软件对硬件的要求,比如说存在多级缓存,多处理器共享内存等,所以在 Armv6开始,新的内存种类(memory type)定义被加入进来,并且在 Armv7中得到扩展。

在 Armv7 中,内存种类一共分为三种,如下所示,所有被映射到虚拟地址的内存页都应该被初始化为其中一种:

  • Strongly-orderd
  • Device
  • Normal
Normal

普通内存,也是系统中最常用的内存种类,没有特殊的映射机制,并且允许指令重排(处理器为了优化程序运行效率的一种机制,可以百度指令重排来简单了解其概念),这种内存可以分为两种,可共享与非可共享,其中非可共享(Non-Shareable)的内存被认定只被一个处理器内核所使用,即不需要解决缓存一致性的问题(不需要担心不同处理器对内存修改以后其他内核的一级缓存没有同步过来的问题),而可共享(Shareable)的内存被认定为会被多个处理器内核所访问,需要通过软件来保证每个内核的缓存是同步的,普通内存的可共享与非可共享问题主要是由缓存技术的存在而引出的,所以这块知识牵扯到缓存(cache)的原理,在之后的博客中会进行具体分析。

Device

设备内存,Arm32 通常使用直接内存映射的方法进行外设寄存器的访问,即将外设寄存器地址映射到内存地址中,可以通过 ldr str 等内存操作指令对其进行读写。这不同于 x86 的处理器,做过 8086 实验的同学可能清楚,使用 in out 等指令可以对设备接口地址进行访问,而这些地址在逻辑上给人的感觉就是,内存地址是一个独立的空间,设备接口地址又是另一个独立的空间,两者没有什么关联,无法通过 mov 等指令对设备寄存器或缓冲区进行操作。通过阅读 intel 白皮书(想要intel 4册白皮书可以在下方留言邮箱 ^_^)我们知道,其实 intel 后几代处理器也支持内存直接映射的方式来操作设备了,内存直接映射的方式是很好的,不仅减轻了跨平台代码的开发难度,也让设备数据读写变得更加有效率(这块不是本篇重点,可以百度一下相关知识进行了解)。

基于上述 Arm32 特意分出了设备内存这个内存种类,专门用来映射外设地址空间(就是那个通过直接内存映射方法将外设寄存器、缓冲区等映射到的内存地址空间)。设备内存的特点就是不支持指令重排,程序对设备内存访问的顺序是完全按照程序指令定义的访问顺序,处理器不会对其进行指令重排优化

Strongly-order

强顺序性内存,非常好理解,即不支持指令重排,所有对强顺序性内存的访问都严格遵循程序指令定义的访问顺序,并且这种内存是可共享的。

上文只是根据手册中的表格进行翻译和简单解释,手册中对内存种类进行解释的表格如下图所示:
(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)
知道了三种内存以后,我们需要如何配置映射表来将某块内存配置为某一种内存呢?还记得虚拟地址转换表项中包含的属性位么,开发人员通过设置属性位的值来对某块内存进行种类初始化,需要设置的属性位有三个:TEX C B ,需要知道的是,不仅可以通过这三个属性位来设置内存的种类,也可以设置对其的缓存策略(cache policy),而且手册中的意思是,这三个标志位主要是用来规定内存的缓存策略。通过上表可以看到除了普通内存外,其他两种内存都是禁用缓存的。下图是手册中的表格,列出了不同的配置组合所对应的内存种类与缓存策略,这个表格写的很详细,下文只对特定的地方进行简单解释:
(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)
通过上表可以看到设备内存也分为可共享与非可共享两种。但是从手册上得知,在 Armv7中并没有使用设备内存共享机制,所以不去具体分析。在上表中可以看到 write allocate|Inner and Outer write-through|Cacheable 可共享、非可共享 等概念,这些概念都是与缓存机制息息相关的,之后的博客会在具体分析缓存技术时进行具体分析,并且在后续的博客中会具体分析 Arm32 对指令重排技术的管理与限制。


其他属性

除了 TEX C B 用来设置内存种类与缓存策略外,转换表项的其他属性位定义了对特定内存的其他访问动作限制,下边来一一介绍:

XN

在二级转换表(除了无效映射表项外)的表项都具有 XN 属性位,而且以及转换表除了无效表项与映射二级转换表的表项是没有 XN 属性位以外,其他表项都具有 XN 属性位,所以可以得出,只要直接映射了物理地址的表项都具有 XN 属性的,这是因为 XN 属性是直接限制物理内存访问动作的,所以一级表项中用来映射二级转换表基地址的表项是不需要 XN 的,可以通过二级转换表的表项来设置 XN。

Arm32 中 XN 属性位被称作 Execute Never ,永不执行,设置了 XN 属性的转换表表项所映射的内存中的数据不可以被当作指令执行,只能当作普通数据。如果程序代码 bug 导致处理器从此类内存中取数据,则会触发预取指令终止异常(prefetch abort exception),操作系统需要提供预取指令终止异常的中断服务函数来处理此类情况,通常会终止进程的运行并给出 segment fault,并保留错误现场来进行核心转储给开发者进行调试。通常情况下设备内存(三种内存中的一种),一般会被标志为永不执行状态

APX/AP

APX/AP 被称作访问权限标志位,规定了映射到的内存的访问权限:可读、可写、只读等等。APX/AP 标志位的设置对应的权限限制描述如下图所示,这张图是手册中的一个表格:
(番外二)Arm32 中虚拟地址机制分析(页表项属性分析)
如果内存访问动作越权,则会导致异常中断,处理器如果越权访问数据则会导致数据终止异常(precise data abort exception),如果处理器越权取指令,则会导致预取指令终止异常(prefetch abort exception),错误发生的位置和原因会被存储到 CP15 协处理器的寄存器(the fault address and fault status registers)中,操作系统需要提供中断处理函数并根据 CP15 中的中断信息来进行相应的操作,或者修改虚拟地址转换表的表项(修改访问权限)或者终止进程的运行并给出错误信息。

S

标志某块内存的共享性,设置了标志位 S 的表项所映射的物理内存是可共享的,反之则是非可共享的。


Arm32 页表机制对多任务操作系统的支持

多任务操作系统的定义(来自百度百科 ):

所谓多道程序设计是指允许多个程序同时进入一个计算机系统的主存储器并运行这些程序的方法。这种多道程序系统也称为多任务操作系统。

就是说操作系统支持多个进程同时加载到内存中,保持运行、就绪等状态,并通过调度程序进行调度运行。多个进程同时加载到内存中就意味着需要为多个进程开辟不同的,无冲突的内存地址空间,并且还需要让这些进程在逻辑上拥有整个 4GB 内存的寻址能力(在32位的架构下),这就需要虚拟内存的技术来实现。通过之前的分析我们知道虚拟内存到物理内存的映射转换需要虚拟内存转换表的支持,而一个页表体系(一个一级页表与一个或多个二级页表组成的映射关系体系)只能映射一个独立的虚拟内存地址空间,翻译成人话就是一个页表体系对一个虚拟内存地址只能映射到一个固定的物理内存地址上,而不能支持一个虚拟内存地址映射到多个物理内存地址上,而我们也知道在虚拟内存技术的支持下,进程逻辑上认为自己拥有全部 4GB 内存的寻址能力,所以不同进程很可能会访问同一个虚拟内存地址,为了不让进程之间产生冲突,操作系统不能只维护一个页表,而是需要为每个进程都维护一个独立的继承页表来转换每个进程对同一个虚拟地址的访问映射。

当进程发生切换时,需要让当前处理器的MMU根据这个进程自己的页表来进行虚拟地址转换工作,这就需要让MMU知道这个进程自己的页表所在的物理内存地址才行。从前边的分析中知道,MMU通过 CP15 协处理器的 C2 寄存器来获取页表的物理内存基地址,这就意味这在进行进程切换时,我们需要修改 CP15 C2寄存器的值,让其指向这个进程自己的页表的物理内存基地址。

简单的来说,Arm32 页表机制对多任务操作系统的支持就是上文所说的这些,但这也引出了几个问题:

  • 如果每次进行虚拟地址转换都需要多次访问内存中的页表的话,效率时非常低的。CPU 的速度要比内存的速度高很多,所以一个简单的想法就是每次进行以虚拟地址转换后,都将结果存储到一个高速缓存中(cache),之后对同样的虚拟地址的访问都先在这个缓存中进行查找,如果找不到才会去内存中查找页表
  • 总有一些物理地址是每个进程都需要进行访问的,如系统调用的中断服务函数(在此声明,这篇文章只会考虑单内核的情况,像外核、多核等架构不在考虑范围内), Linux 的做法是将内核的内存地址空间映射到每个进程的虚拟内存地址空间中的固定位置,这样进程发出软中断陷入系统时,处理器就会根据当前页表来找到中断服务函数的具体位置然后执行。有些人可能会产生一种想法,就是在进行系统调用时,同时切换页表基址寄存器来使用另一个页表来映射系统内存地址空间,这样就不用为每个进程页表开辟系统内存地址空间的映射了,在内存紧缺的环境下也会是一个不错的节省内存的方法。

这两个问题已经通过目前的技术手段解决了,问题一使用高速缓存 TLB 来加快虚拟地址转换,问题二在Arm32 中使用页表基址寄存器 0-1 来分别映射进程与系统的虚拟地址空间来节省内存消耗,下文会对其分别进行简单分析:

未完待续 --> 广告:本人有意向开发自己的实验用操作系统,目前想在 Arm32 的平台上进行开发,并且打算采用微内核的架构,希望与各界大佬一起交流技术 --> QQ群:1126761053

TLB

简单地说,TLB的作用就是暂时保存虚拟地址映射到物理地址的信息

页表基址寄存器 0-1

Arm32 中,这两个页表基址寄存器分别叫做 TTBR0 TTBR1