所有“正常”的ISA,包括8080和x86,在同一核心上保证指令在中断方面是原子性的。要么指令完全执行并且它的所有结构效果都可见(在中断处理程序中),要么没有任何效果。任何与此规则不符的情况通常会被仔细记录。
例如,
Intel的x86手册第3卷(大约1000页PDF) 特别指出以下内容:
“6.6 程序或任务重启
为了允许在处理异常或中断后重新启动程序或任务,在指令边界上保证所有异常(中止除外)都会报告异常。
所有中断都保证在指令边界上被执行。”
Intel的第1卷手册 中的一段旧文字谈到单核系统使用
cmpxchg
而不加
lock
前缀以原子方式读取修改写入(与其他软件而非硬件DMA访问相关)。
CMPXCHG指令通常用于测试和修改信号量。它会检查信号量是否空闲。如果信号量是空闲的,则标记为已分配;否则,获取当前所有者的ID。
这一切都是在一个不可中断的操作中完成的[因为它是单个指令]。在单处理器系统中,CMPXCHG指令消除了在执行多个指令以测试和修改信号量之前切换到保护级别0(禁用中断)的需要。
对于多处理器系统,可以将CMPXCHG与LOCK前缀组合使用以原子方式执行比较和交换操作。(有关原子操作的更多信息,请参见Intel® 64和IA-32体系结构软件开发人员手册第3A卷第8章“多处理器管理”中的“锁定原子操作”部分。)
(有关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设置为“起始值-已复制字节数”,如果此时RCX非零,则会将RIP指向该指令,因此在中断后恢复时,rep movsb将再次运行并完成其余的复制。
其他的x86示例包括SIMD gather loads (AVX2/AVX512)和scatter stores (AVX512)。例如,
vpgatherdd ymm0,[rdi + ymm1 * 4],ymm2
可以根据设置的
ymm2
元素进行多达8个32位加载,并将结果
合并到ymm0中。
在正常情况下(没有中断、页面故障或其他同步异常发生),您将在目标寄存器中获取数据,掩码寄存器最终为零。因此,掩码寄存器为CPU提供了一个存储进度的位置。
Gather和scatter都很慢,可能需要触发多个页面故障,因此对于同步异常,即使在处理页面故障取消所有其他页面的情况下,也可以保证前进。但更相关的是,这意味着避免重新执行TLB misses如果一个中间元素页面故障,并且不丢弃异步中断到达的工作。
一些长期运行的指令(如刷新
所有核心上的所有数据缓存的
wbinvd
)在架构上不可中断,甚至
微架构上也无法中止(以丢弃部分工作并处理中断)。它是特权级别的,因此用户空间无法执行它作为拒绝服务攻击来导致高中断延迟。
记录有趣行为的相关示例是当x86的
popad
从堆栈顶部(段限制)弹出。这是一个异常(不是外部中断),在vol.3手册的6.5节“异常分类”中有记录,即故障/陷阱/中止(有关详细信息,请参见PDF文件)。
注意:
通常报告为故障的一种异常子集是不可重新启动的。这些异常导致一些处理器状态的丢失。例如,在执行一个跨越堆栈段末端的
POPAD指令时会报告故障。在这种情况下,异常处理程序看到指令指针(CS:EIP)已恢复,就好像未执行POPAD指令一样。然而,内部处理器状态(通用寄存器)将被修改。这类情况被认为是编程错误。引发此类异常的应用程序应该由操作系统终止。
请注意,这仅在
popad
本身引起异常的情况下才会发生,而不是其他任何原因。外部中断无法像对
rep movsb
或
vpgatherdd
那样分割
popad
。
(我猜为了
popad
故障的目的,它实际上是迭代工作的,每次弹出1个寄存器并逻辑地修改RSP / ESP / SP以及目标寄存器。而不是在开始之前检查要加载的整个区域的段限制,因为那将需要额外的加法。)
乱序执行的CPU在中断时会回滚到退役状态。
像现代x86这样的乱序执行CPU,将复杂指令分割成多个uops,仍然确保了这一点。当中断到达时,CPU必须选择两个正在运行的指令之间的一个点作为中断结构上发生的位置。它必须放弃已经完成的解码或开始执行任何后续指令的工作。假设中断返回,它们将被重新获取并重新开始执行。
请参见当中断发生时,流水线中的指令会发生什么?。
正如Andy Glew所说,当前的CPU不会重命名特权级别,因此逻辑上发生的事情(中断/异常处理程序在较早的指令完成后执行)与实际发生的事情相匹配。
有趣的事实是:x86中断并不完全序列化,至少在文件上没有保证。(在x86术语中,像cpuid和iret这样的指令被定义为序列化;排空乱序后端和存储缓冲区以及任何可能重要的内容。这是一个非常强大的屏障,许多其他东西则不是,例如mfence。)
在实践中(因为CPU在实践中不会重命名特权级),当中断处理程序运行时,乱序后端中就不会有任何旧的用户空间指令/微操作仍在飞行。
异步(外部)中断也可能会清空存储缓冲区,这取决于我们如何解释
Intel's SDM vol.3 11.10中的措辞:“...在以下情况下,存储缓冲区的内容总是被清空到内存中:”...“当异常或中断被
生成”。显然,这适用于异常(其中CPU核心本身生成中断),并且在服务中断之前也可能意味着清空存储缓冲区。
(来自已
退役存储指令的存储数据不是推测性的;它肯定会发生,并且CPU已经放弃了需要能够回滚到该存储指令之前的状态。因此,一个充满散乱的缓存未命中存储的大型存储缓冲区可能会影响中断延迟。要么是等待它在任何中断处理程序指令可以运行之前完全清空,要么至少在ISR中的任何
in
/
out
或
lock
ed指令之前,如果存储缓冲区没有被清空的话,这些指令就无法执行。)
相关:Sandpile(
https://www.sandpile.org/x86/coherent.htm)有一个表格列出了正在串行化的事物。中断和异常不在其中。但是,这并不意味着它们不会排空存储缓冲区。可以通过实验来测试这一点:在用户空间中进行存储并在ISR中加载(另一个共享变量),观察另一个核心是否发现存储-加载重排序。
这一部分内容实际上不属于这个答案,应该移动到其他地方。这里是因为在
当线程在不同的CPU核心上调度时,预期的内存语义(如写入后读取)会发生什么?的评论讨论中,被引用作为错误的断言来源,即中断不会排空存储缓冲区,我在误解“不串行化”之后写下了这个结论。