1
1
mirror of https://github.com/foxsen/archbase.git synced 2026-02-02 18:09:17 +08:00
Files
archbase/13-privileged-isa.Rmd
2022-11-20 11:31:17 +00:00

391 lines
51 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 特权指令系统 {#sec-privileged-ISA}
## 特权指令系统简介
在计算机系统层次结构中,应用层^[特指直接运行在CPU上的应用把虚拟机及其上运行的应用作为整体看待。]在操作系统层之上,只能看到和使用指令系统的一个子集,即指令系统的用户态部分。每个应用程序都有自己的寄存器、内存空间以及可执行的指令。现代计算机的指令系统在用户态子集之外还定义了操作系统核心专用的特权态部分,我们称之为特权指令系统。
特权指令系统的存在主要是为了让计算机变得更好用、更安全。操作系统通过特权指令系统管理计算机使得应用程序形成独占CPU的假象并使应用间相互隔离互不干扰。应用程序只能在操作系统划定的范围内执行一旦超出就会被CPU切换成操作系统代码运行。
不同指令系统的特权态部分差别较大,但就其机制而言,可以分为以下几类:
1运行模式定义及其转换
现代计算机的操作系统都实现了保护模式至少需要用户态和核心态两种运行模式。应用运行在用户态模式下操作系统运行在核心态模式下。因此指令系统必须有相应的运行模式以做区分。比如MIPS定义了user、supervisor、kernel三种模式X86定义了Ring0\~Ring3四种模式LoongArch定义了PLV0\~PLV3四种模式。
刚开机时CPU初始化为操作系统核心态对应的运行模式执行引导程序加载操作系统。操作系统做完一系列初始化后控制CPU切换到操作系统用户态对应的运行模式去执行应用程序。应用程序执行过程中如果出现用户态对应的运行模式无法处理的事件则CPU会通过异常或中断回到核心态对应的运行模式执行操作系统提供的服务程序。操作系统完成处理后再控制CPU返回用户态对应的运行模式继续运行原来的应用程序或者调度另一个应用程序。在LoongArch指令系统中CPU当前所处的运行模式由当前模式信息控制状态寄存器CSR.CRMD的PLV域的值确定其值为03分别表示CPU正处于PLV0PLV3四种运行模式见图\@ref(fig:crmd))。
```{r crmd, echo=FALSE, fig.align='center', fig.cap="LoongArch当前模式信息控制状态寄存器格式", out.width='100%'}
knitr::include_graphics("images/chapter3/crmd.png")
```
运行模式的转换过程与虚拟存储和异常中断紧密相关,共同构建出完备的保护模式。不少指令系统还支持虚拟机模式、调试模式等,使计算机系统更为易用。
2虚拟存储管理
虚拟存储管理的基本思想是让软件包括系统软件运行在“虚地址”上与真正访问存储的“实地址”物理地址相隔离。虚实地址的转换根据地址段属性的不同有查表转换和直接映射两种方式。查表转换是应用程序使用的主要方式。不同的进程有自己独立的虚地址空间。CPU执行访存指令时根据操作系统给出的映射表来完成虚地址空间到物理内存的转换。
直接映射的方式与使用物理地址差别不大,主要给操作系统使用,因为在初始化之前负责虚存管理的代码本身不能运行在被管理的虚地址空间。通常用户态应用程序无法使用直接映射方式。
\@ref(sec-memory-management)节将对存储管理做更详细的介绍。
3异常与中断处理
异常与中断是一种打断正常的软件执行流切换到专门的处理函数的机制。它在各种运行模式的转换中起到关键的纽带作用。比如用户态代码执行过程中当出现对特权空间的访问或者访问了虚实地址映射表未定义的地址或者需要调用操作系统服务等情况时CPU通过发出异常来切换到核心态进入操作系统定义的服务函数。操作系统完成处理后返回发生异常的代码并同时切换到用户态。
\@ref(sec-exception)节将对异常与中断做更详细的介绍。
4控制状态寄存器
控制状态寄存器位于一个独立的地址空间是支撑前面3种机制的具体实现不同的指令系统差别较大。下面以LoongArch指令系统为例列出其控制状态寄存器的功能。
```{r la-csr, echo = FALSE, message=FALSE, tab.cap='LoongArch处理器的控制寄存器', tab.id='la-csr'}
autonum <- run_autonum(seq_id = "tab", bkm = "la-csr", bkm_all = TRUE)
readr::read_csv('./materials/chapter3/csr.csv') %>%
flextable() %>%
set_caption(caption="LoongArch处理器的控制寄存器", autonum = autonum) %>%
merge_v() %>%
width(j=1, width=1.0) %>%
width(j=2, width=1.0) %>%
width(j=3, width=4.0) %>%
theme_box()
```
控制状态寄存器虽然重要但对其操作的频率通常远远低于通用寄存器所以指令系统中通常不会设计针对控制状态寄存器的访存和复杂运算指令。不过大多数指令系统至少会定义若干在控制状态寄存器和通用寄存器之间进行数据搬运的指令从而可以将数据移动到通用寄存器中进行相关处理或者进一步将处理结果写回控制状态寄存器中。在LoongArch指令系统中就定义了CSRRD和CSRWR指令来完成控制状态寄存器的读写操作。例如指令“csrrd \$t0, CSR_CRMD ^[这里CSR_CRMD是一个宏定义表示一个立即数其值为CRMD控制状态寄存器的编号0x0。使用CSR_CRMD这样的宏定义是为了便于代码理解。] ”将控制状态寄存器CRMD的值读出然后写入通用寄存器\$t0中指令“csrwr \$t0, CSR_CRMD”将通用寄存器\$t0中的值写入到控制状态寄存器CRMD中同时将控制状态寄存器CRMD的旧值写入通用寄存器\$t0中。
## 异常与中断 {#sec-exception}
计算机通常按照软件的执行流进行顺序执行和跳转,但有时会需要中断正常的执行流程去处理其他任务,可以触发这一过程的事件统称为异常。
### 异常分类
从来源来看异常可分为以下6种。
1外部事件来自CPU核外部^[这里“CPU核”可以更为严格地理解为CPU核的指令流水线即旨在强调这些事件并非直接由指令引发。以定时中断为例它由一个物理上独立于CPU指令流水线而存在的定时器触发但是这个定时器既可以放置在CPU核内部也可以放置在CPU核外部。]的事件来自处理器内部其他模块或者处理器外部的真实物理连线也称为中断。中断的存在使得CPU能够异步地处理多个事件。在操作系统中为了避免轮询等待浪费CPU时间与IO相关的任务通常都会用中断方式进行处理。中断事件的发生往往是软件不可控制的因此需要一套健全的软硬件机制来防止中断对正常执行流带来影响。
2指令执行中的错误执行中的指令的操作码或操作数不符合要求例如不存在的指令、除法除以0、地址不对齐、用户态下调用核心态专有指令或非法地址空间访问等。这些错误使得当前指令无法继续执行应当转到出错处进行处理。
3数据完整性问题当使用ECC等硬件校验方式的存储器发生校验错误时会产生异常。可纠正的错误可用于统计硬件的风险不可纠正的错误则应视出错位置进行相应处理。
4地址转换异常在存储管理单元需要对一个内存页进行地址转换而硬件转换表中没有有效的转换对应项可用时会产生地址转换异常。
5系统调用和陷入由专有指令产生其目的是产生操作系统可识别的异常用于在保护模式下调用核心态的相关操作。
6需要软件修正的运算常见的是浮点指令导致的异常某些操作和操作数的组合硬件由于实现过于复杂而不愿意处理寻求软件的帮助。
下表列举了LoongArch指令系统中主要的异常。
```{r exception, echo = FALSE, message=FALSE, tab.cap='LoongArch指令系统的异常一览表', tab.id='exception'}
autonum <- run_autonum(seq_id = "tab", bkm = "exception", bkm_all = TRUE)
readr::read_csv('./materials/chapter3/exception.csv') %>%
flextable() %>%
set_caption(caption="LoongArch指令系统的异常一览表", autonum = autonum) %>%
add_header_row(values=c('异常代号','异常编号','异常说明','所属异常类别'), colwidths=c(1,2,1,1)) %>%
merge_h(part='header') %>%
merge_v(part='header') %>%
merge_v(j=c('Ecode'), part='body') %>%
width(j=1:3, width=0.8) %>%
width(j=4:5, width=1.8) %>%
theme_box()
```
### 异常处理
#### 异常处理流程
异常处理的流程包括异常处理准备、确定异常来源、保存执行状态、处理异常、恢复执行状态并返回等。主要内容是确定并处理异常同时正确维护上下文环境。异常处理是一个软硬件协同的过程通常CPU硬件需要维护一系列控制状态寄存器以用于软硬件之间的交互。LoongArch指令系统中与异常含中断处理相关的控制状态寄存器格式如图\@ref(fig:ex-csr)所示。
```{r ex-csr, echo=FALSE, fig.align='center', fig.cap="LoongArch异常处理相关控制状态寄存器", out.width='100%'}
knitr::include_graphics("images/chapter3/csr.png")
```
下面对异常处理流程的五个阶段进行介绍。
1异常处理准备。当异常发生时CPU在转而执行异常处理前硬件需要进行一系列准备工作。
首先需要记录被异常打断的指令的地址记为EPTR。这里涉及精确异常的概念指发生任何异常时被异常打断的指令之前的所有指令都执行完而该指令之后的所有指令都像没执行一样。在实现精确异常的处理器中异常处理程序可忽略因处理器流水线带来的异常发生位置问题。异常处理结束后将返回EPTR所在地址重新执行被异常打断的指令^[这只是通常的处理流程但并非始终如此存在某些异常处理场景其结束后返回执行的并非最初被该异常打断的指令。例如当执行SYSCALL指令而陷入系统调用异常处理时肯定不能在处理结束后返回触发异常的SYSCALL指令否则将陷入死循环。再譬如当发生中断并陷入操作系统核心进行处理时处理结束后操作系统可能将其他进程或线程调度到该CPU上执行显然此时返回执行的并不是最初被中断打断的那条指令。]因此需要将EPTR记录下来。EPTR存放的位置因不同指令集而不同LoongArch存于CSR.ERA ^[其实TLB重填异常发生时这一信息将被记录在CSR.TLBRBERA中;机器错误异常发生时这一信息将被记录在CSR.MERRERA中。更多细节请见下文中的说明。]PowerPC存于SRR0/CSRR0SPARC存于TPC[TL]X86则用栈存放CS和EIP组合。
其次调整CPU的权限等级通常调整至最高特权等级并关闭中断响应。在LoongArch指令系统中当异常发生时硬件会将CSR.CRMD的PLV域置0以进入最高特权等级并将CSR.CRMD的IE域置0以屏蔽所有中断输入。
再次硬件保存异常发生现场的部分信息。在LoongArch指令系统中异常发生时会将CSR.CRMD中的PLV和IE域的旧值分别记录到CSR.PRMD的PPLV和PIE域中供后续异常返回时使用。
最后记录异常的相关信息。异常处理程序将利用这些信息完成或加速异常的处理。最常见的如记录异常编号以用于确定异常来源。在LoongArch指令系统中这一信息将被记录在CSR.ESTAT的Ecode和EsubCode域前者存放异常的一级编号后者存放异常的二级编号。除此以外有些情况下还会将引发异常的指令的机器码记录在CSR.BADI中或是将造成异常的访存虚地址记录在CSR.BADV中。
2确定异常来源。不同类型的异常需要各自对应的异常处理。处理器确定异常来源主要有两种方式一种是将不同的异常进行编号异常处理程序据此进行区分并跳转到指定的处理入口另一种是为不同的异常指定不同的异常处理程序入口地址这样每个入口处的异常处理程序自然知晓待处理的异常来源。X86由硬件进行异常和中断号的查询根据编号查询预设好的中断描述符表Interrupt Descriptor Table简称IDT得到不同异常处理的入口地址并将CS/EIP等压栈。LoongArch将不同的异常进行编号其异常处理程序入口地址采用“入口页号与页内偏移进行按位逻辑或”的计算方式入口页号通过CSR.EENTRY配置每个普通异常处理程序入口页内偏移是其异常编号乘以一个可配置间隔通过CSR.ECFG的VS域配置。通过合理配置EENTRY和ECFG控制状态寄存器中相关的域可以使得不同异常处理程序入口地址不同。当然也可以通过配置使得所有异常处理程序入口为同一个地址但是实际使用中通常不这样处理。
3保存执行状态。在操作系统进行异常处理前软件要先保存被打断的程序状态通常至少需要将通用寄存器和程序状态字寄存器的值保存到栈中。
4处理异常。跳转到对应异常处理程序进行异常处理。
5恢复执行状态并返回。在异常处理返回前软件需要先将前面第3个步骤中保存的执行状态从栈中恢复出来在最后执行异常返回指令。之所以要采用专用的异常返回指令是因为该指令需要原子地完成恢复权限等级、恢复中断使能状态、跳转至异常返回目标等多个操作。在LoongArch中异常返回的指令是ERTN该指令会将CSR.PRMD的PPLV和PIE域分别回填至CSR.CRMD的PLV和IE域从而使得CPU的权限等级和全局中断响应状态恢复到异常发生时的状态同时该指令还会将CSR.ERA中的值作为目标地址跳转过去。X86的IRET指令有类似效果。
#### 异常嵌套
在异常处理的过程中,又有新的异常产生,这时就会出现异常嵌套的问题。当产生异常嵌套时,需要保存被打断的异常处理程序的状态,这会消耗一定的栈资源,因此无限的异常嵌套是无法容忍的。异常嵌套通常基于优先级,只有优先级更高的异常才能进行嵌套,低优先级或同优先级的异常只能等待当前异常处理完成,系统支持的优先级级数就是异常嵌套的最大层数。
在LoongArch指令系统中异常嵌套时被打断的异常处理程序的状态的保存和恢复主要交由软件处理这就需要保证异常处理程序在完成当前上下文的保存操作之前不会产生新的异常或者产生的新异常不会修改当前需要保存的上下文。这两方面要求仅通过异常处理程序开发人员的精心设计是无法完全保证的因为总有一些异常的产生原因是事先无法预知的如中断、机器错、TLB重填等。为此需要设计硬件机制以保证这些情况发生时不至于产生嵌套异常或即使产生嵌套异常也能保证软件可以获得所要保存上下文的正确内容。例如可以在跳转到异常入口的过程中关闭全局中断使能以禁止中断异常发生还可以在发生嵌套异常的时候将可能被破坏而软件又来不及保存的上下文信息由硬件暂存到指定的控制状态寄存器或内存区域。
### 中断
异常处理的流程是通用的,但有两类异常出现的机会确实比其他类型大很多。一类是地址转换异常,当片内从虚地址到物理地址的地址转换表不包含访问地址时,就会产生缺页异常,在\@ref(sec-memory-management)节中我们将进行详细介绍。另一类常见的异常就是中断中断在外部事件想要获得CPU注意时产生。由于外部事件的不可控性中断处理所用的时间较为关键。在嵌入式系统中CPU的主要作用之一就是处理外设相关事务因此中断发生的数量很多且非常重要。本节以LoongArch指令系统为例介绍中断相关的重要内容。
#### 中断传递机制
中断从系统中各个中断源传递到处理器主要有两种形式:中断线和消息中断。
用中断线传递是最简便直接的方式。当系统的中断源不多时,直接连到处理器引脚即可。若中断源较多,可使用中断控制器汇总后再与处理器引脚相连。由于连线会占用引脚资源,一般只在片上系统(System On Chip,简称SoC)中才会给每个外设连接单独的中断线板级的中断线一般采用共享的方式。比如PCI上有四根中断线供所有的设备共享。中断处理程序在定位到哪根中断线发生中断后逐个调用注册在该中断线的设备中断服务。
LoongArch指令系统支持中断线的中断传递机制共定义了13个中断分别是1个核间中断IPI1个定时器中断TI1个性能监测计数溢出中断PMI8个外部硬中断HWI0\~HWI72个软中断SWI0\~SWI1。其中所有中断线上的中断信号都采用电平中断且都是高电平有效。当有中断发生时这种高电平有效中断方式输入给处理器的中断线上将维持高电平状态直至中断被处理器响应处理。无论中断源来自处理器核外部还是内部是硬件还是软件置位这些中断信号都被不间断地采样并记录到CSR.ESTAT中IS域的对应比特位上。这些中断均为可屏蔽中断除了CSR.CRMD中的全局中断使能位IE外每个中断各自还有其局部中断使能控制位在CSR.ECFG的LIE域中。当CSR.ESTAT中IS域的某位为1且对应的局部中断使能和全局中断使能均有效时处理器就将响应该中断并进入中断处理程序入口处开始执行。
用中断线方式传递中断有一些限制。首先是扩展性不够强,在搭建较复杂的板级系统时会引入过多的共享,降低中断处理的效率。其次,中断处理过程需要通过查询中断控制器以及设备上的状态寄存器来确认中断和中断原因,中间有较长的延迟,同样不利于提高效率。在多处理器平台中,高性能外设(如万兆网卡)对中断处理的性能有更高的要求,需要实现多处理器的负载均衡、中断绑定等功能,传统的中断线方式难以做到。而这正好是消息中断的长处。
消息中断以数据的方式在总线上传递。发中断就是向指定的地址写一个指定的数。相比总线外增加专门的中断线的“带外”Side-Band)传输形式,消息中断在“带内”(In-Band传输。增加中断时不需要改动消息传递的数据通路因而有较高的扩展性和灵活性也为更高程度的优化提供了可能。比如一个设备可以申请更多的中断号使中断处理程序无须查询设备状态只根据中断号就能知道应当做什么处理。
#### 向量化中断
LoongArch指令系统默认支持向量化中断^[尽管将ECFG控制状态寄存器中的VS域置0后所有的异常处理程序入口地址将变为同一个此时中断不再是向量中断形式但这种模式并不是LoongArch指令系统推荐的方式。]其13个线中断各自具有独立的中断处理程序入口地址。在LoongArch指令系统中中断被视作一类特殊的异常进行处理因此在具体计算中断处理程序入口地址时将SWI0IPI这13个中断依次“视作”异常编号6476的异常用异常处理程序入口地址的统一计算方式进行计算。向量化中断的好处之一是省去了中断处理程序开头处识别具体中断源的开销可以进一步加速中断的处理。
X86指令系统支持的向量化中断方案更复杂一些其在地址空间的指定位置处存放中断向量表IVT实模式下默认为0地址或中断描述符表IDT保护模式中断向量表中存放中断入口地址的段地址和偏移量中断描述符表还包含权限等级和描述符类别的信息。X86的向量化中断机制最多可支持256个中断和异常0\~19号为系统预设的异常和NMI20\~31是Intel保留的编号32号开始可用于外部中断详细的实现可参考Intel相关手册。
#### 中断的优先级
在支持多个中断源输入的指令系统中,需要规范在多个中断同时触发的情况下,处理器是否区别不同来源的中断的优先级。当采用非向量中断模式的时候,处理器通常不区别中断优先级,此时若需要对中断进行优先级处理,可以通过软件方式予以实现,其通常的实现方案是:
1软件随时维护一个中断优先级IPL每个中断源都被赋予特定的优先级。
2正常状态下CPU运行在最低优先级此时任何中断都可触发。
3当处于最高中断优先级时任何中断都被禁止。
4更高优先级的中断发生时可以抢占低优先级的中断处理过程。
当采用向量中断模式的时候处理器通常不可避免地需要依照一套既定的优先级规则来从多个已生效的中断源中选择一个跳转到其对应的处理程序入口处。LoongArch指令系统实现的是向量中断采用固定优先级仲裁机制具体规则是硬件中断号越大优先级越高即IPI的优先级最高TI次之SWI0的优先级最低。
#### 中断使能控制位的原子修改
在中断处理程序中经常会涉及中断使能控制位的修改如关闭、开启全局中断使能。在大多数指令系统中这些中断使能控制位位于控制状态寄存器中因此软件在进行中断使能控制调整时必须关注修改的原子性问题。以LoongArch指令系统为例全局中断使能控制位IE位于CRMD控制寄存器的第2位。如果仅用CSRRD和CSRWR指令访问CRMD控制寄存器那么需要通过下面的一段程序才能完成开启中断使能的功能
```
li $t1, IE_BITMASK
csrrd $t0, CSR_CRMD
1:
andn $t0, $t0, $t1
or $t0, $t0, $t1
2:
csrwr $t0, CSR_CRMD
```
这段程序本身也可能被中断若在标号1和2之间被中断且中断处理程序修改了CRMD控制寄存器的值则在返回时该中断处理程序对CRMD控制寄存器的改写会被这段程序覆盖。若不想让这种情况发生就需要保证这段程序不会被打断更正式地说是保证这段程序的原子性。保证原子性的方法有很多种例如添加专门的位原子修改指令、在程序执行时禁用中断、不允许中断处理程序修改SR或者使用通用的方法保证程序段的原子性即将被访问的控制寄存器作为临界区来考虑。LoongArch指令系统中定义了按位掩码修改控制寄存器的指令CSRXCHG。使用该指令时上述开启全局中断使能的代码改写如下
```
li $t0, IE_BITMASK
csrxchg $t0, $t0, CSR_CRMD
```
上面的例子中CRMD寄存器的IE位置1的操作仅通过csrxchg一条指令完成所以自然确保了修改的原子性。
## 存储管理 {#sec-memory-management}
处理器的存储管理部件Memory Management Unit简称MMU支持虚实地址转换、多进程空间等功能是通用处理器体现“通用性”的重要单元也是处理器和操作系统交互最紧密的部分。
本节将介绍存储管理的作用、意义和一般性原理并以Linux/LoongArch系统为例重点介绍存储管理中TLB的结构、操作方式以及TLB地址翻译过程中所涉及异常的处理。
### 存储管理的原理
存储管理构建虚拟的内存地址并通过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的地址转换过程如图\@ref(fig:tlb-convert)所示。
```{r tlb-convert, echo=FALSE, fig.align='center', fig.cap="包含TLB的地址转换过程", out.width='50%'}
knitr::include_graphics("images/chapter3/tlb_convert.png")
```
处理器用地址空间标识符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。
### TLB的结构和使用
#### 地址空间和地址翻译模式
在介绍LoongArch指令系统中TLB相关的存储管理的机制前首先简要了解一下LoongArch中地址空间和地址翻译模式的基本内容。LoongArch处理器支持的内存物理地址空间范围表示为0 - 2^PALEN^-1。在LA32架构下PALEN理论上是一个不超过36的正整数在LA64架构下PALEN理论上是一个不超过60的正整数。
LoongArch指令系统中的虚拟地址空间是线性平整的。对于PLV0级来说LA32架构下虚拟地址空间大小为2^32^字节LA64架构下虚拟地址空间大小为2^64^字节。不过对于LA64架构来说2^64^字节大小的虚拟地址空间并不都是合法的,可以认为存在一些虚拟地址的空洞。合法的虚拟地址空间与地址映射模式紧密相关。
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]位之上的所有位是该位的符号扩展。
#### TLB结构
页表映射模式存储管理的核心部件是TLB。LoongArch指令系统下TLB分为两个部分一个是所有表项的页大小相同的单一页大小TLBSingular-Page-Size TLB简称STLB另一个是支持不同表项的页大小可以不同的多重页大小TLBMultiple-Page-Size TLB简称MTLB。STLB的页大小可通过STLBPS控制寄存器进行配置。
在虚实地址转换过程中STLB和MTLB同时查找。相应地软件需保证不会出现MTLB和STLB同时命中的情况否则处理器行为将不可知。MTLB采用全相联查找表的组织形式STLB采用多路组相联的组织形式。对于STLB如果其有2^INDEX^组且配置的页大小为2^PS^字节那么硬件查询STLB的过程中是将虚地址的\[PS+INDEX:PS\]位作为索引值来访问各路信息的。接下来介绍LoongArch64指令系统中TLB单个表项的结构如图\@ref(fig:tlb-entry)所示。
```{r tlb-entry, echo=FALSE, fig.align='center', fig.cap="LoongArch64指令系统中TLB表项结构", out.width='100%'}
knitr::include_graphics("images/chapter3/tlb_entry.png")
```
在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表明该页表项是有效且被访问过的。
#### 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域中 ^[PC域不包含指令地址的最低两位因为能触发TLB重填异常的指令的PC最低两位一定为0所以这两位不需要记录。]处理器还会将引发该异常的访存虚地址填入CSR.TLBRBAV的VAddr域并从该虚地址中提取虚双页号填入CSR.TLBREHI的VPPN域。当触发非TLB重填异常的其他TLB类异常时除了像普通异常发生时一样更新CRMD、PRMD和ERA这些控制状态寄存器的相关域外处理器还会将引发该异常的访存虚地址填入CSR.BADV的VAddr域并从该虚地址中提取虚双页号填入CSR.TLBEHI的VPPN域。
#### 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。三类寄存器的具体格式如图\@ref(fig:tlb-reg)所示。
```{r tlb-reg, echo=FALSE, fig.align='center', fig.cap="LoongArch指令系统TLB相关控制寄存器", out.width='100%'}
knitr::include_graphics("images/chapter3/tlb_reg.png")
```
上述寄存器中第二类专用于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位域
上述第三类寄存器的工作及使用方式将在后面\@ref(sec-tlb-ex)节中予以介绍。
#### 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表项将被无效掉。
### TLB地址翻译相关异常的处理 {#sec-tlb-ex}
上一节介绍了LoongArch指令系统中与TLB相关的硬件规范这些设计为操作系统提供了必要的支持而存储管理则需要CPU和操作系统紧密配合CPU硬件在使用TLB进行地址翻译的过程中将产生相关异常再由操作系统介入进行异常处理。本节将重点讲述这些异常处理的过程。
#### 多级页表结构
Linux操作系统通常采用多级页表结构。对于64位的LoongArch处理器如果其有效虚地址位宽为48位那么当Linux操作系统采用16KB页大小时其页表为三级结构如图\@ref(fig:page-table)所示。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域中。
```{r page-table, echo=FALSE, fig.align='center', fig.cap="Linux/LoongArch三级页表结构", out.width='100%'}
knitr::include_graphics("images/chapter3/page_table.png")
```
#### 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指令的功能简述如表\@ref(tab:lddir-ldpte)所示。
```{r lddir-ldpte, echo = FALSE, message=FALSE, tab.cap='LoongArch软件页表遍历指令', out.width='100%'}
autonum <- run_autonum(seq_id = "tab", bkm = "lddir-ldpte", bkm_all = TRUE)
data.frame('指令' = c('LDDIR rd, rj, level', 'LDPTE rj, seq'),
'描述' = c('将rj寄存器中的值作为当前页目录表的基地址同时根据CSR.TLBRBADV中VAddr域存放的TLB缺失地址以及PWCL、PWCH寄存器中定义的页目录表level索引的起始位置和位宽信息计算出当前目录页表的偏移量两者相加作为访存地址从内存中读取待访问页目录表/页表的基址写入rd寄存器中。',
'将rj寄存器中的值作为末级页表的基地址同时根据CSR.TLBRBADV中VAddr域存放的TLB缺失地址以及PWCL、PWCH寄存器中定义的末级页表索引的起始位置和位宽信息计算出末级页表的偏移量两者相加作为访存地址从内存中读取偶数号(seq=0)或奇数号(seq=1)页表项的内容将其写入到TLBRELO0或TLBRELO1寄存器中。')) %>%
flextable() %>%
set_caption(caption="LoongArch软件页表遍历指令", autonum = autonum) %>%
merge_v() %>%
width(j=1, width=1.5) %>%
width(j=2, width=4.5) %>%
theme_box()
```
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重填异常处理程序如下。可见遍历一个三级页表的处理过程只需要执行9条指令且每增加一级页表只需增加一条LDDIR指令即可。
```
csrwr $t0, CSR_TLBRSAVE
csrrd $t0, CSR_PGD
lddir $t0, $t0, 3 #访问页目录表PGD
lddir $t0, $t0, 1 #访问页目录表PMD
ldpte $t0, 0 #取回偶数号页表项
ldpte $t0, 1 #取回奇数号页表项
tlbfill
csrrd $t0, CSR_TLBRSAVE
ertn
```
#### 其它TLB地址翻译相关异常处理
除了TLB重填异常外LoongArch指令系统下常见的TLB类异常有取指操作页无效异常、load操作页无效异常、store操作页无效异常和页修改异常。这四种异常在Linux/LoongArch中处理的伪代码如下所示其中取指操作页无效异常和load操作页无效异常的处理流程一致。伪代码中的load pte函数遍历页表并取得页表项DO_FAULT函数在内存中分配物理页并把该页内容从对换区中取到内存_PAGE_PRESENT、_PAGE_READ和_PAGE_WRITE分别表示相应的物理页是否在内存中、是否可读、是否可写。
```
TLB modified exception:
(1)load pte;
(2)if(_PAGE_WRITE) set VALID|DIRTY, reload tlb, tlbwr;
else DO_FAULT(1);
TLB load exception:
(1)load pte;
(2)if(_PAGE_PRESENT && _PAGE_READ) set VALID, reload tlb, tlbwr;
else DO_FAULT(0);
TLB store exception:
(1)load pte;
(2)if(_PAGE_PRESENT && _PAGE_WRITE) set VALID|DIRTY, reload tlb, tlbwr;
else DO_FAULT(1);
```
下面通过一个例子来深入分析处理器、操作系统以及应用程序间的交互。图3.9是一个分配数组和对数组赋值的小程序。从程序员的角度看,这个程序很简单,但从结构和操作系统的角度看,这个程序的执行却涉及复杂的软硬件交互过程。
```
array=(int *)malloc(0x1000);
for(i=0;i<1024;i++) array[i]=0;
```
该用户程序首先调用内存分配函数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重填异常的处理速度。
## 本章小结
本章介绍了异常的类型和通用处理过程,并对中断这类特殊异常进行了探讨。在计算机系统中,处理器全速地执行指令,而异常与中断起到纽带的作用,使得运行级别、存储管理等机制有机结合,共同打造安全、高效、易用的系统。
本章首先介绍存储管理的意义并引出对页表进行硬件加速的结构TLB随后以LoongArch指令系统为例介绍TLB的结构和使用方法最后介绍TLB异常的类型和处理方法。存储管理在计算机系统中得到了广泛的应用为使存储管理系统流畅运行硬件设计、软件设计需紧密配合协同优化。
## 习题
1. 请说明LoongArch指令系统中为何要定义ERTN指令用于异常处理的返回。
2. 简述LoongArch与X86在异常处理过程中的区别。
3. 简述精确异常与非精确异常的区别,并在已有的处理器产品实现中找出一个非精确异常示例。
4. 在一台Linux/LoongArch机器上执行如下程序片段假设数组a和b的起始地址都是8KB边界对齐的操作系统仅支持4KB页大小。处理器中的TLB有32项采用LRU替换算法。如果在该程序片段开始执行前数组a和b均从未被访问过且程序片段执行过程中未发生中断同时忽略程序代码和局部变量i所占地址空间的影响请问执行该程序片段的过程中会发生多少次与TLB地址翻译相关的异常
```{r memcpy-program, echo=FALSE, fig.align='center', out.width='50%'}
knitr::include_graphics("images/chapter3/memcpy_program.png")
```
5. 请用C语言伪代码形式描述一台64位LoongArch机器上的TLB进行访存虚实地址转换的过程包含TLB地址翻译相关异常的判定过程提示①可以将TLB的每一项定义为一个结构体将整个TLB视作一个结构体数组②无须直接体现过程中电路的并发执行特性只需要确保最终逻辑状态一致即可。
\newpage