我知道在汇编语言级别的指令集架构提供了比较和交换以及类似的操作。但是,我不明白芯片如何能够提供这些保证。
正如我想象的那样,指令的执行必须
- 从内存中获取一个值
- 比较值
- 根据比较,可能在内存中存储另一个值
是什么阻止了另一个内核在第一个内核获取它之后但在它设置新值之前访问内存地址?内存控制器是否管理这个?
编辑:如果 x86 实现是秘密的,我很高兴听到任何处理器系列是如何实现它的。
我知道在汇编语言级别的指令集架构提供了比较和交换以及类似的操作。但是,我不明白芯片如何能够提供这些保证。
正如我想象的那样,指令的执行必须
是什么阻止了另一个内核在第一个内核获取它之后但在它设置新值之前访问内存地址?内存控制器是否管理这个?
编辑:如果 x86 实现是秘密的,我很高兴听到任何处理器系列是如何实现它的。
这是software.intel.com 上的一篇文章,它对用户级别的锁几乎没有说明:
用户级锁涉及利用处理器的原子指令以原子方式更新内存空间。原子指令涉及在指令上使用锁定前缀并将目标操作数分配给内存地址。以下指令可以在当前 Intel 处理器上以锁定前缀原子运行:ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD 和XCHG. [...]在大多数指令中,必须明确使用锁定前缀,但 xchg 指令除外,如果指令涉及内存地址,则隐含锁定前缀。
在 Intel 486 处理器时代,锁定前缀用于断言总线上的锁定,同时性能受到很大影响。从 Intel Pentium Pro 架构开始,总线锁被转换为缓存锁。如果锁驻留在不可缓存的内存中,或者如果锁超出缓存线边界分割缓存线,那么在最现代的架构中,仍然会在总线上声明锁。这两种情况都不太可能发生,因此大多数锁前缀将被转换为成本更低的缓存锁。
那么是什么阻止了另一个内核访问内存地址呢?缓存一致性协议已经管理了缓存行的访问权限。因此,如果一个核心具有(临时)对高速缓存行的独占访问权限,则没有其他核心可以访问该高速缓存行。要访问该缓存行,另一个核心必须首先获得访问权限,并且获得这些权限的协议涉及当前所有者。实际上,缓存一致性协议会阻止其他内核静默访问缓存行。
如果锁定的访问没有绑定到单个缓存行,事情就会变得更加复杂。有各种令人讨厌的极端情况,例如页面边界上的锁定访问等。英特尔没有透露细节,他们可能使用各种技巧来加快锁定速度。
缓存一致性协议本身不足以实现原子操作。假设您要实现原子增量。以下是涉及的步骤
因此,为了以原子方式实现上述 3 条指令,我们应该首先获得对包含所需值的缓存行的独占访问权。一旦我们获得了独占访问权,在“存储”操作完成之前,我们不应该放弃对该缓存行的独占访问权。这意味着执行原子指令的 CPU 同时不应响应此高速缓存行的任何高速缓存一致性协议消息。虽然魔鬼在细节上是如何实现的,但至少它给了我们一个心智模型
下面是 linus torvalds 提到的关于原子指令的内容
原子指令绕过存储缓冲区,或者至少它们的行为就像它们一样——它们可能实际上使用了存储缓冲区,但是它们在加载之前刷新它和指令管道,并等待它在之后耗尽,并在高速缓存行上锁定它们作为负载的一部分,并作为存储的一部分释放——所有这些都是为了确保缓存线不会在两者之间消失,并且在此过程中没有其他人可以看到存储缓冲区的内容。
内存控制器仅负责确保不同处理器上的内存和缓存保持一致 - 如果您在 CPU1 上写入内存,CPU2 将无法从其缓存中读取其他内容。确保他们都试图操纵相同的数据不是它的责任。有一些低级指令使用了锁定和原子操作。它们在操作系统级别用于操作小块内存以创建诸如互斥锁和信号量之类的东西,这些实际上是一两个字节的内存,需要对其执行原子的同步操作。然后应用程序在此之上构建以对更大的数据结构和资源执行操作。