是的,包括 8080 和 x86 在内的所有“正常”ISA 都保证指令对于同一内核上的中断是原子的。 一条指令已完全执行并且其所有架构效果都是可见的(在中断处理程序),或者它们都不是。任何与此规则的偏差通常都会仔细记录。
例如,Intel's x86 manual vol.3 (~1000 page PDF) 确实特别强调了这一点:
6.6 程序或任务重启
为了允许在处理异常或中断后重新启动程序或任务,所有异常
(中止除外)保证报告指令边界上的异常。 所有中断都保证是
采取指令边界。
Intel's vol.1 manual 中的一段旧段落谈到使用 cmpxchg 的单核系统 没有 lock 前缀以原子方式读取-修改-写入(相对于其他软件,而不是硬件 DMA访问)。
CMPXCHG 指令通常用于测试和修改信号量。它检查是否有信号量
免费。如果信号量空闲,则标记为已分配;否则获取当前所有者的 ID。 这一切都完成了
在一个不间断的操作中 [因为它是一条指令]。在单处理器系统中,CMPXCHG 指令无需
在执行多条指令以测试和修改信号量之前切换到保护级别 0(以禁用中断)。
对于多处理器系统,CMPXCHG 可以与 LOCK 前缀组合来执行比较和
原子交换操作。 (参见第 8 章“多处理器管理”中的“锁定原子操作”,
英特尔® 64 和 IA-32 架构软件开发人员手册第 3A 卷,了解有关原子的更多信息
操作。)
(有关 lock 前缀的更多信息,以及它是如何实现与非锁定 add [mem], 1 的,请参阅 Can num++ be atomic for 'int num'?)
正如英特尔在第一段中指出的那样,实现多指令原子性的一种方法是禁用中断,然后在完成后重新启用。这比使用互斥锁更好保护一个更大的整数,尤其是当您谈论主程序和中断处理程序之间共享的数据时。如果在主程序持有锁时发生中断,它不能等待锁被释放;那永远不会发生。
在简单的有序流水线,尤其是微控制器上,禁用中断通常非常便宜。 (有时您需要保存之前的中断状态,而不是无条件启用中断。例如,可能在中断已禁用的情况下调用的函数。)
无论如何,禁用中断是您可以在 8080 上以原子方式使用 64 位整数执行某些操作的方法。
根据为该指令记录的规则,一些长时间运行的指令是可中断的。
例如x86 的 rep-string 指令,如 rep movsb(任意大小的单指令 memcpy)在架构上等同于重复基本指令 (movsb) RCX 次,每次递减 RCX 并递增或递减指针输入(RSI和 RDI)。复制期间到达的中断可以设置 RCX starting_value - byte_copied 并且(如果 RCX 非零)让 RIP 指向该指令,因此在中断后恢复 rep movsb 将再次运行并完成其余的复制。
其他 x86 示例包括 SIMD 收集负载 (AVX2/AVX512) 和分散存储 (AVX512)。例如。 vpgatherdd ymm0, [rdi + ymm1*4], ymm2 最多执行 8 个 32 位加载,根据 ymm2 的元素设置。结果被合并到ymm0中。
在正常情况下(在收集期间没有中断、没有页面错误或其他同步异常),您在目标寄存器中获取数据,并且掩码寄存器最终归零。因此,掩码寄存器为 CPU 提供了存储进度的地方。
收集和分散很慢,并且可能需要触发多个页面错误,因此对于同步异常,即使在处理页面错误取消映射所有其他页面的病态条件下,这也可以保证向前进展。但更相关的是,这意味着避免在中间元素页面错误时重做 TLB 未命中,并且在异步中断到达时不丢弃工作。
其他一些长时间运行的指令(例如 wbinvd 刷新所有内核中的所有数据缓存)在架构上不可中断,甚至在架构上微可中止(丢弃部分工作并去处理中断)。它具有特权,因此用户空间无法将其作为拒绝服务攻击执行,从而导致高中断延迟。
记录有趣行为的相关示例是当 x86 popad 离开堆栈顶部(段限制)时。这是一个异常(不是外部中断),在第 3 卷手册中的第 6.5 节异常分类(即故障/陷阱/中止,有关详细信息,请参阅 PDF)中进行了记录。
注意
通常报告为故障的一个异常子集不可重新启动。此类异常会导致损失
一些处理器状态。例如,执行一个POPAD instruction,其中堆栈帧
越过堆栈段的末尾会导致报告错误。在这种情况下,
异常处理程序看到指令指针 (CS:EIP) 已恢复,就好像 POPAD
指令尚未执行。 但是,内部处理器状态(通用
寄存器)将被修改。这种情况被认为是编程错误。一个应用程序
导致此类异常的应由操作系统终止。
请注意,这仅在popad 本身导致异常,不会出于任何其他原因。外部中断不能像 rep movsb 或 vpgatherdd 那样拆分 popad
(我猜出于popad 故障的目的,它有效地迭代工作,一次弹出 1 个寄存器并在逻辑上修改 RSP/ESP/SP 以及目标寄存器。而不是检查整个区域,它将在开始之前加载段限制,因为我猜这需要额外的添加。)
无序 CPU 在中断时回滚到退出状态。
像现代 x86 这样具有乱序执行和将复杂指令拆分为多个微指令的 CPU 仍然可以确保这种情况。当中断到达时,CPU 必须在两条正在运行的指令之间选择一个点作为中断在架构上发生的位置。它必须丢弃已经在解码或开始执行任何后续指令时完成的任何工作。假设中断返回,它们将被重新获取并重新开始执行。
见When an interrupt occurs, what happens to instructions in the pipeline?。
正如 Andy Glew 所说,当前的 CPU 不会重命名特权级别,因此逻辑上发生的事情(中断/异常处理程序在早期指令完成后执行)与 实际 发生的事情相匹配。
有趣的事实是:x86 中断没有完全序列化,至少在纸面上不能保证。 (在 x86 术语中,像 cpuid 和 iret 这样的指令被定义为序列化;耗尽 OoO 后端和存储缓冲区,以及其他任何可能重要的东西。这是一个非常强大的障碍和许多其他事情 不是,例如mfence。)
实际上(因为 CPU 实际上不会重命名特权级别),当中断处理程序运行时,乱序后端中不会有任何旧的用户空间指令/微指令仍在运行.
异步(外部)中断也可能耗尽存储缓冲区,这取决于我们如何解释Intel's SDM vol.3 11.10 的措辞:*the
在以下情况下,存储缓冲区的内容总是被排空到内存中:“...”当异常或中断产生”。显然,这适用于异常(CPU 内核本身产生中断) ),也可能意味着在服务中断之前。
(来自 retired 存储指令的存储数据不是推测性的;它肯定会发生,并且 CPU 已经放弃了它需要能够回滚到该存储指令之前的状态。所以一个充满分散缓存未命中存储的大型存储缓冲区可能会损害中断延迟。
在任何中断处理程序指令完全可以运行之前等待它耗尽,或者至少在 ISR 中的任何 in/out 或 locked 指令可能发生之前,如果事实证明存储缓冲区是'没有耗尽。)
相关:Sandpile (https://www.sandpile.org/x86/coherent.htm) 有一个正在序列化的事物表。中断和异常不是。但同样,这并不意味着它们不会耗尽存储缓冲区。这可以通过实验进行测试:在用户空间中的存储和 ISR 中的加载(不同共享变量的)之间寻找 StoreLoad 重新排序,正如另一个内核所观察到的那样。
这部分的一部分并不真正属于这个答案,应该移到其他地方。它在这里是因为在 What happens to expected memory semantics (such as read after write) when a thread is scheduled on a different CPU core? 上的 cmets 中的讨论将此作为可能错误声称的来源中断不会耗尽存储缓冲区,这是我在误解“不序列化”后写的。