- 计算机体系结构基础(第3版)
- 胡伟武等
- 5458字
- 2021-12-04 00:21:03
3.2 异常与中断
计算机通常按照软件的执行流进行顺序执行和跳转,但有时需要中断正常的执行流程去处理其他任务,可以触发这一过程的事件统称为异常。
3.2.1 异常分类
从来源来看,异常可分为以下6种。
1)外部事件:来自CPU核外部的事件,来自处理器内部其他模块或者处理器外部的真实物理连线也称为中断。中断的存在使得CPU能够异步地处理多个事件。在操作系统中,为了避免轮询等待浪费CPU时间,与IO相关的任务通常都会用中断方式进行处理。中断事件的发生往往是软件不可控制的,因此需要一套健全的软硬件机制来防止中断对正常执行流带来影响。
2)指令执行中的错误:执行中的指令的操作码或操作数不符合要求,例如不存在的指令、除法除以0、地址不对齐、用户态下调用核心态专有指令或非法地址空间访问等。这些错误使得当前指令无法继续执行,应当转到出错处进行处理。
3)数据完整性问题:当使用ECC等硬件校验方式的存储器发生校验错误时,会产生异常。可纠正的错误可用于统计硬件的风险,不可纠正的错误则应视出错位置进行相应处理。
4)地址转换异常:在存储管理单元需要对一个内存页进行地址转换,而硬件转换表中没有有效的转换对应项可用时,会产生地址转换异常。
5)系统调用和陷入:由专有指令产生,其目的是产生操作系统可识别的异常,用于在保护模式下调用核心态的相关操作。
6)需要软件修正的运算:常见的是浮点指令导致的异常,某些操作和操作数的组合硬件由于实现过于复杂而不愿意处理,寻求软件的帮助。
表3.2列举了LoongArch指令系统中主要的异常。
表3.2 LoongArch指令系统中主要的异常
[1] 这里“CPU核”可以更为严格地理解为CPU核的指令流水线,即旨在强调这些事件并非直接由指令引发。以定时中断为例,它由一个物理上独立于CPU指令流水线而存在的定时器触发,但是这个定时器既可以放置在CPU核内部也可以放置在CPU核外部。
3.2.2 异常处理
1.异常处理流程
异常处理的流程包括异常处理准备、确定异常来源、保存执行状态、处理异常、恢复执行状态并返回等。主要内容是确定并处理异常,同时正确维护上下文环境。异常处理是一个软硬件协同的过程,通常CPU硬件需要维护一系列控制状态寄存器(域)以用于软硬件之间的交互。LoongArch指令系统中与异常(含中断)处理相关的控制状态寄存器格式如图3.2所示。
图3.2 LoongArch异常处理相关控制状态寄存器
下面对异常处理流程的五个阶段进行介绍。
1)异常处理准备。当异常发生时,CPU在转而执行异常处理前,硬件需要进行一系列准备工作。
首先,需要记录被异常打断的指令的地址(记为EPTR)。这里涉及精确异常的概念,指发生任何异常时,被异常打断的指令之前的所有指令都执行完,而该指令之后的所有指令都像没执行一样。在实现精确异常的处理器中,异常处理程序可忽略因处理器流水线带来的异常发生位置问题。异常处理结束后将返回EPTR所在地址,重新执行被异常打断的指令,因此需要将EPTR记录下来。EPTR存放的位置因不同指令集而不同,LoongArch存于CSR.ERA,PowerPC存于SRR0/CSRR0,SPARC存于TPC[TL],X86则用栈存放CS和EIP组合。
其次,调整CPU的权限等级(通常调整至最高特权等级)并关闭中断响应。在LoongArch指令系统中,当异常发生时,硬件会将CSR.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指令有类似效果。
2.异常嵌套
在异常处理的过程中,又有新的异常产生,这时就会出现异常嵌套的问题。当产生异常嵌套时,需要保存被打断的异常处理程序的状态,这会消耗一定的栈资源,因此无限的异常嵌套是无法容忍的。异常嵌套通常基于优先级,只有优先级更高的异常才能进行嵌套,低优先级或同优先级的异常只能等待当前异常处理完成,系统支持的优先级级数就是异常嵌套的最大层数。
在LoongArch指令系统中,异常嵌套时被打断的异常处理程序的状态的保存和恢复主要交由软件处理,这就需要保证异常处理程序在完成当前上下文的保存操作之前,不会产生新的异常,或者产生的新异常不会修改当前需要保存的上下文。这两方面要求仅通过异常处理程序开发人员的精心设计是无法完全保证的,因为总有一些异常的产生原因是事先无法预知的,如中断、机器错、TLB重填等。为此需要设计硬件机制以保证这些情况发生时不至于产生嵌套异常,或即使产生嵌套异常也能保证软件可以获得所要保存上下文的正确内容。例如,可以在跳转到异常入口的过程中关闭全局中断使能以禁止中断异常发生,还可以在发生嵌套异常的时候将可能被破坏而软件又来不及保存的上下文信息由硬件暂存到指定的控制状态寄存器或内存区域。
[1] 这只是通常的处理流程,但并非始终如此,存在某些异常处理场景,其结束后返回执行的并非最初被该异常打断的指令。例如,当执行SYSCALL指令而陷入系统调用异常处理时,肯定不能在处理结束后返回触发异常的SYSCALL指令,否则将陷入死循环。再譬如,当发生中断并陷入操作系统核心进行处理时,处理结束后,操作系统可能将其他进程或线程调度到该CPU上执行,显然此时返回执行的并不是最初被中断打断的那条指令。
[2] 其实TLB重填异常发生时,这一信息将被记录在CSR.TLBRBERA中;机器错误异常发生时,这一信息将被记录在CSR.MERRERA中。更多细节请见下文中的说明。
3.2.3 中断
异常处理的流程是通用的,但有两类异常出现的机会确实比其他类型大很多。一类是地址转换异常,当片内从虚地址到物理地址的地址转换表不包含访问地址时,就会产生缺页异常,在3.3节中我们将进行详细介绍。另一类常见的异常就是中断,中断在外部事件想要获得CPU注意时产生。由于外部事件的不可控性,中断处理所用的时间较为关键。在嵌入式系统中,CPU的主要作用之一就是处理外设相关事务,因此中断发生的数量很多且非常重要。本节以LoongArch指令系统为例介绍中断相关的重要内容。
1.中断传递机制
中断从系统中各个中断源传递到处理器主要有两种形式:中断线和消息中断。
1)中断线。用中断线传递是最简便直接的方式。当系统的中断源不多时,直接连到处理器引脚即可。若中断源较多,可使用中断控制器汇总后再与处理器引脚相连。由于连线会占用引脚资源,一般只在片上系统(System On Chip,简称SoC)中才会给每个外设连接单独的中断线,板级的中断线一般采用共享的方式。比如PCI上有四根中断线,供所有的设备共享。中断处理程序在定位到哪根中断线发生中断后,逐个调用注册在该中断线的设备中断服务。
LoongArch指令系统支持中断线的中断传递机制,共定义了13个中断,分别是:1个核间中断(IPI),1个定时器中断(TI),1个性能监测计数溢出中断(PMI),8个外部硬中断(HWI0~HWI7),2个软中断(SWI0~SWI1)。其中所有中断线上的中断信号都采用电平中断,且都是高电平有效。当有中断发生时,这种高电平有效中断方式输入给处理器的中断线上将维持高电平状态直至中断被处理器响应处理。无论中断源来自处理器核外部还是内部,是硬件还是软件置位,这些中断信号都被不间断地采样并记录到CSR.ESTAT中IS域的对应比特位上。这些中断均为可屏蔽中断,除了CSR.CRMD中的全局中断使能位IE外,每个中断各自还有其局部中断使能控制位,在CSR.ECFG的LIE域中。当CSR.ESTAT中IS域的某位为1且对应的局部中断使能和全局中断使能均有效时,处理器就将响应该中断,并进入中断处理程序入口处开始执行。
用中断线方式传递中断有一些限制。首先是扩展性不够强,在搭建较复杂的板级系统时会引入过多的共享,降低中断处理的效率。其次,中断处理过程需要通过查询中断控制器以及设备上的状态寄存器来确认中断和中断原因,中间有较长的延迟,同样不利于提高效率。在多处理器平台中,高性能外设(如万兆网卡)对中断处理的性能有更高的要求,需要实现多处理器的负载均衡、中断绑定等功能,传统的中断线方式难以做到。而这正好是消息中断的长处。
2)消息中断。消息中断以数据的方式在总线上传递。发中断就是向指定的地址写一个指定的数。相比在总线外增加专门的中断线的“带外”(Side-Band)传输形式,消息中断在“带内”(In-Band)传输。增加中断时不需要改动消息传递的数据通路,因而有较高的扩展性和灵活性,也为更高程度的优化提供了可能。比如一个设备可以申请更多的中断号,使中断处理程序无须查询设备状态,只根据中断号就能知道应当做什么处理。
2.向量化中断
LoongArch指令系统默认支持向量化中断,其13个中断线各自具有独立的中断处理程序入口地址。在LoongArch指令系统中,中断被视作一类特殊的异常进行处理,因此在具体计算中断处理程序入口地址时将SWI0~IPI这13个中断依次“视作”异常编号64~76的异常,用异常处理程序入口地址的统一计算方式进行计算。向量化中断的好处之一是省去了中断处理程序开头处识别具体中断源的开销,可以进一步加速中断的处理。
X86指令系统支持的向量化中断方案更复杂一些,其在地址空间的指定位置处存放中断向量表(IVT,实模式下默认为0地址)或中断描述符表(IDT,保护模式),中断向量表中存放中断入口地址的段地址和偏移量,中断描述符表还包含权限等级和描述符类别的信息。X86的向量化中断机制最多可支持256个中断和异常,0~19号为系统预设的异常和NMI,20~31是Intel保留的编号,32号开始可用于外部中断,详细的实现可参考Intel相关手册。
3.中断的优先级
在支持多个中断源输入的指令系统中,需要规范在多个中断同时触发的情况下,处理器是否区别不同来源的中断的优先级。当采用非向量中断模式的时候,处理器通常不区别中断的优先级,此时若需要对中断进行优先级处理,可以通过软件方式予以实现,其通常的实现方案是:
1)软件随时维护一个中断优先级(IPL),每个中断源都被赋予特定的优先级。
2)正常状态下,CPU运行在最低优先级,此时任何中断都可触发。
3)当处于最高中断优先级时,任何中断都被禁止。
4)更高优先级的中断发生时,可以抢占低优先级的中断处理过程。
当采用向量中断模式的时候,处理器通常不可避免地需要依照一套既定的优先级规则来从多个已生效的中断源中选择一个,跳转到其对应的处理程序入口处。LoongArch指令系统实现的是向量中断,采用固定优先级仲裁机制,具体规则是硬件中断号越大优先级越高,即IPI的优先级最高,TI次之,…,SWI0的优先级最低。
4.中断使能控制位的原子修改
在中断处理程序中,经常会涉及中断使能控制位的修改,如关闭、开启全局中断使能。在大多数指令系统中,这些中断使能控制位位于控制状态寄存器中,因此软件在进行中断使能控制调整时,必须关注修改的原子性问题。以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一条指令完成,所以自然确保了修改的原子性。
[1] 尽管将ECFG控制状态寄存器中的VS域置0后,所有的异常处理程序入口地址将变为同一个,此时中断不再是向量中断形式,但这种模式并不是LoongArch指令系统推荐的方式。