Intel 64/x86_64/IA-32/x86处理器 - 锁原子操作(2) - 总线*/缓存*
Bus Locking
Intel 64和IA-32处理器提供了LOCK#信号,在某些关键的访存操作时会自动地**assert这个信号,用于*系统总线或类似的链接。当这个输出信号被**时,就会阻塞来自于其他的处理器或总线代理的总线控制请求。在其他情况下,如果软件业务逻辑需要*总线,可以在相应的指令前放置一个LOCK指令前缀。
对于Intel386,Intel486,和Pentium处理器,显示地指定LOCK前缀,会使处理器**LOCK#信号。硬件设计人员有责任在设计硬件时,提供LOCK#信号的接收机制,用来控制处理器间的访存操作。
对于P6及其后代处理器,如果访存操作命中处理器的内部缓存,在LOCK#信号通常不会被**;而且,“加锁操作”只会发生在处理器的内部缓存中。
Automatic Locking
在以下操作中,处理器自动遵循“加锁的”的语义(即执行原子操作):
- 当执行XCHG指令时,有操作数引用存储器数据。
- 当设置TSS描述符中的B(busy)标志位时 – 当切换任务时,处理器会测试并设置(test and set)TSS描述符的TYPE域的B标志位。为了确保两个处理器不会同时切换到同一个任务,处理器在处理B标志位时,会遵循“加锁的”语义。
- 当更新段描述符时 – 当载入一个段描述符时,如果段描述符的A(accessed)标志位没有被设置,则处理器会将其置1。在这个操作中,处理器也遵循加锁语义,这样段描述符在被更新时,就不会被其他的处理器修改。为了让这个过程更加高效,用于更新描述符的操作系统程序应该遵守如下的步骤:
- 使用加锁的操作修改段描述符的访问权限字节,指示描述符不存在,在描述符的TYPE域设定一个值,指示描述符正在被更新。
- 更新段描述符的域(这个操作可能需要多次访存操作;因此,不能使用加锁的操作)
- 使用加锁的操作修改段描述符的访问权限字节,指示描述符有效而且可用。
注:参看下图,以数据段描述符为例。
- 不管段描述符的访问标志位是否被设置,Intel386处理器总是会更新这个标志位。但是在Intel486,Pentium,P6 family,Pentium 4,Intel Xeon处理器上,会先测试这个标志位,只有当没有设置时,才会将其置1。
- 当更新页目录和页表条目时,处理器使用加锁的操作来设置页目录和页表条目的访问标志位A与脏标志位D。
- 中断确认 – 在中断请求发出后,中断控制器可能使用数据总线给处理器发送中断向量。处理器使用加锁语义,确保在中断向量传输时,没有其他的数据会出现在数据总线上。
Software Controlled Bus Locking
要显式地指示加锁语义,软件可以使用LOCK指令前缀。不是所有的指令都可以加LOCK前缀。下列的指令在操作存储器操作数时,可以使用LOCK前缀。当在其他指令前或者下述的指令前但是没有向内存写入数据(即目标操作数是寄存器)时,使用LOCK前缀会导致处理器产生无效操作码异常(#UD)。
- 比特位测试与修改指令(BTS,BTR,和BTC)
- 交换指令(XADD,CMPXCHG,和CMPXCHG8B)
- 对XCHG指令,处理器自动假设LOCK前缀
- 单操作数算术与逻辑指令:INC,DEC,NOT,和NEG
- 双操作数算术与逻辑指令:ADD,ADC,SUB,SBB,AND,OR和XOR
对于加锁的指令,处理器提供的保证是目标操作数的内存区域一定是原子性操作;但是系统可能将这个区域解释为更大的范围(注:例如,一个缓存行)。
软降应该使用相同的地址与操作数长度来访问信号量(在多个处理器之间传递信号的共享内存空间)。例如,如果一个处理器使用单字长度访问信号量,其他的处理器就不应该使用字节长度访问信号量。
注意:不要使用WC类型的内存实现信号量。对用来实现信号量的缓存行,不要使用非时效存储操作。
总线*操作的完整性不会受到内存区域的对齐方式影响。LOCK语义会一直持续到这个个操作数更新完成。但是,非常推荐将LOCK的操作数放置于它们的自然对齐的地址上,主要是出于性能方面的考虑:
- 单字节访问可以是任何位置
- 两字节访问在16位边界上
- 双字访问在32位边界上
- 四字访问在64位边界上
相对于其他的访存操作与其他的外部可见事件,加锁的操作时原子性的。只有取指与页表访问可以跨越加锁的指令。因此,加锁的指令可以用于同步一个处理器的数据写入与另一个处理器的数据读取。
对于P6 family处理器,加锁的操作会串行化所有未完成的读存与写存操作(即等待所有的访存指令完成)。这个规则对于Pentium 4与Intel Xeon处理器也适用,只有一个例外:引用弱排序内存类型(例如WC类型的内存)的读操作不会被串行化。
加锁的指令不应该被用来保证写入的数据能被当作指令进行取指。
注意:当前版本的Pentium 4,Intel Xeon,P6 family,Pentium和Intel 486处理器允许写入的数据被当作指令读取。但是,Intel推荐想要使用自修改代码的开发者使用另一种同步机制达到这个目的。参看下节。
Handling Self- and Cross-Modifying Code
TODO
Effects of a Lock Operation on Internal Processor Caches
对于Intel486和Pentium处理器,在执行LOCK操作时,LOCK#信号总是被**,即使被加锁操作的内存区域已经位于在处理器的缓存中。
对于P6和更新的处理器,如果加锁的指令要操作的数据已经位于处理器的缓存中且完全包含在一个缓存行中,同时数据所在的内存区域属于回写式WB内存,处理器可能不会在总线上**LOCK#信号。相应的,处理器会修改缓存中的数据,同时使用缓存一致性机制来确保加锁操作的原子性。这个操作叫做缓存*(cache locking)。缓存一致性协议自动的阻止两个或多个处理器同时修改它们缓存的相同地址区域里的数据。