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