登录后台

页面导航

本文编写于 397 天前,最后修改于 397 天前,其中某些信息可能已经过时。

基础知识

在操作系统还没有出来之前,程序存放在卡片上,计算机每读取一张卡片就运行一条指令,这种从外部存储介质上直接运行指令的方法效率很低。后来出现了内存存储器,也就是说,程序要运行,首先要加载,然后执行,这就是所谓的“存储程序”。这一概念开启了操作系统快速 发展的道路,直至后来出现的分页机制。

对于固定分区,在系统编译阶段,内存被划分成许多静态分区,进程可以装入大于或等于自身大小的分区。固定分区实现简单,操作系统的管理开销比较小。但是缺点也很明显:一是程序大小和分区的大小必须匹配;二是活动进程的数目比较固定;三是地址空间无法增长。

动态分区的思想就是在一整块内存中划出一块内存供操作系统本身使用,剩下的内存空间供用户进程使用。当进程A运行时,先从这一大片内存中划出一块与进程A大小一样的内存供 进程A使用。当进程B准备运行时,从剩下的空闲内存中继续划出一块和进程B大小相等的 内存供进程B使用,以此类推。这样进程A和进程B以及后面进来的进程就可以实现动态分区了。

这种动态分区方法在系统刚启动时效果很好,但是随着时间的推移会出现很多内存空洞,内存的利用率随之下降,这些内存空洞便是我们常说的内存碎片。为了解决内存碎片化的问题, 操作系统需要动态地移动进程,使得进程占用的空间是连续的,并且所有的空闲空间也是连续 的。整个进程的迁移是一个非常耗时的过程. 总之,不管是固定分区还是动态分区,都存在很多问题。

站在内存使用的角度看,进程大概在3个地方需要用到内存。

  • 进程本身。比如,代码段以及数据段用来存储程序本身需要的数据。
  • 栈空间。程序运行时需要分配内存空间来保存函数调用关系、局部变量、函数参数以 及函数返回值等内容,这些也是需要消耗内存空间的。
  • 堆空间。程序运行时需要动态分配程序需要使用的内存,比如,存储程序需要使用的 数据等。

不管是刚才提到的固定分区还是动态分区,进程需要包含上述3种内存。但是,如果直接使用物理内存,在编写程序时,就需要时刻关心分配的物理内存地址是多少、内存空间够不够等问题。 后来,设计人员对内存进行了抽象,把上述用到的内存抽象成进程地址空间或虚拟内存。
进程不用关心分配的内存在哪个地址,它只管使用。最终由处理器来处理进程对内存的请求, 经过转换之后把进程请求的虚拟地址转换成物理地址。这个转换过程称为地址转换(address translation),而进程请求的地址可以理解为虚拟地址(virtual address)。 我们在处理器里对进程地址空间做了抽象,让进程感觉到自己可以拥有全部的物理内存。进程 可以发出地址访问请求,至于这些请求能不能完全满足,那就是处理器的事情了。总之,进程地址空间是对内存的重要抽象,让内存虚拟化得到了实现。进程地址空间、进程的CPU虚拟化 以及文件对存储地址空间的抽象,共同组成了操作系统的3个元素。

image-20230904193538755

虚拟内存机制可以提供隔离性。因为每个进程都感觉自己拥有了整个地址空间,可以随意访问,然后由处理器转换到实际的物理地址,所以进程A没办法访问进程B的物理内存,也没办法做破坏。后来出现的分页机制可以解决动态分区中出现的内存碎片化和效率问题。 进程换入和换出时访问的地址变成相同的虚拟地址。进程不用关心具体物理地址在什么地方。

分段机制

分段机制的基本思想是把程序所需的内存空间的虚拟地址映射到某个物理地址空间。分段机制可以解决地址空间保护问题,进程A和进程B会被映射到不同的物理地址空间,它们在物理地址空间中是不会有重叠的。因为进程看的是虚拟地址空间,不关心实际映射到哪个物理地址。如果一个进程访问了没有映射的虚拟地址空间,或者访问了不属于该进程的虚拟地址空间,那么CPU会捕捉到这次越界访问,并且拒绝此次访问。同时CPU会发送异常错误给操作系统,由操作系统去处理这些异常情况,这就是我们常说的缺页异常。另外,对于进程来说,它不再需要关心物理地址的布局,它访问的地址位于虚拟地址空间,只需要按照原来的 地址编写程序并访问地址,程序就可以无缝地迁移到不同的系统上。

基于分段机制解决问题的思路可以总结为增加虚拟内存(virtual memory)。进程运行时看到的地址是虚拟地址,然后需要通过CPU提供的地址映射方法,把虚拟地址转换成实际的物理地址。当多个进程在运行时,这种方法就可以保证每个进程的虚拟内存空间是相互隔离的,操作系统只需要维护虚拟地址到物理地址的映射关系。

虽然分段机制有了比较明显的改进,但是内存使用效率依然比较低。分段机制对虚拟内存到物理内存的映射依然以进程为单位。当物理内存不足时,换出到磁盘的依然是整个进程,因此会有大量的磁盘访问,进而影响系统性能。站在进程的角度看,对整个进程进行换出和换入 的方法还不太合理。在运行进程时,根据局部性原理,只有一部分数据一直在使用。若把那些不常用的数据交换出磁盘,就可以节省很多系统带宽,而把那些常用的数据驻留在物理内存中也可以得到比较好的性能。因此,人们在分段机制之后又发明了一种新的机制,这就是分页 (paging)机制。

分页机制

程序运行所需要的内存往往大于实际物理内存,采用传统的动态分区方法会把整个程序交换到交换磁盘,这不仅费时费力,而且效率很低,后来出现了分页机制,分页机制引入了虚拟存储器的概念。分页机制的核心思想是让程序中一部分不使用的内存可以存放到交换磁盘中,而程序正在使用的内存继续保留在物理内存中。

在使能了分页机制的处理器中, 我们通常把处理器能寻址的地址空间称为虚拟地址(virtual address)空间。和虚拟存储器对应的是物理存储器(physical memory),它对底着系统中使用的物理存储设备的地址空间,比如DDR内存颗粒等。在没有使能分页机制的系统中,处理器直接寻址物理地址,把物理地址发送到内存控制器;而在使能了分页机制的系统中,处理器直接寻址虚拟地址,这个地址不会直接发给内存控制器,而是先发送给内存管理单元(Memory Management Unit, MMU)。MMU 负责虚拟地址到物理地址的转换和翻译工作。

在虚拟地址空间里可按照固定大小来分页,典型的页面粒度为4KB,现代处理器都支持大粒度的页面,比如16 KB、64KB甚至2 MB的巨页。而在物理内存中,空间也分成和虚拟地址空间大小相同的块,称为页帧(page frame), 程序可以在虚拟地址空间里任意分配虚拟内存,但只有当程序需要访问或修改虚拟内存时, 操作系统才会为其分配物理页面,这个过程叫作请求调页(demand page)或者缺页异常(page fault) 。

虚拟地址VA[31:0]可以分成两部分:

  • 一部分是虚拟页面内的偏移量,以4KB页为例,VA[11:0]是虚拟页面偏移量;
  • 另一部分用来寻找属于哪个页,这称为虚拟页帧号(Virtual Page Frame Number, VPN)。

物理地址中:

  • PA[11:0]表示物理页帧的偏移量
  • 剩余部分表示物理页帧号(Physical Frame Number, PFN)。

MMU的工作内容就是把虚拟页帧号转换成物理页帧号。 处理器通常使用一张表来存储VPN到PFN的映射关系,这张表称为页表(Page Table, PT)。 页表中的每一项称为页表项(Page Table Entry, PTE)。若将整张页表存放在寄存器中,则会占用很多硬件资源,因此通常的做法是把页表放在主内存里,通过页表基地址寄存器来指向这种页表的起始地址。如图所示,处理器发出的地址是虚拟地址,通过MMU查询页表,处理器便得到了物理地址,最后把物理地址发送给内存控制器。

image-20230904193548194

通常操作系统支持多进程,进程调度器会在合适的时间(比如当进程A使用完时间片时)从进程A切换到进程B。另外,分页机制也让每个进程都感觉到自己拥有了全部的虚拟地址空间。为此,每个进程拥有一套属于自己的页表,在切换进程时需要切换页表基地址。比如,对于上面的一级页表,每个进程需要为其分配连续物理内存,这是无法接受的,因为这太浪费内存了。为此,人们设计了多级页表来减少页表占用的内存空间。把页表分成一级页表和二级页表,页表基地址寄存器指向一级页表的基地址,一级页表的页表项里存放了一个指针,指向二级页表的基地址。当处理器执行程序时,只需要把一级页表 加载到内存中,并不需要把所有的二级页表都加载到内存中,而是根据物理内存的分配和映射情况逐步创建和分配二级页表。

当操作系统准备让进程运行时,会设置一级页表在物理内存中的起始地址到页表基地址寄存器中。进程在执行过程中需要访问物理内存,因为一级页表的页表项是空的,这会触发缺页异常。在缺页异常里分配一个二级页表,并且把二级页表的起始地址填充到一级页表的相应页表项中。接着,分配一个物理页面,然后把这个物理页面的PFN填充到二级页表的对应页表项中,从而完成页表的填充。随着进程的执行,需要访问逃来越多的物理内存,于是操作系统逐步地把页表填充并建立起来。

内存管理

ARM64处理器内核的MMU包括了TLB和页表遍历单元(table walk unit TWU)。TLB是一个高速缓存,用于缓存页表转换的结果,从而缩短页表查询的时间。

一个完整的页表翻译和查找的过程叫作页表查询,页表查询的过程由硬件自动完成,但是页表的维护需要软件来完成。页表查询是一个较耗时的过程。理想的状态下,TLB里应有页表的相关信息。当TLB未命中时,MMU才会查询页表,从而得到翻译后的物理地址。页表通常存储在内存中。得到物理地址之后,首先需要查询该物理地址的内容是否在高速缓存中有最新的副本。如果没有,则说明高速缓存未命中,需要访问内存。MMU的工作职责就是把输入的虚拟地址翻译成对应的物理地址以及相应的页表属性和内存访问权限等信息。另外,如果地址访问失败,那么会触发一个与MMU相关的缺页异常。

image-20230904193647224

对于多任务操作系统,每个进程都拥有独立的进程地址空间。这些进程地址空间在虚拟地址空间内是相互隔离的,但是在物理地址空间可能映射同一个物理页面。

image-20230904193700354

页表

AArch64执行状态的MMU支持单一阶段的页表转换,也支持虚拟化扩展中两阶段的页表转换。单一阶段的页表转换指把虚拟地址(VA翻译成物理地址PA)。 两阶段的页表转换包括两个阶段。在阶段1,把虚拟地址翻译成中间物理地址(Intermediate Physical Address, IPA);在阶段 2,把 IPA 翻译成最终 PA。

当TLB未命中时,处理器查询页表的过程如下:

  • 处理器根据虚拟地址来判断使用TTBR0还是TTBR1。当虚拟地址第63位(简称VA[63])为1时,选择TTBR1 ;当VA[63]为0时,选择TTBR0。TTBR中存放着L0页表的基地址。
  • 处理器以VA作为索引,在页表中找到页表项,加上VA[11:0],就构成了新的物理地址,因此处理器就完成了页表的查询和翻译工作。

与x86_64体系结构的一套页表设计不同,AArch64执行状态的体系结构采用分离的两套页表设计。整个虚拟地址空间分成3部分,下面是用户空间,中间是非规范区域,上面是内核空间。当CPU要访问用户空间的地址时,MMU会自动选择TTBR0指向的页表。当CPU要访问内核空间的时候,MMU会自动选择TTBR1这个寄存器指向的页表,这是硬件自动做的。

image-20230904193726511

当CPU访问内核空间地址(即虚拟地址的高16位为1)时,MMU自动选择TTBR1_EL1 指向的页表。

当CPU访问用户空间地址(即虚拟地址的高16位为0)时,MMU自动选择TTBR0_EL0 指向的页表。

页表属性

共享性与缓存性

缓存性(cacheability)指的是页面是否使能了高速缓存以及高速缓存的范围。通常只有普通内存可以使能高速缓存,通过页表项AttrIndx[2:0]来设置页面的内存属性。另外,还能指定高速缓存是内部共享属性还是外部共享属性。通常处理器内核集成的高速缓存属于内部共享的高速缓存,而通过系统总线集成的高速缓存属于外部共享的高速缓存。

共享性指的是在多核处理器系统中某一个内存区域的高速缓存可以被哪些观察者观察到。没有共享性指的是只有本地CPU能观察到,内部共享性只能被具有内部共享属性的高速缓存的 CPU观察到,外部共享性通常能被外部共享的观察者(例如系统中所有的CPU、GPU以及DMA等主接口控制器)观察到。

访问权限

页表项属性通过AP字段来控制CPU 对页面的访问,例如,指定页面是否具有可读、可写权限,不同的异常等级对这个页面的访问极限等。

AP字段有两位。 AP[1]用来控制不同异常等级下CPU的访问权限。若AP[1]为1,表示在非特权模式下可以访问;若AP[1]为0,表示在非特权模式下不能访问。 AP[2]用来控制是否具有可读、可写权限.若AP[2]为1,表示只读权限;若AP[2]为0,表示可读、可写权限。

当AP[1]为1时表示非将权模式和特权模式具有相同的访问权限,这样的设计会导致一个问题:特权模式下的内核态可以任意访问用户态的内存。攻击者可以在内核态任意访问用户态的恶意代码。为了修复这个漏洞,在ARMV8.1架构里新增了 PAN (特权禁止访问)特性,在PSTATE寄存器中新增一位来表示PAN。内核态访问用户态内存时会触发一个访问权限异常,从而限制在内核态恶意访问用户态内存。

执行权限

页表项属性通过PXN字段以及XN/UXN字段来设置CPU是否对这个页面具有执行权限。

当系统中使用两套页表时,UXN (Unprivileged eXecute-Never)用来设置非特权模式下的页表(通常指的是用户空间页表)是否具有可执行权限。若UXN为1,表示不具有可执行权限; 若为0,表示具有可执行权限。当系统只使用一套页表时,使用XN (eXecute-Never)字段。

当系统中使用两套页表时,PXN (Privilegsd eXecute-Never)用来设置特权模式下的页表(通常指的是内核空间页表)是否具有可执行权限。若PXN为1,表示不具有可执行权限;若为0, 表示具有可执行权限。

除此之外,为了提高系统的安全性,SCTRL_ELx寄存器中还用WXN字段来全局地控制执行权限。当WXN字段为1时,在EL0里具有可写权限的内存区域不可执行,包括特权模式(EL1) 和非特权模式(EL0);在EL1里具有可写权限的内存区域相当于设置PXN为1,即在特权模式下不可执行。

image-20230904193739976

访问标志位

页表项属性中有一个访问字段AF (Access Flag),用来指示页面是否被访问过。

  • AF为1表示页面己经被CPU访问过。
  • AF为0表示页面还没有被CPU访问过。

在ARMv8.0体系结构里需要软件来维护访问位。当CPU尝试第一次访问页面时会触发访问标志位异常(access flag fault),然后软件就可以设置访问标志位为1。 操作系统使用访问标志位有如下好处。

  • 用来判断某个已经分配的页面是否被操作系统访问过。如果访问标志位为0,说明这个页面没有被处理器访问过。
  • 用于操作系统中的页面回收机制。

non-global

页表项属性中有一个nG字段(non-Global)用来设置对应TLB的类型。TLB的表项分成全局的和进程特有的。当设置nG为1时,表示这个页面对应的TLB表项是进程特有的;当为 0时,表示这个TLB表项是全局的。

内存属性

ARMv8体系结构处理器主要提供两种类型的内存属性,分别是普通类型内存和设备类型内存。

普通类型内存实现的是弱一致性的(weakly ordered)内存模型,没有额外的约束,可以提供最高的内存访问性能。通常代码段、数据段以及其他数据都会放在普通内存中。普通类型内存可 以让处理器做很多优化,如分支预测、数据预取、高速缓存行预取和填充、乱序加载等硬件优化。

处理器访问设备类型内存会有很多限制,如不能进行预测访问等。设备类型内存是严格按照指令顺序来执行的。通常设备类型内存留给设备来访问。若系统中所有内存都设置为设备内存,就会有很大的副作用。

ARMv8体系结构定义了多种关于设备内存的属性:

  • Device-nGnRnE
  • Device-nGnRE
  • Device-nGRE
  • Device-GRE

G和nG分别表示聚合(Gathering)与不聚合(non Gathering)。聚合表示在同一个内存属性的区域中允许把多次访问内存的操作合并成一次总线传输。 若一个内存地址标记为"nG”,则会严格按照访问内存的次数和大小来访问内存,不会做合并优化。若一个内存地址标记为“G”,则会做总线合并访问,如合并两个相邻的字节访问为一次多字节访问。若程序访问同一个内存地址两次,则处理器只会访问内存一次,但是在第二次访问内存指令后返回相同的值。

R和nR分别表示指令重排(Re-ordering)与不重排(non Re-ordering)。

E和nE分别表示提前写应答(Early write acknowledgement)与不提前写应答(non Early write acknowledgement)o往外部设备写数据时,处理器先把数据写入写缓冲区(writebuffer)中,若使能了提前写应答,则数据到达写缓冲区时会发送写应答;若没有使能提前写应答,则数据到达外设时才发送写应答。