3.3 存储管理
处理器的存储管理部件(Memory Management Unit,简称MMU)支持虚实地址转换、多进程空间等功能,是通用处理器体现“通用性”的重要单元,也是处理器和操作系统交互最紧密的部分。
本节将介绍存储管理的作用、意义和一般性原理,并以Linux/LoongArch系统为例重点介绍存储管理中TLB的结构、操作方式以及TLB地址翻译过程中所涉及异常的处理。
3.3.1 存储管理的原理
存储管理构建虚拟的内存地址,并通过MMU进行虚拟地址到物理地址的转换。存储管理的作用和意义包括以下方面。
1)隐藏和保护:用户态程序只能访问受限内存区域的数据,其他区域只能由核心态程序访问。引入存储管理后,不同程序仿佛在使用独立的内存区域,互相之间不会影响。此外,分页的存储管理方法对每个页都有单独的写保护,核心态的操作系统可防止用户程序随意修改自己的代码段。
2)为程序分配连续的内存空间:MMU可以由分散的物理页构建连续的虚拟内存空间,以页为单元管理物理内存分配。
3)扩展地址空间:在32位系统中,如果仅采用线性映射的虚实地址映射方式,则至多访问4GB物理内存空间,而通过MMU进行转换则可以访问更大的物理内存空间。
4)节约物理内存:程序可以通过合理的映射来节约物理内存。当操作系统中有相同程序的多个副本在同时运行时,让这些副本使用相同的程序代码和只读数据是很直观的空间优化措施,而通过存储管理可以轻松完成这些。此外,在运行大型程序时,操作系统无须将该程序所需的所有内存都分配好,而是在确实需要使用特定页时再通过存储管理的相关异常处理来进行分配,这种方法不但节约了物理内存,还能提高程序初次加载的速度。
页式存储管理是一种常见而高效的方式,操作系统将内存空间分为若干个固定大小的页,并维护虚拟页地址和物理页地址的映射关系(即页表)。页大小涉及页分配的粒度和页表所占空间,目前的操作系统常用4KB的页。此时,虚拟内存地址可表示为虚拟页地址和页内偏移两部分,在进行地址转换时通过查表的方式将虚拟页地址替换为物理页地址就可得到对应的物理内存地址。
在32位系统中,采用4KB页时,单个完整页表需要1M项,对每个进程维护页表需要相当可观的空间代价,因此页表只能放在内存中。若每次进行地址转换时都需要先查询内存,则会对性能产生明显的影响。为了提高页表访问的速度,现代处理器中通常包含一个转换后援缓冲器(Translation Lookaside Buffer,简称TLB)来实现快速的虚实地址转换。TLB也称页表缓存或快表,借由局部性原理,存储当前处理器中最经常访问页的页表。一般TLB访问与Cache访问同时进行,而TLB也可以被视为页表的Cache。TLB中存储的内容包括虚拟地址、物理地址和保护位,可分别对应于Cache的Tag、Data和状态位。包含TLB的地址转换过程如图3.3所示。
图3.3 包含TLB的地址转换过程
处理器用地址空间标识符(Address Space Identifier,简称ASID)和虚拟页号(Virtual Page Number,简称VPN)在TLB中进行查找匹配,若命中则读出其中的物理页号(Physical Page Number,简称PPN)和标志位(Flag)。标志位用于判断该访问是否合法,一般包括是否可读、是否可写、是否可执行等,若非法则发出非法访问异常;物理页号用于和页内偏移(Offset)拼接组成物理地址。若未在TLB中命中,则需要将页表内容从内存中取出并填入TLB中,这一过程通常称为TLB重填(TLB Refill)。TLB重填可由硬件或软件进行,例如X86、ARM处理器采用硬件TLB重填,即由硬件完成页表遍历(Page Table Walker),将所需的页表项填入TLB中;而MIPS、LoongArch处理器默认采用软件TLB重填,即查找TLB发现不命中时,将触发TLB重填异常,由异常处理程序进行页表遍历并进行TLB填入。
在计算机中,外存、内存、Cache、通用寄存器可以组织成速度由慢到快的存储层次。TLB在存储层次中的位置和作用与Cache类似,可视为页表这种特殊内存数据的专用Cache。
3.3.2 TLB的结构和使用
1.地址空间和地址翻译模式
在介绍LoongArch指令系统中TLB相关的存储管理的机制前,首先简要了解一下LoongArch中地址空间和地址翻译模式的基本内容。LoongArch处理器支持的内存物理地址空间范围表示为0~2PALEN-1。在LA32架构下,PALEN理论上是一个不超过36的正整数;在LA64架构下,PALEN理论上是一个不超过60的正整数。
LoongArch指令系统中的虚拟地址空间是线性平整的。对于PLV0级来说,LA32架构下虚拟地址空间大小为232字节,LA64架构下虚拟地址空间大小为264字节。不过对于LA64架构来说,264字节大小的虚拟地址空间并不都是合法的,可以认为存在一些虚拟地址的空洞。合法的虚拟地址空间与地址映射模式紧密相关。
LoongArch指令系统的MMU支持两种虚实地址翻译模式:直接地址翻译模式和映射地址翻译模式。在直接地址翻译模式下,物理地址默认直接等于虚拟地址(高位不足补0、超出截断),此时可以认为整个虚拟地址空间都是合法的。当CSR.CRMD中的DA域为1且PG域为0时CPU处于直接地址翻译模式。CPU复位结束后将进入直接地址翻译模式。
当CSR.CRMD中的DA域为0且PG域为1时CPU处于映射地址翻译模式。映射地址翻译模式又分为直接映射地址翻译模式(简称“直接映射模式”)和页表映射地址翻译模式(简称“页表映射模式”)两种。在映射地址翻译模式下,地址翻译时将优先看其能否按照直接映射模式进行地址翻译,无法进行后再通过页表映射模式进行翻译。
直接映射模式通过直接映射配置窗口机制完成虚实地址翻译,简单来说就是将一大段连续的虚地址空间线性连续地映射至一段相同大小的物理地址空间。这里被翻译的一整段地址空间的大小通常远大于页表映射模式下所使用的页的大小,因此需要的配置信息更少。LoongArch中将一对直接映射关系称为一个直接映射配置窗口,共定义了四个直接映射配置窗口。四个窗口的配置信息存于CSR.DMW0~CSR.DMW3中,每个窗口的配置信息包含该窗口对应的地址范围、该窗口在哪些权限等级下可用以及该窗口上的访存操作的存储访问类型。
LoongArch指令系统中的页表映射模式,顾名思义,通过页表映射完成虚实地址转换。在该模式下,合法虚拟地址的[63:PALEN]位必须与[PALEN-1]位相同,即虚地址第[PALEN-1]位之上的所有位是该位的符号扩展。
2.TLB结构
页表映射模式存储管理的核心部件是TLB。LoongArch指令系统的TLB分为两个部分,一个是所有表项的页大小相同的单一页大小TLB(Singular-Page-Size TLB,简称STLB),另一个是支持不同表项的页大小可以不同的多重页大小TLB(Multiple-Page-Size TLB,简称MTLB)。STLB的页大小可通过STLBPS控制寄存器进行配置。
在虚实地址转换过程中,STLB和MTLB同时查找。相应地,软件需保证不会出现MTLB和STLB同时命中的情况,否则处理器行为将不可知。MTLB采用全相联查找表的组织形式,STLB采用多路组相联的组织形式。对于STLB,如果其有2INDEX组,且配置的页大小为2PS字节,那么硬件查询STLB的过程中,是将虚地址的[PS+INDEX:PS]位作为索引值来访问各路信息的。接下来介绍LoongArch64指令系统中TLB单个表项的结构,如图3.4所示。
图3.4 LoongArch64指令系统中TLB表项结构
在TLB表项中,E表示该TLB表项是否存在,E为0的项在进行TLB查找时将被视为无效项;ASID标记该TLB表项属于哪个地址空间,只有CPU中当前的ASID(由CSR.ASID的ASID域决定)与该域相同时才能命中,ASID用于区分不同进程的页表;G位域表示全局域,为1时关闭ASID匹配,表示该TLB表项适用于所有的地址空间;PS表示该页表项中存放的页大小,数值是页大小的2的幂指数,有6比特宽,因此LoongArch指令系统的页大小理论上可以任意变化,处理器可以实现其中的一段范围;VPPN表示虚双页号,在LoongArch指令系统中,TLB的每项把两个连续的虚拟页映射为两个物理页;PPN为物理页号,这个域的实际有效宽度取决于该处理器支持的物理内存空间的大小;PLV表示该页表项对应的权限等级;RPLV为受限权限等级使能,当RPLV=0时,该页表项可以被任何权限等级不低于PLV的程序访问,否则,该页表项仅可以被权限等级等于PLV的程序访问;MAT控制落在该页表项所在地址空间上的访存操作的存储访问类型,如是否可通过Cache缓存等;NX为不可执行位,为1表示该页表项所在地址空间上不允许执行取指操作;NR为不可读位,为1表示该页表项所在地址空间上不允许执行load操作;D被称为“脏”(Dirty)位,为1表示该页表项所对应的地址范围内已有脏数据;V为有效位,为1表明该页表项是有效且被访问过的。
3.TLB虚实地址翻译过程
用TLB进行虚实地址翻译时,首先要进行TLB查找,将待查虚地址vaddr和CSR.ASID中ASID域的值asid一起与STLB中每一路的指定索引位置项以及MTLB中的所有项逐项进行比对。如果TLB表项的E位为1,vaddr对应的虚双页号vppn与TLB表项的VPPN相等(该比较需要根据TLB表项对应的页大小,只比较地址中属于虚页号的部分),且TLB表项中的G位为1或者asid与TLB表项的ASID域的值相等,那么TLB查找命中该TLB表项。如果没有命中项,则触发TLB重填异常(TLBR)。如果查找到一个命中项,那么根据命中项的页大小和待查虚地址确定vaddr具体落在双页中的哪一页,从奇偶两个页表项取出对应页表项作为命中页表项。如果命中页表项的V等于0,说明该页表项无效,将触发页无效异常,具体将根据访问类型触发对应的load操作页无效异常(PIL)、store操作页无效异常(PIS)或取指操作页无效异常(PIF)。如果命中页表项的V值等于1,但是访问的权限等级不合规,将触发页权限等级不合规异常(PPI)。权限等级不合规体现为,该命中页表项的RPLV值等于0且CSR.CRMD中PLV域的值大于命中页表项中的PLV值,或是该命中页表项的RPLV=1且CSR.CRMD中PLV域的值不等于命中页表项中的PLV值。如果上述检查都合规,还要进一步根据访问类型进行检查。如果是一个load操作,但是命中页表项中的NR值等于1,将触发页不可读异常(PNR);如果是一个store操作,但是命中页表项中的D值等于0,将触发页修改异常(PME);如果是一个取指操作,但是命中页表项中的NX值等于1,将触发页不可执行异常(PNX)。如果找到了命中项且经检查上述异常都没有触发,那么命中项中的PPN值和MAT值将被取出,前者用于和vaddr中提取的页内偏移拼合成物理地址paddr,后者用于控制该访问操作的内存访问类型属性。
当触发TLB重填异常时,除了更新CSR.CRMD外,CSR.CRMD中PLV、IE域的旧值将被记录到CSR.TLBRPRMD的相关域中,异常返回地址也将被记录到CSR.TLBRERA的PC域中,处理器还会将引发该异常的访存虚地址填入CSR.TLBRBAV的VAddr域并从该虚地址中提取虚双页号填入CSR.TLBREHI的VPPN域。当触发非TLB重填异常的其他TLB类异常时,除了像普通异常发生时一样更新CRMD、PRMD和ERA这些控制状态寄存器的相关域外,处理器还会将引发该异常的访存虚地址填入CSR.BADV的VAddr域并从该虚地址中提取虚双页号填入CSR.TLBEHI的VPPN域。
4.TLB相关控制状态寄存器
除了上面提到的TLB查找操作外,LoongArch指令系统中定义了一系列用于访问和控制TLB的控制状态寄存器,用于TLB内容的维护操作。
LoongArch指令系统中用于访问和控制TLB的控制状态寄存器大致可以分为三类:第一类用于非TLB重填异常处理场景下的TLB访问和控制,包括TLBIDX、TLBEHI、TLBELO0、TLBELO1、ASID和BADV;第二类用于TLB重填异常处理场景,包括此场景下TLB访问控制专用的TLBREHI、TLBRELO0、TLBRELO1和TLBRBADV以及此场景下保存上下文专用的TLBRPRMD、TLBRERA和TLBRSAVE;第三类用于控制页表遍历过程,包括PGDL、PGDH、PGD、PWCL和PWCH。三类寄存器的具体格式如图3.5所示。
上述寄存器中,第二类专用于TLB重填异常处理场景(CSR.TLBRERA的IsTLBR域值等于1)的控制寄存器,其设计目的是确保在非TLB重填异常处理程序执行过程中嵌套发生TLB重填异常处理后,原有异常处理程序的上下文不被破坏。例如,当发生TLB重填异常时,其异常处理返回地址将填入CSR.TLBRERA而非CSR.ERA,这样被嵌套的异常处理程序返回时所用的返回目标就不会被破坏。因硬件上只维护了这一套保存上下文专用的寄存器,所以需要确保在TLB重填异常处理过程中不再触发TLB重填异常,为此,处理器因TLB重填异常触发而陷入异常处理后,硬件会自动将虚实地址翻译模式调整为直接地址翻译模式,从而确保TLB重填异常处理程序第一条指令的取指和访存一定不会触发TLB重填异常,与此同时,软件设计人员也要保证后续TLB重填异常处理返回前的所有指令的执行不会触发TLB重填异常。
在访问和控制TLB的控制状态寄存器中,ASID中的ASID域、TLBEHI中的VPPN域、TLBELO0和TLBELO1中的所有域、TLBIDX中的PS和E域所构成的集合对应了一个TLB表项中的内容(除了TLB表项中的G位域),ASID中的ASID域、TLBREHI中的VPPN和PS域、TLBRELO0和TLBRELO1中的所有域所构成的集合也对应了一个TLB表项中的内容(除了G位域和E位域)。这两套控制状态寄存器都用来完成TLB表项的读写操作,前一套用于非TLB重填异常处理场景,而后一套仅用于TLB重填异常处理场景。写TLB时把上述寄存器中各个域存放的值写到TLB某一表项(将TLBELO0和TLBELO1的G位域相与或者将TLBRELO0和TLBRELO1的G位域相与后写入TLB表项的G位域),读TLB时将TLB表项读到并写入上述寄存器中的对应域(将TLB表项的G位域的值同时填入TLBELO0和TLBELO1的G位域,或者同时填入TLBRELO0和TLBRELO1的G位域)。
图3.5 LoongArch指令系统中TLB相关控制寄存器
上述第三类寄存器的工作及使用方式将在3.3.3节中予以介绍。
5.TLB访问和控制指令
为了对TLB进行维护,除了上面提到的TLB相关控制状态寄存器外,LoongArch指令系统中还定义了一系列TLB访问和控制指令,主要包括TLBRD、TLBWR、TLBFILL、TLBSRCH和INVTLB。
TLBRD是读TLB的指令,其用CSR.TLBIDX中Index域的值作为索引读出指定TLB表项中的值并将其写入CSR.TLBEHI、CSR.TLBELO0、CSR.TLBELO1以及CSR.TLBIDX的对应域中。
TLBWR是写TLB的指令,其用CSR.TLBIDX中Index域的值作为索引将CSR.TLBEHI、CSR.TLBELO0、CSR.TLBELO1以及CSR.TLBIDX相关域的值(当处于TLB重填异常处理场景时,这些值来自CSR.TLBREHI、CSR.TLBRELO0和CSR.TLBRELO1)写到对应的TLB表项中。
TLBFILL是填入TLB的指令,其将CSR.TLBEHI、CSR.TLBELO0、CSR.TLBELO1以及CSR.TLBIDX相关域的值(当处于TLB重填异常处理场景时,这些值来自CSR.TLBREHI、CSR.TLBRELO0和CSR.TLBRELO1)填入TLB中的一个随机位置。该位置的具体确定过程是,首先根据被填入页表项的页大小来决定是写入STLB还是MTLB。当被填入的页表项的页大小与STLB所配置的页大小(由CSR.STLBPS中PS域的值决定)相等时将被填入STLB,否则将被填入MTLB。页表项被填入STLB的哪一路,或者被填入MTLB的哪一项,是由硬件随机选择的。
TLBSRCH为TLB查找指令,其使用CSR.ASID中ASID域和CSR.TLBEHI中VPPN域的信息(当处于TLB重填异常处理场景时,这些值来自CSR.ASID和CSR.TLBREHI)去查询TLB。如果有命中项,那么将命中项的索引值写入CSR.TLBIDX的Index域,同时将其NE位置为0;如果没有命中项,那么将该寄存器的NE位置1。
INVTLB指令用于无效TLB中符合条件的表项,即从通用寄存器rj和rk得到用于比较的ASID和虚地址信息,依照指令op立即数指示的无效规则,对TLB中的表项逐一进行判定,符合条件的TLB表项将被无效掉。
[1] PC域不包含指令地址的最低两位,因为能触发TLB重填异常的指令的PC最低两位一定为0,所以这两位不需要记录。
[2] 如果第一条指令即为访存指令。
3.3.3 TLB地址翻译相关异常的处理
上一节介绍了LoongArch指令系统中与TLB相关的硬件规范,这些设计为操作系统提供了必要的支持,而存储管理则需要CPU和操作系统紧密配合,CPU硬件在使用TLB进行地址翻译的过程中将产生相关异常,再由操作系统介入进行异常处理。本节将重点讲述这些异常处理的过程。
1.多级页表结构
Linux操作系统通常采用多级页表结构。对于64位的LoongArch处理器,如果其有效虚地址位宽为48位,那么当Linux操作系统采用16KB页大小时,其页表为三级结构,如图3.6所示。33位的虚双页号(VPPN)分为三个部分:最高11位作为一级页表(页目录表PGD)索引,一级页表中每一项保存一个二级页表(页目录表PMD)的起始地址;中间11位作为二级页表索引,二级页表中每一项保存一个三级页表(末级页表PTE)的起始地址;最低11位作为三级页表索引。每个三级页表包含2048个页表项,每个页表项管理一个物理页,大小为8字节,包括RPLV、NX、NR、PPN、W、P、G、MAT、PLV、D、V的信息。“P”和“W”两个域分别代表物理页是否存在,以及该页是否可写。这些信息虽然不填入TLB表项中,但用于页表遍历的处理过程。每个进程的PGD表基地址放在进程上下文中,内核进程进行切换时把PGD表的基地址写到CSR.PGDH的Base域中,用户进程进行切换时把PGD表的基地址写到CSR.PGDL的Base域中。
2.TLB重填异常处理
当TLB重填异常发生后,其异常处理程序的主要处理流程是根据CSR.TLBRBADV中VAddr域记录的虚地址信息以及从CSR.PGD中得到的页目录表PGD的基址信息,遍历发生TLB重填异常的进程的多级页表,从内存中取回页表项信息填入CSR.TLBRELO0和CSR.TLBRELO1的相应域中,最终用TLBFILL指令将页表项填入TLB。前面在讲述TLBFILL指令写操作过程时,提到此时写入TLB的信息除了来自CSR.TLBRELO0和CSR.TLBRELO1的各个域之外,还有来自CSR.ASID中ASID域和CSR.TLBREHI中VPPN域的信息。在TLB重填异常从发生到进行处理的过程中,软硬件都没有修改CSR.ASID中的ASID域,所以在执行TLBFILL指令时,CSR.ASID中的ASID域记录的就是发生TLB重填异常的进程对应的ASID。至于CSR.TLBREHI中的VPPN域,在TLB重填异常发生并进入异常入口时,已经被硬件填入了触发该异常的虚地址中的虚双页号信息。
整个TLB重填异常处理过程中,遍历多级页表是一个较为复杂的操作,需要数十条普通访存、运算指令才能完成,而且如果遍历的页表级数增加,则需要更多的指令。LoongArch指令系统中定义了LDDIR和LDPTE指令以及与之配套的CSR.PWCL和CSR.PWCH来加速TLB重填异常处理中的页表遍历。LDDIR和LDPTE指令的功能简述如表3.3所示。
图3.6 Linux/LoongArch三级页表结构
表3.3 LoongArch软件页表遍历指令
CSR.PWCL和CSR.PWCH用来配置LDDIR和LDPTE指令所遍历页表的规格参数信息,其中CSR.PWCL中定义了每个页表项的宽度(PTEwidth域)以及末级页表索引的起始位置和位宽(PTbase和PTwidth域)、页目录表1索引的起始位置和位宽(Dir1_base和Dir1_width域)、页目录表2索引的起始位置和位宽(Dir2_base和Dir2_width域),CSR.PWCH中定义了页目录表3索引的起始位置和位宽(Dir3_base和Dir3_width域)、页目录表4索引的起始位置和位宽(Dir4_base和Dir4_width域)。在Linux/LoongArch64中,当进行三级页表的遍历时,通常用Dir1_base和Dir1_width域来配置页目录表PMD索引的起始位置和位宽,用Dir3_base和Dir3_width域来配置页目录表PGD索引的起始位置和位宽,Dir2_base和Dir2_width域、Dir4_base和Dir4_width域空闲不用。
使用上述指令,TLB重填异常处理程序见图3.7。可见,遍历一个三级页表的处理过程只需要执行9条指令,且每增加一级页表只需增加一条LDDIR指令即可。
图3.7 Linux/LoongArch64 TLB重填异常处理程序
3.其他TLB地址翻译相关异常处理
除了TLB重填异常外,LoongArch指令系统下常见的TLB类异常有取指操作页无效异常、load操作页无效异常、store操作页无效异常和页修改异常。这四种异常在Linux/LoongArch中处理的伪代码如图3.8所示,其中取指操作页无效异常和load操作页无效异常的处理流程一致。伪代码中的load pte函数遍历页表并取得页表项,DO_FAULT函数在内存中分配物理页并把该页内容从对换区中取到内存,_PAGE_PRESENT、_PAGE_READ和_PAGE_WRITE分别表示相应的物理页是否在内存中、是否可读、是否可写。
下面通过一个例子来深入分析处理器、操作系统以及应用程序间的交互。图3.9是一个分配数组和对数组赋值的小程序。从程序员的角度看,这个程序很简单,但从结构和操作系统的角度看,这个程序的执行却涉及复杂的软硬件交互过程。
图3.8 LoongArch四种TLB异常在Linux中的处理
图3.9 数组分配和赋值程序
该用户程序首先调用内存分配函数malloc来分配大小为0x1000字节的空间,假设返回一个虚地址0x450000。操作系统在进程的vma_struct链表里记录地址范围0x450000~0x451000为已分配地址空间,并且是可读、可写的。但操作系统只是分配了一个地址范围,还没有真实分配内存的物理空间,也没有在页表里建立页表项,TLB里更没有——因为如果进程没有访问,就不用真为其分配物理空间。接下来的for循环对数组array进行赋值,用户程序写地址为0x450000的单元。store操作在完成地址运算后查找TLB,由于TLB里面没有这一表项,因此引起TLB重填异常。TLB重填异常处理程序从相应的页表位置取页表内容填入TLB,但此时这个地址空间的页表还没有有效的页表项信息。当异常处理返回用户程序重新开始访问时,TLB里面有了对应的虚地址,但是还没有物理地址。因为还没有分配具体的物理空间,所以引起store操作页无效异常。处理store操作页无效异常时,操作系统需要查找vma_struct这个结构,如果判断出这个地址已经分配,处于可写状态,这时操作系统才真正分配物理页面,并分配物理页表,将物理地址填入页表,更新TLB相应的表项。store操作页无效异常处理完成之后返回,store操作再次执行,这次就成功了,因为TLB里已经有了相应的表项,并且是有效、可写的。由于分配的页面恰好为4KB大小,且在同一页中,因此后续的地址访问都会在TLB中命中,不会再产生异常。产生两次异常而非一次完成所有操作的原因是保证TLB重填异常的处理速度。