TL;DR:这取决于架构和操作系统。在 x86 上,这种类型的 read-after-write 危险大多不是必须在软件级别考虑的问题,除了弱顺序 WC 存储,它需要在软件之前在同一逻辑内核上执行存储栅栏线程已迁移。
通常线程迁移操作包括至少一个内存存储。考虑具有以下属性的架构:
您提到的排序风险在这样的架构上可能是可能的,因为即使线程迁移操作完成,也不一定意味着线程执行的所有存储都是全局可观察的。在具有严格顺序存储排序的架构上,不会发生这种危险。
在完全假设的架构上,可以在不进行单个内存存储的情况下迁移线程(例如,通过直接将线程的上下文转移到另一个内核),即使所有存储在具有以下属性:
- 在商店退休和全球可观察到之间存在一个“漏洞窗口”。例如,由于存储缓冲区和/或 MSHR 的存在,可能会发生这种情况。大多数现代处理器都具有此属性。
因此,即使使用顺序存储排序,在新内核上运行的线程也可能看不到最后 N 个存储。
请注意,在按顺序退休的机器上,漏洞窗口是支持可能不按顺序存储的内存模型的必要条件但不充分条件。
通常使用以下两种方法之一重新调度线程以在不同的内核上运行:
- 发生硬件中断(例如定时器中断),最终导致线程在不同的逻辑内核上重新调度。
- 线程本身执行系统调用,例如
sched_setaffinity,最终导致它在不同的内核上运行。
问题是系统在什么时候保证退休商店成为全球可观察的?在 Intel 和 AMD x86 处理器上,硬件中断是完全序列化事件,因此所有用户模式存储(包括可缓存和不可缓存)都保证在执行中断处理程序之前是全局可观察的,其中线程可能被重新调度以运行不同的逻辑核心。
在 Intel 和 AMD x86 处理器上,有多种方法可以执行系统调用(即更改权限级别),包括 INT、SYSCALL、SYSENTER 和远 CALL。它们都不能保证所有以前的商店都可以在全球范围内观察到。因此,操作系统应该在通过执行存储围栏操作在不同内核上调度线程时显式执行此操作。这是将线程上下文(架构用户模式寄存器)保存到内存并将线程添加到与其他内核关联的队列中的一部分。这些操作涉及至少一个受顺序排序保证约束的商店。当调度程序在目标内核上运行时,它会看到该内核上可用的线程的完整寄存器和内存架构状态(在最后一条退出指令的点)。
在 x86 上,如果线程使用 WC 类型的存储,它不保证顺序排序,操作系统可能不保证在这种情况下它会使这些存储全局可观察。 x86 规范明确指出,为了使 WC 存储全局可观察,必须使用存储栅栏(在同一内核上的线程中,或者更简单地,在操作系统中)。正如@JohnDMcCalpin 的回答中所提到的,操作系统通常应该这样做。否则,如果操作系统不为软件线程提供程序顺序保证,那么用户模式程序员可能需要考虑到这一点。一种方法如下:
- 保存当前 CPU 掩码的副本并将线程固定到当前内核(或任何单个内核)。
- 执行弱排序的商店。
- 执行商店围栏。
- 恢复 CPU 掩码。
这会暂时禁用迁移,以确保存储栅栏与弱排序存储在同一核心上执行。执行完存储栅栏后,线程可以安全迁移,不会违反程序顺序。
请注意,用户模式的睡眠指令(例如 UMWAIT)不会导致线程在不同的内核上重新调度,因为在这种情况下操作系统不会进行控制。
Linux 内核中的线程迁移
@JohnDMcCalpin 的答案中的代码 sn-p 落在发送处理器间中断的路径上,这是使用 WRMSR 指令到 APIC 寄存器来实现的。可能出于多种原因发送 IPI。例如,执行 TLB 击落操作。在这种情况下,重要的是要确保更新的分页结构在使其他内核上的 TLB 条目无效之前是全局可观察的。这就是为什么可能需要x2apic_wrmsr_fence 的原因,它会在发送 IPI 之前被调用。
也就是说,我认为线程迁移不需要发送 IPI。本质上,通过将线程从与一个核心相关联的某个数据结构中移除并将其添加到与目标核心相关联的数据结构中来迁移线程。迁移线程可能有多种原因,例如亲缘关系发生变化或调度程序决定重新平衡负载时。正如Linux source code中提到的,源码中所有线程迁移的路径最终都会执行如下:
stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg)
其中arg 保存要迁移的任务和目标核心标识符。 migration_cpu_stop 是一个执行实际迁移的函数。但是,要迁移的任务可能当前正在运行或在某个运行队列中等待以在源核心(即当前调度任务的核心)上运行。在迁移之前需要停止任务。这是通过将函数migration_cpu_stop 的调用添加到与源内核关联的停止器任务的队列中来实现的。 stop_one_cpu 然后将停止器任务设置为准备执行。停止任务具有最高优先级。因此,在源内核(可能与当前内核相同)上的下一个定时器中断时,将选择运行具有最高优先级的任务之一。最终,stopper 任务将运行并执行migration_cpu_stop,然后执行迁移。由于此过程涉及硬件中断,因此保证目标任务的所有存储都是全局可观察的。
x2apic_wrmsr_fence 中似乎存在错误
x2apic_wrmsr_fence 的目的是在发送 IPI 之前使所有以前的存储全局可观察。正如this 线程中所讨论的,SFENCE 在这里是不够的。要了解原因,请考虑以下顺序:
store
sfence
wrmsr
这里的 store fence 可以命令前面的 store 操作,但不能命令 MSR write。在 x2APIC 模式下写入 APIC 寄存器时,WRMSR 指令没有任何序列化属性。这在英特尔 SDM 第 3 卷第 10.12.3 节中有所提及:
为了在 x2APIC 模式下高效访问 APIC 寄存器,
WRMSR 的序列化语义在写入
APIC 寄存器。
这里的问题是MFENCE 也不能保证相对于以前的商店订购后来的WRMSR。在 Intel 处理器上,它被记录为仅对内存操作进行排序。只有在 AMD 处理器上才能保证完全序列化。因此,要使其在 Intel 处理器上工作,在 MFENCE 之后需要有一个 LFENCE(SFENCE 不与 LFENCE 一起订购,因此即使我们不需要订购,也必须使用 MFENCE负载)。实际上第 10.12.3 节提到了这一点。