现在的位置: 首页 > 综合 > 正文

Intel系统编程指南第八章——8.1 加锁的原子操作

2012年09月15日 ⁄ 综合 ⁄ 共 4696字 ⁄ 字号 评论关闭

32位的IA-32处理器对系统存储器中的位置支持加锁的原子操作。这些操作一般用于共享的数据结构(诸如信号量、段描述符、系统段和页表)。而对于这些共享的数据结构,可能会有两个或更多处理器试图同时修改它们的同一个域(译者注:即相当于C语言中结构体变量的一个数据成员)或标志。处理器使用三个相互依赖的机制来执行加锁的原子操作:

1、有保证的原子操作

2、锁总线,使用LOCK#信号以及LOCK指令前缀

3、确保原子操作可以在被cache的数据结构上执行的Cache一致性协议(Cache锁);该机制已经在奔腾4、P6家族以及Intel致强处理器中有所实现。

 

这些机制以下列方法相互依赖。某些基本的存储器事务(诸如在系统存储器中读写一个字节)总是能确保被自动处理的。也就是说,一旦启动,处理器就确保那个操作将在其它处理器或总线代理被允许访问该存储器位置之前被完成。处理器也支持对执行所选中的存储器操作(诸如在一个共享存储器区域中的读-修改-写操作)的锁总线。这些操作一般需要被自动处理,但在这种方式下不会被自动处理。(译者注:像对一个基于多核的多线程共享的变量进行读-修改-写操作时,我们要确保这个操作是原子的,即在操作过程中不可被打断,而其它线程要对此共享变量进行操作的话将会被总线阻塞。但这个不是由处理器自动处理的,而是需要我们手工使用原子操作指令进行操作。)由于频繁使用的存储器位置经常被cache到处理器的L1或L2 Cache中,因此原子操作可以经常在一个处理器的Cache内部被执行,而不需要断言总线锁。这里,处理器的Cache一致性协议保证了当原子操作在已被Cache的存储器位置上执行时,正要cache同一存储器位置的其它处理器将被适当地管理。

 

注:在有受竞争的锁访问的地方,软件可能需要实现算法来确保对资源的公平访问以防止锁饥饿。硬件不会提供资源,这样确保对参与代理的公平性。管理信号量和互斥锁功能的公平性是软件的责任。

 

处理上锁的原子操作的机制已经随着IA-32处理器的复杂度而演化。最近的IA-32处理器(比如奔腾4、Intel致强和P6家族处理器)以及Intel 64提供了一个比起早期的处理器更精炼的锁机制。这些机制将在以下小节中描述。

 

8.1.1 有保证的原子操作

 

Intel486处理器(以及更新的处理器)确保了以下基本存储器操作将总是被自动执行:

1、读或写一个字节

2、读或写一个16位边界对齐的字

3、读或写一个32位边界对齐的双子

奔腾处理器(以及更新的处理器)确保了以下额外的存储器操作将总是被自动执行:

1、读或写一个64位边界对齐的四字

2、对适应于一个32位数据总线的非被cache的存储器位置的16位访问

P6家族处理器(以及更新的处理器)确保了以下额外的存储器操作将总是被自动执行:

1、对适应于一个Cache行的被cache的存储器的非对齐的16位、32位以及64位访问

 

对可被cache的存储器的访问,并且此访问跨Cache行以及页边界而被分裂,不能受以下处理器确保是原子的:Intel酷睿2 Duo、Intel凌动、Intel酷睿Duo、奔腾M、奔腾4、Intel致强、P6家族、奔腾、以及Intel486处理器。Intel酷睿2 Duo、Intel凌动、Intel酷睿Duo、奔腾M、奔腾4、Intel致强以及P6家族处理器提供了允许外部存储器子系统使得分裂访问成为原子的总线控制信号;然而,非对齐的数据访问将严重影响处理器的性能并应该被避免。

 

像一条x87指令或一条SSE指令那种访问大于一个四字长度的数据的指令可以通过使用多次存储器访问来实现。如果这样的指令对存储器做存储操作,那么其中一些访问可以完成(写入到存储器),而另一些出于架构上的原因(比如,由于页表条目被标记为“不存在”)导致操作故障(fault)。在这种情况下,已完成访问的效果可以对软件可见,即使整条指令导致了一个故障。如果TLB失效已被延迟(见4.10.4.4小节),那么这样的页故障可能会发生,即使所有的访问都是对同一个页。

 

8.1.2 锁总线

 

Intel 64和IA-32处理器提供了一个LOCK#信号,在某些对临界的存储器操作期间被自动断言,以锁系统总线或等价的连接(译者注:比如处理器到特殊目的处理器之间的专用传输通道)。当此输出信号被断言时,来自其它处理器或要对总线进行控制的总线代理(译者注:比如DMA控制器)的请求将被阻塞。软件可以指定其它场合,比如在一条指令前加上LOCK前缀,那么这条指令将有LOCK语义。

在Intel386、Intel486以及奔腾处理器的情况下,显式加锁的指令将导致LOCK#信号的断言。硬件设计者要负责使LOCK#信号在系统硬件中可用,以在各个处理器之间控制存储器访问。

对于P6以及更新的处理器,如果正被访问的存储器区域在处理器内部被cache,那么LOCK#信号通常不会被断言;取而代之的是,锁仅应用于处理器的Cache(见8.1.4小节)。

 

8.1.2.1 自动锁

 

对处理器自动遵循LOCK语义的操作如下:

1、当执行一条引用存储器的XCHG指令时。

2、当设置一个TSS描述符的B(忙)标志时——当切换到一个任务时,处理器测试并设置TSS描述符的类型域中的忙标志。为了确保两个处理器不会同时切换到同一个任务,处理器在测试并设置此标志时
遵循LOCK语义。

3、当更新段描述符时——当加载一个段描述符时,处理器将设置段描述符中被访问的标志,如果该标志已被清零的话。在此操作期间,处理器遵循LOCK语义,这样描述符在它被更新时不会被另一个处理器修改。为了使这个动作有效,更新描述符的操作系统过程应该使用下列步骤:

——使用一个加锁的操作来修改访问权限字节,以暗示段描述符当前不可用,并为暗示那个描述符正在被更新的类型域指定一个值。

——更新段描述符的域。(这个操作可能需要几次存储器访问;因此,加锁操作不能被使用。)

——使用一个加锁的操作来修改访问权限字节,以暗示段描述符有效并当前可用。

4、Intel386处理器总是更新段描述符中的被访问标志,无论它是否被清零。奔腾4、Intel致强、P6家族、奔腾、以及Intel486处理器只有在它还没有被设置的情况下才会去更新。

5、当更新页目录和页表条目时——当更新页目录和页表条目时,处理器使用上锁的周期来设置页目录和页表条目中的被访问标志和脏标志。

6、应答中断——在一个中断请求后,一个中断控制器可以使用数据总线把此中断所对应的中断向量发送给处理器。处理器在这段时间内遵循LOCK语义以确保当中断向量被传送时没有其它数据出现在数据总线上。

 

8.1.2.2 软件控制的锁总线

 

要显式地强制LOCK语义,软件可以在以下描述的指令前放上LOCK前缀,当这些指令被用于修改一个存储器位置时。当LOCK前缀与任何其它指令一起使用时,或当没有对存储器进行写操作时(即,当目的操作数是在寄存器中时),一个无效操作码异常(#UD)会被生成。(译者注:LOCK前缀只能用于某些特定的指令,以下将会介绍;并且这些指令的目的操作数必须是存储器类型,否则该指令是无效的。)

1、位测试并修改指令(BTS,BTR,BTC)

2、交换指令(XADD,CMPXCHG,CMPXCHG8B)

3、LOCK前缀对XCHG指令自动假定

4、以下单操作数算术逻辑指令:INC、DEC、NOT和NEG。

5、以下双操作数算术逻辑指令:ADD、ADC、SUB、SBB、AND、OR和XOR。

 

一个加锁的指令能确保只锁由目的操作数定义的存储器区域,但也可能会被系统解释为能够针对更大的一块存储器区域的锁。

软件应该使用同一个地址以及相同的操作数长度来访问信号量(用于在多个处理器之间发送信号的共享存储器)。比如,如果一个处理器使用一个字的长度来访问一个信号量,那么其它处理器不应该使用一个字节的长度来访问此信号量。

 

注:不要使用WC存储器类型来实现信号量。不要对含有用于实现一个信号量的位置的一个Cache行执行非临时的存储。

 

一个总线锁的完整性并不受存储器域的对齐的影响。LOCK语义的遵从需要有足够多的周期来更新整个操作数。然而,推荐使用加锁访问能够在它们自然边界处对齐以获得更好的系统性能。

 

加锁操作就所有其它存储器操作以及所有外部可见事件而言是原子的。只有取指令和页表访问可以通过加锁指令。加锁指令可以用于同步被
一个处理器所写的,而被其它处理器读的数据。

 

 

8.1.3 处理自修改代码以及跨修改代码

 

一个处理器将数据写入到当前正在执行的代码段中以便将那数据作为代码来执行的行为被称作是自修改代码。IA-32处理器在执行自修改代码时表现出模型特定的行为,依赖于正被修改的代码在当前执行指针之前有多远。

由于处理器微架构变得越来越复杂并且开始在隐退点之前投机地执行代码(在P6以及更新的处理器中),对于代码应该执行的规则、前或后修改,变得模糊。为了写自修改代码并确保它与当前或未来IA-32架构兼容,要使用下列编码选项的其中之一:

(*选项1*)

将被修改的代码(作为数据)存储到代码段中;跳转到新的代码或一个中介位置;执行新的代码

 

(*选项2*)

将被修改的代码(作为数据)存储到代码段中;执行一条串行化指令(比如一条CPUID指令);执行新的代码

如果要在Intel486或奔腾处理器上运行程序,那么不需要使用这些选项中的一个。但为了确保与P6家族或更新的处理器兼容,推荐这么做。

 

自修改代码将以比非自修改代码或正常代码更低的性能等级执行。性能退化程度将依赖于修改的频率以及代码特定的特征。

 

一个处理器将数据写到另一个处理器的当前执行代码段,以让第二个处理器将那部分数据作为代码来执行的行为被称为跨修改代码。与自修改代码一样,当执行跨修改代码时,IA-32处理器展示了模型特定的行为,依赖于已被修改的代码距离执行处理器的当前执行指针之前有多远。

要写跨修改代码并确保它与当前的和未来的IA-32架构兼容,以下的处理器同步算法必须被实现:

(*修改处理器行为*)

Memory_Flag <- 0; (*将Memory_Flag设置为1以外的值*)

将被修改的代码(作为数据)存放到代码段;

Memory_Flag <- 1;

 

(*执行处理器的行为*)

WHILE(Memory_Flag != 1)

    等待代码更新;

ELIHW;

执行串行化指令; (*比如CPUID*)

开始执行被修改的代码;

 

(如果是在Intel486处理器上运行程序,那么上面的选项是不必要的。但是为了确保对更新处理器的兼容,这个是推荐使用的。)

跟自修改代码一样,跨修改代码将以比正常代码更低的性能等级执行,依赖于修改频率以及代码的特定特征。

 

自修改代码以及跨修改代码的限制也应用于Intel 64架构。

 

8.1.4 对内部处理器Cache的一个LOCK操作的效果

 

对于Intel486和奔腾处理器,在LOCK操作期间,LOCK#信号总是总线上被断言,即使正被锁的存储器区域在处理器中被cache。

对于P6和最近处理器家族,在一个LOCK操作期间,如果正被锁的存储器区域在处理器中被cache,而该处理器正在执行将LOCK操作作为写回存储器执行,并且完全包含在一条Cache行中,那么处理器将不会在总线上断言LOCK#信号。取而代之的是它将在内部修改存储器位置,并允许其Cache一致性机制,以确保该操作被自动执行。该操作被称为“Cache锁”。Cache一致性机制自动地防止两个或更多处理器在已cache了相同存储器区域的情况下,对那个区域同时修改数据。

抱歉!评论已关闭.