Intel Locked Atomic Operations


前言

因为之前学习了Linux的原子操作实现原理,发现与处理器硬件有关,就查看了Intel手册相关章节。

一、简介

32 位 IA-32 处理器支持对系统内存中位置的锁定原子操作,这些操作通常用于管理共享数据结构(例如信号量、段描述符、系统段或页表),其中两个或多个处理器可能同时尝试修改相同的字段或标志。处理器使用三种相互依赖的机制来执行锁定的原子操作:
Guaranteed atomic operations.
总线锁定,使用 LOCK# 信号和 LOCK 指令前缀。
缓存一致性协议,确保可以对缓存的数据结构进行原子操作(缓存锁);

这些机制以下列方式相互依赖。 某些基本的内存事务(例如在系统内存中读取或写入字节)始终保证以原子方式处理。也就是说,一旦启动,处理器保证操作将在另一个处理器或总线代理被允许访问内存位置之前完成。处理器还支持总线锁定以执行选定的内存操作(例如内存共享区域中的读-修改-写操作),这些操作通常需要以原子方式处理,但不会以这种方式自动处理。由于经常使用的内存位置通常缓存在处理器的 L1 或 L2 缓存中,因此原子操作通常可以在处理器的缓存内执行,而无需声明总线锁。
在这里,处理器的缓存一致性协议确保缓存相同内存位置的其他处理器得到正确管理,同时在缓存内存位置上执行原子操作。

注意:在存在有争议的锁访问的情况下,软件可能需要实现确保公平访问资源的算法,以防止锁饥饿。 硬件不提供任何资源来保证参与代理的公平性。 管理信号量和独占锁定功能的公平性是软件的责任。

二、Guaranteed Atomic Operations

Intel486 处理器(以及之后的更新处理器)保证以下基本内存操作将始终以原子方式执行:
(1)读取或写入一个字节。
(2)读取或写入在 16 位边界上对齐的字。
(3)读取或写入在 32 位边界上对齐的双字。

Pentium 处理器(以及之后的更新处理器)保证以下额外的内存操作将始终以原子方式执行:
(1)读取或写入在 64 位边界上对齐的四字
(2)对适合 32 位数据总线的未缓存内存位置进行 16 位访问。

P6 系列处理器(以及之后的更新处理器)保证以下附加内存操作将始终以原子方式执行:
(1)对缓存行内的缓存内存进行不对齐的16、32和64位访问。

枚举对英特尔® AVX 支持的处理器(通过设置功能标志 CPUID.01H:ECX.AVX[bit 28])保证以下指令执行的 16 字节内存操作将始终以原子方式执行:
(1) MOVAPD, MOVAPS, and MOVDQA.
(2)VMOVAPD, VMOVAPS, and VMOVDQA when encoded with VEX.128.
(3)VMOVAPD, VMOVAPS, VMOVDQA32, and VMOVDQA64 when encoded with EVEX.128 and k0 (masking
disabled).
(请注意,这些指令要求其内存操作数的线性地址是 16 字节对齐的。)

Intel Core 2 Duo、Intel Atom、Intel Core Duo、Pentium M、Pentium 4、Intel Xeon、P6 系列、Pentium 和 Intel486 处理器不保证对跨缓存行和页面边界拆分的可缓存内存的访问是原子的 . Intel Core 2 Duo、Intel Atom、Intel Core Duo、Pentium M、Pentium 4、Intel Xeon 和 P6 系列处理器提供总线控制信号,允许外部存储器子系统使分离访问原子化; 但是,不对齐的数据访问会严重影响处理器的性能,应该避免。

三、Bus Locking

Intel 64 和 IA-32 处理器提供 LOCK# 信号,该信号在某些关键内存操作期间自动 asserted 以锁定系统总线或等效链接(equivalent link)。当该输出信号被断言时,来自其他处理器或总线代理的对总线控制的请求被阻止。软件可以指定在其他情况下,在LOCK语义之后将LOCK前缀添加到指令前面。
对于 Intel386、Intel486 和 Pentium 处理器,显式锁定指令将导致 LOCK# 信号的断言。 硬件设计人员有责任在系统硬件中提供 LOCK# 信号以控制处理器之间的内存访问。

3.1 Automatic Locking

处理器自动遵循LOCK语义的操作如下:
(1)当执行引用内存的XCHG指令时。
(2)设置 TSS 描述符的 B (busy)标志时 — 处理器在切换到任务时测试并在 TSS 描述符的类型字段中设置busy标志。 为了确保两个处理器不会同时切换到同一个任务,处理器在测试和设置此标志时遵循 LOCK 语义。
(3)更新段描述符时 — 加载段描述符时,如果该标志被清除,处理器将在段描述符中设置访问标志。 在此操作期间,处理器遵循 LOCK 语义,以便描述符在更新时不会被另一个处理器修改。 为了使此操作有效,更新描述符的操作系统过程应使用以下步骤:
— 使用锁定操作修改访问权限字节以指示段描述符不存在,并为指示描述符正在更新的类型字段指定值。
— 更新段描述符的字段。 (此操作可能需要多次内存访问;因此,不能使用锁定操作。)
— 使用锁定操作来修改访问权限字节以指示段描述符有效且存在。
(4)Intel386 处理器总是更新段描述符中的访问标志,无论是否清除。 Pentium 4、Intel Xeon、P6 系列、Pentium 和 Intel486 处理器仅在尚未设置此标志时更新。
(5)更新页目录和页表条目时 — 更新页目录和页表条目时,处理器使用锁定周期来设置页目录和页表条目中的已访问和脏标志。
(6)确认中断——在中断请求之后,中断控制器可以使用数据总线将中断向量发送到处理器。 处理器在此期间遵循 LOCK 语义,以确保在传输向量时没有其他数据出现在数据总线上。

3.2 Software Controlled Bus Locking

为了显式地强制 LOCK 语义,软件可以在使用 LOCK 前缀和以下指令来修改内存位置时使用它们。 当 LOCK 前缀与任何其他指令一起使用或未对内存进行写操作时(即,当目标操作数在寄存器中时),将生成无效操作码异常 (#UD)。
• 位测试和修改指令(BTS、BTR 和 BTC)。
• 交换指令(XADD、CMPXCHG 和 CMPXCHG8B)。
• XCHG 指令自动使用LOCK 前缀。
• 以下 single-operand 算术和逻辑指令:INC、DEC、NOT 和NEG。
• 以下 two-operand 算术和逻辑指令:ADD、ADC、SUB、SBB、AND、OR 和 XOR。

软件应该使用相同的地址和操作数长度来访问信号量(用于在多个处理器之间发出信号的共享内存)。 例如,如果一个处理器使用字访问来访问信号量,则其他处理器不应使用字节访问来访问信号量。

总线锁的完整性不受内存字段对齐的影响。 LOCK 语义遵循更新整个操作数所需的尽可能多的总线周期。 但是,建议锁定访问在其自然边界上对齐,以获得更好的系统性能:
• Any boundary for an 8-bit access (locked or otherwise).
• 16-bit boundary for locked word accesses.
• 32-bit boundary for locked doubleword accesses.
• 64-bit boundary for locked quadword accesses

相对于所有其他内存操作和所有外部可见事件,锁定操作是原子操作。 只有取指令和页表访问才能传递锁定指令。 锁定指令可用于同步一个处理器写入和另一个处理器读取的数据。

对于 P6 系列处理器,锁定操作会序列化所有未完成的加载和存储操作(即等待它们完成)。 此规则也适用于 Pentium 4 和 Intel Xeon 处理器,但有一个例外。 引用弱排序内存类型(如 WC 内存类型)的加载操作可能不会被序列化。

锁定指令不应用于确保写入的数据可以作为指令获取。

四、Handling Self- and Cross-Modifying Code

4.1 self-modifying code

处理器将数据写入当前正在执行的代码段,目的是将该数据作为代码执行,这种行为称为self-modifying code。IA-32处理器在执行 self-modified code 时,会表现出特定于模型的行为,这取决于代码在当前执行指针之前被修改的时间。

随着处理器微架构变得更加复杂,并开始在 retirement point之前推测性地执行代码(如在 P6 和更新的处理器系列中),关于应该执行哪些代码(修改前或修改后)的规则变得模糊。 要编写 self-modified code 并确保它与 IA-32 架构的当前和未来版本兼容,请使用以下编码选项之一:

* 选项1 *)
将修改后的代码(作为数据)存储到代码段中;
跳转到新代码或中间位置;
执行新代码;

(* 选项 2 *)
将修改后的代码(作为数据)存储到代码段中;
执行序列化指令; (* 例如,CPUID 指令 *)
执行新代码;

在Pentium或Intel486处理器上运行的程序不需要使用这些选项,但建议使用这些选项以确保与P6和最新的处理器系列兼容。

Self-modifying code 的执行性能低于 non-self-modifying or normal code 。性能下降的程度取决于修改的频率和代码的特定特征。

4.2 cross-modifying code

一个处理器将数据写入第二处理器当前正在执行的代码段,目的是让第二处理器将该数据作为代码执行,这种行为称为 cross-modifying code。与 self-modifying code 一样,IA-32处理器在执行 cross-modifying code 时会表现出特定于模型的行为,这取决于执行处理器当前执行指针之前的代码修改时间。

编写cross-modifying code并确保它与 IA-32 架构的当前和未来版本兼容,必须实现以下处理器同步算法:

(* 修改处理器的动作 *)
Memory_Flag := 0; (* 将 Memory_Flag 设置为 1 以外的值 *)
将修改后的代码(作为数据)存储到代码段中;
Memory_Flag := 1;

(* 执行处理器的动作 *)
WHILE (Memory_Flag ≠ 1)
等待代码更新;
ELIHW;
执行序列化指令; (* 例如,CPUID 指令 *)
开始执行修改后的代码;

(在Intel486处理器上运行的程序不需要使用此选项,但建议使用此选项以确保与Pentium 4、Intel Xeon、P6系列和Pentium处理器兼容。)

与 self-modifying code一样, cross-modifying code的执行性能将低于non-cross-modifying (normal) code,具体取决于修改频率和代码的具体特征。

对self-modifying code 和 cross-modifying code的限制同样适用于Intel 64架构。

五、Effects of a LOCK Operation on Internal Processor Caches

对于 Intel486 和 Pentium 处理器,LOCK# 信号在 LOCK 操作期间始终在总线上 asserted,即使被锁定的内存区域被缓存在处理器中。

对于 P6 和最新的处理器系列,如果在 LOCK 操作期间被锁定的内存区域作为 write-back 内存缓存在执行 LOCK 操作的处理器中并且完全包含在缓存行中,则处理器可能不会 assert 总线上的 LOCK# 信号。 相反,它将在内部修改内存位置并允许其缓存一致性机制以确保操作以原子方式执行。 此操作称为“缓存锁定”。 缓存一致性机制自动防止两个或多个缓存了同一内存区域的处理器同时修改该区域中的数据。

总结

Intel 手册 3 chapter 8.1 Locked Atomic Operations的翻译。

参考资料

Intel 手册 3

猜你喜欢

转载自blog.csdn.net/weixin_45030965/article/details/125709626