流水线中的软件中断会如何处理?

时间:2019-01-29 18:51:32

标签: x86 intel pipeline interrupt-handling trap

阅读此内容后:

When an interrupt occurs, what happens to instructions in the pipeline?

关于软件中断会发生什么的信息并不多,但是我们确实了解到以下内容:

  

相反,异常(如页面错误)将指令标记为受影响。当该指令即将提交时,此时将清除异常之后的所有后续指令,并重定向指令提取。

我想知道管道中的软件中断(INT 0xX)会发生什么,首先,何时检测到它们?是否在预解码阶段检测到它们?在指令队列中?在解码阶段?还是他们到达后端并立即完成操作(不要进入预订站),依次退休,退休阶段就发现这是一条INT指令(似乎很浪费)。

让我们说它是在预解码时拾取的,必须有一种信号通知IFU停止获取指令或实际上对其进行时钟/电源门控,或者如果它是在指令队列中拾取的,则是一种在其之前刷新指令的方法在队列中。然后必须有一种向某种逻辑(“控制单元”)发送信号的方式,例如生成用于软件中断的索引(索引到IDT,检查DPL> = CPL> =段RPL等),是幼稚的的建议,但是如果有人对此过程有更好的了解,那就太好了。

我还想知道当该过程受到干扰(即发生硬件中断(记住陷阱不能清除EFLAGS中的IF))时如何处理它,现在必须开始一个全新的中断处理和uop生成过程,它将如何恢复到随后处理软件中断的状态。

2 个答案:

答案 0 :(得分:3)

我同意彼得在回答中所说的一切。尽管它们可以是实现INTn指令的多种方式,但很可能会针对CPU设计的简化而非性能来调整实现。可以非推测地确定存在这样的指令的最早点是在流水线的解码阶段的末尾。可能可以预测所提取的字节是否包含可能引发或确实引发异常的指令,但我找不到研究此想法的研究论文,因此似乎并不值得。 >

INTn的执行涉及从IDT提取指定的条目,执行许多检查,计算异常处理程序的地址,然后告诉提取单元从那里开始预提取。此过程取决于处理器的操作模式(实模式,64位模式等)。该模式由CR0CR4Eflags寄存器中的多个标志描述。因此,要实际调用异常处理程序将花费很多时间。在Skylake中,有4个简单解码器和1个复杂解码器。简单的解码器只能发出一个融合的uop。复合解码器最多可以发射4个融合uops。它们都无法处理INTn,因此需要使用MSROM来执行软件中断。请注意,INTn指令本身可能会导致异常。在这一点上,未知INTn本身是否将控制权更改为指定的异常处理程序(无论其地址是什么)还是其他某种异常处理程序。可以肯定的是,指令流一定会在INTn处结束并在其他地方开始。

有两种可能的方式来激活微码定序器。第一个是在解码需要多于4个微指令的宏指令时,类似于rdtsc。第二个是退休指令时,至少其指令的ROB条目中具有有效的事件代码。根据{{​​3}}专利,有专门的事件代码用于软件中断。因此,我认为INTn被解码为一个带有中断向量的uop(或最多4uop)。 ROB已经需要具有一个字段来保存描述相应指令是否引发异常以及哪种异常的信息。同一字段可用于保存中断向量。 uop只是经过分配阶段,可能不需要调度到执行单元之一中,因为不需要进行任何计算。当uop即将退休时,ROB确定它是INTn,并且应该引发一个事件(请参见专利中的图10)。此时,有两种可能的处理方式:

  • ROB调用通用微代码辅助,该辅助首先检查处理器的当前操作模式,然后选择与当前模式相对应的专门辅助。
  • ROB单元本身包括检查当前操作模式并选择相应辅助的逻辑。它将辅助地址传递给负责引发事件的逻辑,逻辑继而指示MSROM发出存储在该地址的辅助例程。该例程包含可读取IDT条目并执行其余异常处理程序调用过程的uops。

在执行协助过程中,可能会发生异常。它将像引起异常的任何其他指令一样进行处理。 ROB单元从ROB中提取异常描述,并调用一个辅助程序来处理它。

无效的操作码可以用类似的方式处理。在predcode阶段,唯一重要的是正确确定无效操作码之前的指令长度。在执行了这些有效指令之后,边界是无关紧要的。当简单的解码器收到无效的操作码时,它将发出一个特殊的uop,其唯一目的只是引发无效的操作码异常。负责最后一条有效指令之后的指令的其他解码器都可以发出特殊的uop。由于指令是按顺序淘汰的,因此可以保证第一个特殊的uop会引发异常。当然,除非先前的uop引发异常或发生分支错误预测或发生内存排序清除事件。

当任何一个解码器发出特殊的uop时,获取和解码阶段可能会停止,直到确定了宏指令异常处理程序的地址为止。这可能是uop指定的异常,也可能是其他异常。对于处理该特殊uop的每个阶段,该阶段本身都可以停止(掉电/时钟门控)。这样可以节省电量,并且我认为它很容易实现。

或者,如果另一个逻辑核心处于活动状态,则将其视为该逻辑线程将其前端周期放弃给另一个超线程的任何其他原因。分配周期通常在超线程之间交替,但是当一个线程停顿(例如ROB已满或前端为空)时,另一个线程可以在连续的周期中进行分配。这也可能在解码器中发生,但是也许可以用足够大的代码块对其进行测试,以阻止它从uop缓存中运行。 (或者太密集而无法进入uop缓存)。

答案 1 :(得分:2)

来自Andy @Krazy Glew的引用是关于在执行“正常”指令期间发现的同步异常,例如mov eax, [rdi]如果发现RDI指向未映射的页面,则抛出#PF。 1 您希望这不会出错,因此,如果分支错误预测或较早的例外情况使您推迟任何事情直到退休。


但是,是的,他的答案没有详细说明流水线如何针对同步int陷阱指令进行优化,而我们在解码时知道这些陷阱指令总是会导致异常。陷阱指令在整个指令组合中也很少见,因此对其进行优化并不能节省很多精力。值得做些简单的事情。

正如Andy所说,当前的CPU不会重命名特权级别,因此无法推测为中断/异常处理程序,因此在看到intsyscall之后停滞获取/解码工作肯定是明智的事情。我只是要写int或“陷阱指令”,但是syscall / sysenter / sysret / iret和其他特权中的做法也一样。更改“分支”指令。 1-byte versions of int类似于int30xcc)和int10xf1)。有条件的溢出陷阱into很有趣;对于在无陷阱情况下的非恐怖性能,可能会假设它没有被陷阱捕获。 (当然,还有vmcall和一些用于VMX扩展的东西,可能还有SGX EENTER,还有其他东西。但是就停滞管道而言,我猜所有陷阱指令都是相同的有条件的into除外)


我认为,像lfence一样,CPU不会推测陷阱指令。您是对的,将这些uops放入其中是没有意义的。管道,因为int之后的所有内容肯定都会被清空。

如果在int指令变为非推测性之前会从IVT(实模式中断向量表)或IDT(中断描述符表)中获取int处理程序的地址,则获取IDK在后端。可能吧。 (某些陷阱指令,例如syscall,使用MSR设置处理程序地址,因此从那里开始获取代码可能会很有用,尤其是如果它提早触发了L1i未命中。必须权衡以下可能性:分支未命中后,在错误的路径上看到int和其他陷阱说明。)

误认为命中陷阱指令的可能性可能非常罕见,如果前端发现陷阱指令,则应该立即从IDT开始加载或预取syscall入口点,这是值得的。前端足够聪明,可以处理所有这一切。但这可能不是。将花哨的东西留给微代码可以限制前端的复杂性。即使在syscall繁重的工作负载中,陷阱也很少见。批量工作以跨用户/内核障碍传递更大的块是一件好事,因为在Spectre之后,便宜的syscall非常困难...


因此,最迟将在问题/重命名中检测到陷阱(该陷阱已经知道如何暂停(部分)序列化指令),并且不会在输出中分配更多的uu-顺序的后端,直到int退休并且采取了例外措施为止。

但是似乎有可能在解码中检测到它,并且不会在绝对需要例外的指令之后再解码。 (而且我们不知道下一步要去哪里。)解码器阶段确实知道如何停顿,例如非法指令陷阱。

  

假设它是在预解码中获取的

这可能不切实际,直到完全解码,您才知道它是int。预解码只是在Intel CPU上进行指令长度查找。我假设intsyscall的操作码只是长度相同的许多操作码中的两个。

在硬件中构建以更深入地查找陷阱指令将比在预解码中花费更多的精力。 (请记住,陷阱非常少见,并且尽早发现陷阱只会节省电量,因此,在将陷阱传递到解码器后停止预解码,您所花费的精力无法比节省更多的精力。

您需要对int进行解码,以便其微代码能够执行并再次使CPU运行中断处理程序,但是从理论上讲,您可以在之后的通过。

例如,在常规解码器中会识别出分支预测遗漏的跳转指令,因此对于主解码阶段而言,不再进行任何处理就更有意义。


超线程

发现摊位时,您不仅可以对前端进行电源门控。您让另一个逻辑线程拥有所有周期。

超线程使前端在没有后端帮助的情况下开始从IDT指向的内存中获取数据的价值较小。如果另一个线程没有停止,并且可以在清理该陷阱的同时受益于额外的前端带宽,则CPU会做有用的工作。

我当然不会排除从SYSCALL入口点获取代码的情况,因为该地址位于MSR中,并且它是在某些工作负载中与性能相关的少数陷阱之一。

我很好奇的另一件事是,一个逻辑核心切换特权级别的性能是否会对另一核心的性能产生多大的影响。为了测试这一点,您需要构建一些工作负载,这些工作负载会限制您选择的前端问题带宽,后端端口,后端dep链延迟或后端在中长距离上查找ILP的能力(RS大小或ROB大小)。或组合或其他东西。然后将运行在核心上的测试工作负载的周期/迭代与自身进行比较,并与紧密的dec/jnz线程,4x pause / dec/jnz工作负载和进行ENOSYS系统调用的syscall工作负载共享一个核心在Linux下。也许还有int 0x80的工作量来比较不同的陷阱。


脚注1:异常处理,如正常负载下的#PF。

(主题外,re:无辜的看似出错的指令,而不是陷阱,这些指令可以在解码器中检测为引发异常)。

您一直等到提交(退休),因为您不想立即开始进行昂贵的管道刷新,只是发现该指令在分支未命中(或较早的故障指令)的阴影下,并且不应首先已经(用那个错误的地址)运行了。让快速的分支恢复机制抓住它。

这种等待至退休的策略(以及危险的L1d缓存不会将L1d命中的加载值压缩为0,而TLB表示有效,但没有读取权限)是为什么Meltdown和{{3 }}漏洞可在某些Intel CPU上使用。 (L1TF)。理解Meltdown有助于理解高性能CPU中的同步异常处理策略:标记指令并仅在如果达到退役状态时才执行任何操作,这是一个很好的廉价策略,因为异常很少见。

如果后端中的任何uop检测到未决的#PF或其他异常,则让执行单元发信号回前端以停止获取/解码/发出问题显然不那么复杂强>。 (大概是因为这样可以更紧密地耦合CPU的其他部分,而这些部分之间相距甚远。)

由于在从分支未命中快速恢复期间,错误路径中的指令可能仍在执行中,并且确保仅针对预期的错误停止前端,因为我们认为这是当前正确的执行路径,因此需要更多跟踪。后端中的所有uop都曾经被认为处于正确的路径上,但是到执行单元的末端时可能不再存在。

如果您没有进行快速恢复,那么可能值得后端发送“东西不对”信号来暂停前端,直到后端实际采取异常措施或发现正确的路径。

使用SMT(超线程),当一个线程检测到当前正在推测导致故障的(可能正确的)路径时,这可能为其他线程留下更多的前端带宽。

所以这个想法可能有一些好处;我想知道是否有CPU可以做到吗?