深入理解原子操作-底层基础

目录

上锁的原子操作(LOCKED ATOMIC OPERATIONS)

1. 可靠原子锁

2. 总线锁

2.1 自动锁

2.2 软件控制的总线锁

3. 处理自操作和交叉修改代码(Handling Self- and Cross-Modifying Code)

4. 处理器内部缓存上LOCK操作的影响


       

我们在开发高并发系统时,必定会用到大量的原子操作,而要真正理解原子操作的本质,仅仅学习高级语言提供的原子操作函数是不够的,为了能够从本质上理解原子操作,还必须深入到处理器设计层面来看看原子操作是怎么实现的,各有什么特点,从而能够更好的利用原子操作。

为什么要用到原子操作,就必须从多处理器管理说起,看看Intel处理器是如何来处理原子操作的。(相关内容参见Intel官方文档)

Intel 64 和IA-32架构提供了管理多处理器连接到同一系统总线的机制,并提升了多处理器连接同一系统总线的性能。这些机制和提升方法包括:

  • 提供提高系统内存上原子操作(atomic operations)性能的总线锁(bus locking)和/或缓存一致性(cache coherency)的管理方法。
  • 串行化指令(Serializing instructions)。
  • 有一个位于处理器芯片内的高级程序中断控制器(An advance programmable interrupt controller),简称APIC。
  • 一个二级缓存((level 2, L2)。对于Pentium 4, Intel Xeon, 和P6家族的处理器,L2包括在处理器包中,与处理器紧密相连。对于Pentium 和Intel486处理器而言,处理器提供了引脚来支持外部L2缓存。
  • 一个三级缓存(level 3,L3)。对于Intel Xeon 处理器,三级缓存位于处理器包中,与处理器紧密相连。
  • Intel超线程技术(Hyper-Threading)。超线程技术使得Intel 64 和IA-32架构处理器单个处理器内核可以并发地运行2个或多个线程。

       这些特性对于对称多处理(symmetric-multiprocessing,SMP)特别有用,也可应用于Intel 64 和IA-32处理器,以及专用处理器访问共享总线。(专用处理器包括通信,图形,视频处理器)

这些多处理机制有以下特征:

  • 维护系统内存的一致性 — 当两个或多个处理器试图同时访问同一段系统内存地址的时候,必须有一些通讯机制或同存访问协议来保证数据的一致性,以及在某些情况下,要允许同处理器临时性地锁住一块内存地址
  • 维护缓存的一致性 — 当一个处理器访问另一个处理器缓存上的数据时,它必须获得准确的数据。如果它修改了数据,另一个处理器获得的必须也是修改过后的数据。
  • 允许可预测地写入内存的顺序 — 在某些场景下,外部可以观察到其写入和外部程序的写入是顺序一致的,这非常重要。
  • 在一组处理器之间分发中断处理信号 — 当多个处理器在一个系统中并行操作的时候,有一个中心来接收和分发中断信号到可用的处理器来提供中断服务,这种机制非常有用。
  • 通过探索当代操作系统和应用程序多线程和多个进程的特征,从而增强了系统的性能。

上锁的原子操作(LOCKED ATOMIC OPERATIONS)

32-bit IA-32处理器支持在系统内存上上锁的原子操作。这些原子操作最典型的就是用于共享数据结构的管理(例如,信号量(semaphores),段描述符(segment descriptors),系统段(system segments),或者是分而表(page tables),在这些情况,两个或多个处理器试图修改同一个域和标识)。处理器使用3个相互依赖的机制来执行上锁的原子操作:

  • 可靠原子操作(Guaranteed atomic operations),这里权且称之为可靠原子操作,其实都必须可靠,

这个操作是什么意思呢,从描述来看,是指由CPU来保证的原子操作。看看文档是如何定义可靠原子操作的:

such as reading or writing a byte in system memory are always guaranteed to be handled atomically. That is, once started, the processor guarantees that the operation will be completed before another processor or bus agent is allowed access to the memory location.

        某些操作始终由处理器自动执行原子操作,例如从内存地址读写一个字节,处理器自动保证访问期间其它处理器和地址总线代理不能访问这个内存地址。这就是可靠原子操作。

自动原子操作在处理器的内部缓存中执行,而无须插入总线锁。

  • 总线锁(Bus locking),使用LOCK#信号和LOCK指令前缀。
  • 缓存一致性协议(Cache coherency protocols)。缓存一致性协议可以确保缓存数据结构的原子操作,将它称为缓存锁吧。这个机制出现在Pentium 4, Intel Xeon, 和P6家族的处理器中。

说明:当有竞争条件的时候,应该由软件来保证访问资源的公平性,硬件没有机制来保证。

1. 可靠原子锁

Intel486及以上处理器保证以下操作自动执行原子操作:

  • 读写一个字节。
  • 读写按16位边界对齐的字。
  • 读写按32位边界对齐的双字。

Pentium及以上处理器除了以上操作保留和Intel486系列自动原子操作以外,还增加了以下自动执行原子操作的内容:

  • 读写按64位边界对齐的四字。
  • 通过适配32位数据总线访问一个未缓存的16位内存位置。

P6及以上处理器除了以上操作保留和Intel486系列自动原子操作以外,还增加了以下自动执行原子操作的内容:

  • 通过适配一条缓存行来访问未对齐的16位,32位和64位缓存的内存。

对于Intel处理器而言,下列处理器由于访问缓存内存被缓存行(cache line,缓存内存单元)和分页边界给拆分开了,所以不能保证自动原子操作:

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家族处理器提供了总线控制信号允许外部内存子系统来拆分访问原子操作。尽管允许这么做,但是由于非对齐的数据访问严重影响处理器的性能,应当尽量避免非对齐的访问操作

2. 总线锁

Intel 64 和IA-32架构处理器提供了一个LOCK#信号,在访问临界内存操作期间,处理器会自动插入这个LOCK#信号,从而锁住总线或类似总线的连接线。当插入这个信号以后,其它的处理器或者总线代理请求控制总线的信号将被阻塞。在LOCK被预先作为LOCK语义指定前缀后,软件可以指定其它场合的LOCK#语义,实际上就是说,如果操作时指定了LOCK主义,处理器会自动执行LOCK操作。

在Intel386, Intel486,以及Pentium 处理器中,显式的lock指令会引发LOCK#信号中断,应该由硬件设计者来决定那些处理器可以通过LOCK#信号来访问系统内存。

在P6及更新的处理器家族中,如果被访问的内存被缓存到处理器内部,则不会引起LOCK#信号中断。反之,锁仅用于处理器缓存。

2.1 自动锁

如下操作处理器会自动执行遵循LOCK语义,自动执行加锁操作:

  • 通过XCHG指定访问内存。
  • 设置TSS(Task-State Segment)描述符的B(Busy)标识 — 当系统切换任务时,处理器会测试并设置TSS中的B标识,以确保两个处理器不会同时切换到同一个任务。当处处理器测试和设置TSS的B标识时,遵循了LOCK语义,处理器自动执行了加锁操作。
  • 更新段描述符号(segment descriptors)—处理器在载入段描述符时,如果段描述符中的标识被清除了,处理器会设置这个标识,这个操作遵循了LOCK语义,以确保它在更新期间其它处理器不会更新这个标识。为了使得这个操作更加有效,负责更新段描述符的操作系统例程应该遵循以下步骤:
  • 用一个锁操作来修改访问权限字节,以表明段描述符当前不可得(is not present)(即我在使用,你不可得),并为这个类型域(fields)指定一个值,表明描述符正在更新。
  • 更新段描述符的相关域(fields)的值。(这个操作可能会有几个内存操作动作,因此不能用锁)
  • 用一个锁操作来修改访问权限字节,表示段描述符有效且可得(present)。

Intel386处理器总是更新段描述符中的访问标识,不管这个标识是否被清除。Pentium 4, Intel Xeon, P6 family, Pentium, 和Intel486 处理器仅在该标识未设置时才更新其值。

  • 更新页目录或页表条目(page table entries)时 — 当更新页目录和页表条件时,处理器使用锁定的循环来设置页目录和页表条件中的访问标识和“赃”标识。
  • 应答中断时(Acknowledging interrupts) — 一个中断请求后,中断控制器可能会使用数据总线来向处理器发送中断向量表。在中断控制器使用数据总线向处理器发送中断向量表期间,处理器遵循LOCK语义,以确保在发送期间其它数据不会进入数据总线。

2.2 软件控制的总线锁

       软件在使用下列指令修改内存的时候,可以通过在指令前面加上LOCK前缀来显式地使用LOCK语义。如果在下列指令以外的指令中,或者不是向内存中执行写入操作(也就是说,如果目录操作在寄存器中)的指令中加入LOCK前缀,将会引发一个非法操作码异常(invalid-opcode exception, #UG)。可以加LOCK前缀的指令如下:

  • 位测试和修改指令(BTS, BTR, BTC)。
  • 交换指令(XADD, CMPXCHG, and CMPXCHG8B)。
  • 自动假设有LOCK前缀的XCHG指令(即XCHG指令未加LOCK前缀,但是处理器认为和加了LOCK一样)。
  • 下列单操作算术和逻辑指令(INC, DEC, NOT, NEG)。
  • 下列双操作和逻辑指令(ADD, ADC, SUB, SBB, AND, OR, XOR)。

锁指令确保锁住的仅是由目标操作数定义的内存区域,但可能会被系统解析为锁住更新内存区域的锁(也就是说锁住的区域超出了目标操作数所定义的内存区域)。

       软件应该采用相当的编址和操作数长度来访问信号量(多处理器之间发送信号的共享内存)。例如,一个处理器使用一个字来访问信号量(这里应当是指一个处理器使用一个字来创建和操作这个共享信号量),那么其它处理器就不应当使用一个字节来访问信号量。

注意:不要使用WC(Write Combining,缓存的一种类型)来实现信号量。不要在一个含有实现信号量地址的缓存上执行非临时性的存储。

       总线锁的完整性不通内存区域对齐的影响。锁语义遵守完成整个操作所需要的总线周期数的要求。话虽如此,为了更好的发挥系统性能,还是推荐锁操作在操作对象的自然边界对齐上进行:

  • 8位访问适的任何边界对齐(加锁或不加锁)(指按字节访问,没有对齐要求)。
  • 锁定字访问按16位边界对齐。
  • 锁定双字访问按32位边界对齐。
  • 锁定四字访问按64位边界对齐。

锁操作对于其它所有内存和外部可见事务的操作都是原子性的。仅存取指令和页表操作可以越过锁指令

锁指令可以用于在一个处理器写数据,而在另一个处理器上读数据的数据同步。

    对于P6家族处理器而言,锁操作串行化所有批量载入和存取操作(也就是说一直等待它们到执行完成)。Pentium 4 和 Intel Xeon 处理器除了一个例外以外也是这个规则,这个例外是,引用弱内存序内存的载入操作可能不会串行化。锁操作不应该用于确保写数据像存取指令一样操作

注意:对于当前的Pentium 4, Intel Xeon, P6 family, Pentium,Intel486处理器版本,锁指定允许写数据像存取指令一样操作。Intel推荐这些有自修改代码(self-modifying code)要求的开发者用别的数据同步机制,下面会讲到。

3. 处理自操作和交叉修改代码(Handling Self- and Cross-Modifying Code)

  • 自修改代码(self-modifying code,处理器将数据作为代码写入当前正在执行的代码段的这种行为就移为自修改代码。IA-32处理器在执行自修改代码的时候,所表现出来的具体模式的行为取决于所修改的代码距离当前指令指针的头有多远。随着处理器微架构变得越来越复杂并开始出现在投机的在退出点之前执行代码,是否考虑那些代码应该在修改前还是修改后执行,已经变理模糊。为了写自修改代码而又要让它们与当前及以后的IA-32处理器兼容,使用下面的编码选择:

选择一:

将修改的数据作为代码存入代码段

跳转到新的代码或者立即数地址

执行新的代码

选择二:

将修改的数据作为代码存入代码段

执行串行化指令(例如CPUID)

执行新的代码

在Pentium或Intel486处理器上运行的程序对这些选择没有要求,P6及以后的处理器推荐使用兼容模式。自修改代码比起非自修改代码和普通代码,将会在一个更底层的性能层级上执行,性能层级的程度取决于修改代码的频率和具本的特征。

  • 交叉修改代码(cross-modifying code),一个处理器将第二个处理器的数据作为代码写入第二个处理器正在执行的代码段的这种行为,称为交叉修改代码。对于自修改代码,IA-32处理器在执行交叉修改代码码的时候,所表现出来的具体模式的行为取决于所修改的代码距离当前指令指针的头有多远。要写交叉修改代码,并确保与当前及以后的IA-32架构处理器保持兼容,必须实现下面的处理器同步算法:

修改代码的处理器的动作:

将Memory_Flag置为0

将数据作为代码写入段

将Memory_Flag置为1

执行代码的处理器动作:

     WHILE (Memory_Flag  ≠ 1)

Wait for code to update;

ELIHW;

Execute serializing instruction;

在Pentium或Intel486处理器上运行的程序对这些选择没有要求,Pentium 4, Intel Xeon, P6 家族, PentiumP6及以后的处理器推荐使用兼容模式。

自修改代码和交叉修改代码,将会在一个更底层的性能层级上执行,性能层级的程度取决于修改代码的频率和具体的特征。这种限制也适用于Intel 64位处理器。

4. 处理器内部缓存上LOCK操作的影响

    对于Intel486和Pentium处理器,在LOCK操作期间,LOCK#信号在地址总线上一直存在,即使lock访问的内存已经在处理器的缓存中。对于P6及以后的处理器家族而言,如果执行锁操作的处理器缓存完全存储有所锁定内存并回写的内容,处理器总线上的这个LOCK#信号就会失效,代之的是,它会修改内部修改存储内容,并允许缓存一致性机制来确保这个操作的原子性,这个操作就称为缓存锁(cache locking)。这个机制会阻止两个或多个已经缓存了那个内容的处理器同时修改那块内存区域。

综合以上,原子锁的基本类型有:可靠原子操作(处理器保证),总线锁(锁总线对内存访问),自操作与交叉操作的一套方法,缓存锁协议(cache locking)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值