read内存屏障与load预加载许多CPU都会预测并提前加载:即,当系统发现它即将需要从内存中加载一个条目时,系统会寻找没有其它load指令占用总线资源的时候提前加载 —— 即使还没有达到指令执行流中的该点。这使得实际的load指令可能会立即完成,因为CPU已经获得了值。 也可能CPU根本不会使用这个值,因为执行到了另外的分支而绕开了这个load – 在这种情况下,它可以丢弃该值或仅是缓存该值供以后使用。 考虑下面的场景: CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE } Divide instructions generally DIVIDE } take a long time to perform LOAD A 可能出现: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : : DIVIDE| | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : : DIVIDE| | : : ~ | | Once the divisions are complete --> : : ~-->| | the CPU can then perform the : : | | LOAD with immediate effect : : +-------+ 在第二个LOAD指令之前,放置一个read屏障或数据依赖屏障: CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE DIVIDE <read barrier> LOAD A 是否强制重新获取预取的值,在一定程度上依赖于使用的屏障类型。如果值没有发送变化,将直接使用预取的值: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : : DIVIDE| | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : : DIVIDE| | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrr~ | | : : ~ | | : : ~-->| | : : | | : : +-------+ 但如果另一个CPU有更新该值或者使该值失效,就必须重新加载该值: : : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : : DIVIDE| | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : : DIVIDE| | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrrr | | +-------+ | | The speculation is discarded ---> --->| A->1 |------>| | and an updated value is +-------+ | | retrieved : : +-------+ 传递性传递性是有关顺序的一个非常直观的概念,但是真实的计算机系统往往并不保证。下面的例子演示传递性(也可称为“积累律(cumulativity)”): CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <general barrier> <general barrier> LOAD Y LOAD X 假设CPU 2 的load X返回1、load Y返回0。这表明,从某种意义上来说,CPU 2的LOAD X在CPU 1 store X之后,CPU 2的load y在CPU 3的store y 之前。问题是“CPU 3的 load X是否可能返回0?” 因为,从某种意义上说,CPU 2的load X在CPU 1的store之后,我们很自然地希望CPU 3的load X必须返回1。这就是传递性的一个例子:如果在CPU B上执行了一个load指令,随后CPU A 又对相同位置进行了load操作,那么,CPU A load的值要么和CPU B load的值相同,要么是个更新的值。 在Linux内核中,使用通用内存屏障能保证传递性。因此,在上面的例子中,如果从CPU 2的load X指令返回1,且其load Y返回0,那么CPU 3的load X也必须返回1。 但是,read或write屏障不保证传递性。例如,将上述例子中的通用屏障改为read屏障,如下所示: CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <read barrier> <general barrier> LOAD Y LOAD X 这就破坏了传递性:在本例中,CPU 2的load X返回1,load Y返回0,但是CPU 3的load X返回0是完全合法的。 关键点在于,虽然CPU 2的read屏障保证了CPU2上的load指令的顺序,但它并不能保证CPU 1上的store顺序。因此,如果这个例子运行所在的CPU 1和2共享了存储缓冲区或某一级缓存,CPU 2可能会提前获得到CPU 1写入的值。因此,需要通用屏障来确保所有的CPU都遵守CPU1和CPU2的访问组合顺序。 要重申的是,如果你的代码需要传递性,请使用通用屏障。 显式内核屏障Linux内核有多种不同的屏障,工作在不同的层上:
编译器屏障Linux内核有一个显式的编译器屏障函数,用于防止编译器将内存访问从屏障的一侧移动到另一侧: barrier(); 这是一个通用屏障 – 不存在弱类型的编译屏障。 编译屏障并不直接影响CPU,CPU依然可以按照它所希望的顺序进行重排序。 CPU内存屏障Linux内核有8个基本的CPU内存屏障: TYPE MANDATORY SMP CONDITIONAL =============== ======================= =========================== GENERAL mb() smp_mb() WRITE wmb() smp_wmb() READ rmb() smp_rmb() DATA DEPENDENCY read_barrier_depends() smp_read_barrier_depends() 除了数据依赖屏障之外,其它所有屏障都包含了编译器屏障的功能。数据依赖屏障不强加任何额外的编译顺序。 旁白:在数据依赖的情况下,可能希望编译器以正确的顺序发出load指令(如:’a[b]‘,将会在load a[b]之前load b),但在C规范下并不能保证如此,编译器可能不会预先推测b的值(即,等于1),然后在load b之前先load a(即,tmp = a [1];if(b!= 1)tmp = a[b];)。还有编译器重排序的问题,编译器load a[b]之后重新load b,这样,b就拥有比a[b]更新的副本。关于这些问题尚未形成共识,然而ACCESS_ONCE宏是解决这个问题很好的开始。 在单处理器编译系统中,SMP内存屏障将退化为编译屏障,因为它假定CPU可以保证自身的一致性,并且可以正确的处理重叠访问。 [!]注意:SMP内存屏障必须用在SMP系统中来控制引用共享内存的顺序,使用锁也可以满足需求。 强制性屏障不应该被用来控制SMP,因为强制屏障在UP系统中会产生过多不必要的开销。但是,它们可以用于控制在通过松散内存I / O窗口访问的MMIO操作。即使在非SMP系统中,这些也是必须的,因为它们可以禁止编译器和CPU的重排从而影响内存操作的顺序。 下面是些更高级的屏障函数: (*) set_mb(var, value) 这个函数将值赋给变量,然后在其后插入一个完整的内存屏障,根据不同的实现。在UP编译器中,不能保证插入编译器屏障之外的屏障。 (*) smp_mb__before_atomic_dec(); (*) smp_mb__after_atomic_dec(); (*) smp_mb__before_atomic_inc(); (*) smp_mb__after_atomic_inc(); 这些都是用于原子加,减,递增和递减而不用返回值的,主要用于引用计数。这些函数并不包含内存屏障。 例如,考虑下面的代码片段,它标记死亡的对象, 然后将该对象的引用计数减1: obj->dead = 1; smp_mb__before_atomic_dec(); atomic_dec(&obj->ref_count); 这可以确保设置对象的死亡标记是在引用计数递减之前; 更多信息参见Documentation/atomic_ops.txt ,“Atomic operations” 章节介绍了它的使用场景。 (*) smp_mb__before_clear_bit(void); (*) smp_mb__after_clear_bit(void); 这些类似于用于原子自增,自减的屏障。他们典型的应用场景是按位解锁操作,必须注意,因为这里也没有隐式的内存屏障。 考虑通过清除一个lock位来实现解锁操作。 clear_bit()函数将需要像下面这样使用内存屏障: smp_mb__before_clear_bit(); clear_bit( ... ); 这可以防止在clear之前的内存操作跑到clear后面。UNLOCK的参考实现见”锁的功能”小节。 更多信息见Documentation/atomic_ops.txt , “Atomic operations“章节有关于使用场景的介绍; MMIO write屏障对于内存映射I / O写操作,Linux内核也有个特殊的障碍; mmiowb(); 这是一个强制性写屏障的变体,保证对弱序I / O区的写操作有偏序关系。其影响可能超越CPU和硬件之间的接口,且能实际地在一定程度上影响到硬件。 更多信息参见”锁与I / O访问”章节。 隐式内核内存屏障Linux内核中的一些其它的功能暗含着内存屏障,主要是锁和调度功能。 该规范是一个最低限度的保证,任何特定的体系结构都可能提供更多的保证,但是在特定体系结构之外不能依赖它们。 锁功能Linux内核有很多锁结构:
所有的情况下,它们都是LOCK操作和UNLOCK操作的变种。这些操作都隐含着特定的屏障:
在LOCK操作之后的内存操作将会在LOCK操作结束之后完成; 在LOCK操作之前的内存操作可能在LOCK操作结束之后完成; 在UNLOCK操作之前的内存操作将会在UNLOCK操作结束之前完成; 在UNLOCK操作之后的内存操作可能在UNLOCK操作结束之前完成; 在一个LOCK之前的其它LOCK操作一定在该LOCK结束之前完成; 在某个UNLOCK之前的所有其它LOCK操作一定在该UNLOCK结束之前完成; 在某个LOCK之前的所有其它UNLOCK操作一定在该LOCK结束之前完成; 某些锁操作的变种可能会失败,要么是由于无法立即获得锁,要么是在休眠等待锁可用的同时收到了一个解除阻塞的信号。失败的锁操作并不暗含任何形式的屏障。 因此,根据(1),(2)和(4),一个无条件的LOCK后面跟着一个UNLOCK操作相当于一个完整的屏障,但一个UNLOCK后面跟着一个LOCK却不是。 [!]注意:将LOCK和UNLOCK作为单向屏障的一个结果是,临界区外的指令可能会移到临界区里。 LOCK后跟着一个UNLOCK并不认为是一个完整的屏障,因为存在LOCK之前的存取发生在LOCK之后,UNLOCK之后的存取在UNLOCK之前发生的可能性,这样,两个存取操作的顺序就可能颠倒: *A = a; LOCK UNLOCK *B = b; 可能会发生: LOCK, STORE *B, STORE *A, UNLOCK 锁和信号量在UP编译系统中不保证任何顺序,所以在这种情况下根本不能考虑为屏障 —— 尤其是对于I / O访问 —— 除非结合中断禁用操作。 更多信息请参阅”CPU之间的锁屏障”章节。 考虑下面的例子: *A = a; *B = b; LOCK *C = c; *D = d; UNLOCK *E = e; *F = f; 以下的顺序是可以接受的: LOCK, {*F,*A}, *E, {*C,*D}, *B, UNLOCK [+] Note that {*F,*A} indicates a combined access. 但下列情形的,是不能接受的: {*F,*A}, *B, LOCK, *C, *D, UNLOCK, *E *A, *B, *C, LOCK, *D, UNLOCK, *E, *F *A, *B, LOCK, *C, UNLOCK, *D, *E, *F *B, LOCK, *C, *D, UNLOCK, {*F,*A}, *E 中断禁用功能禁止中断(等价于LOCK)和允许中断(等价于UNLOCK)仅可充当编译屏障。所以,如果某些场景下需要内存或I / O屏障,必须通过其它的手段来提供。 休眠和唤醒功能一个全局数据标记的事件上的休眠和唤醒,可以被看作是两块数据之间的交互:正在等待的任务的状态和标记这个事件的全局数据。为了确保正确的顺序,进入休眠的原语和唤醒的原语都暗含了某些屏障。 首先,通常一个休眠任务执行类似如下的事件序列: for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); } set_current_state()会在改变任务状态后自动插入一个通用内存屏障; CPU 1 =============================== set_current_state(); set_mb(); STORE current->state <general barrier> LOAD event_indicated set_current_state()可能包含在下面的函数中: prepare_to_wait(); prepare_to_wait_exclusive(); 因此,在设置状态后,这些函数也暗含了一个通用内存屏障。上面的各个函数又被封装在其它函数中,所有这些函数都在对应的地方插入了内存屏障; wait_event(); wait_event_interruptible(); wait_event_interruptible_exclusive(); wait_event_interruptible_timeout(); wait_event_killable(); wait_event_timeout(); wait_on_bit(); wait_on_bit_lock(); 其次,执行正常唤醒的代码如下: event_indicated = 1; wake_up(&event_wait_queue); 或: event_indicated = 1; wake_up_process(event_daemon); 类似wake_up()的函数都暗含一个内存屏障。当且仅当他们唤醒某个任务的时候。任务状态被清除之前内存屏障执行,也即是在设置唤醒标志事件的store操作和设置TASK_RUNNING的store操作之间: CPU 1 CPU 2 =============================== =============================== set_current_state(); STORE event_indicated set_mb(); wake_up(); STORE current->state <write barrier> <general barrier> STORE current->state LOAD event_indicated 可用唤醒函数包括: complete(); wake_up(); wake_up_all(); wake_up_bit(); wake_up_interruptible(); wake_up_interruptible_all(); wake_up_interruptible_nr(); wake_up_interruptible_poll(); wake_up_interruptible_sync(); wake_up_interruptible_sync_poll(); wake_up_locked(); wake_up_locked_poll(); wake_up_nr(); wake_up_poll(); wake_up_process(); [!]注意:在休眠任务执行set_current_state()之后,若要load唤醒前store指令存储的值,休眠和唤醒所暗含的内存屏障都不能保证唤醒前多个store指令的顺序。例如:休眠函数如下 set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) break; __set_current_state(TASK_RUNNING); do_something(my_data); 以及唤醒函数如下: my_data = value; event_indicated = 1; wake_up(&event_wait_queue); 并不能保证休眠函数在对my_data做过修改之后能够感知到event_indicated的变化。在这种情况下,两侧的代码必须在隔离数据访问之间插入自己的内存屏障。因此,上面的休眠任务应该这样: set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) { smp_rmb(); do_something(my_data); } 以及唤醒者应该做的: my_data = value; smp_wmb(); event_indicated = 1; wake_up(&event_wait_queue); 其它函数其它暗含内存屏障的函数:
CPU之间的锁屏障效应在SMP系统中,锁原语提供了更加丰富的屏障类型:在任意特定的锁冲突的情况下,会影响其它CPU上的内存访问顺序。 锁与内存访问考虑下面的场景:系统有一对自旋锁(M)、(Q)和三个CPU,然后发生以下的事件序列: CPU 1 CPU 2 =============================== =============================== *A = a; *E = e; LOCK M LOCK Q *B = b; *F = f; *C = c; *G = g; UNLOCK M UNLOCK Q *D = d; *H = h; 对CPU 3来说, *A到*H的存取顺序是没有保证的,不同于单独的锁在单独的CPU上的作用。例如,它可能感知的顺序如下: *E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M 但它不会看到任何下面的场景: *B, *C or *D 在 LOCK M 之前 *A, *B or *C 在 UNLOCK M 之后 *F, *G or *H 在 LOCK Q 之前 *E, *F or *G 在 UNLOCK Q 之后 但是,如果发生以下情况: CPU 1 CPU 2 =============================== =============================== *A = a; LOCK M [1] *B = b; *C = c; UNLOCK M [1] *D = d; *E = e; LOCK M [2] *F = f; *G = g; UNLOCK M [2] *H = h; CPU 3可能会看到: *E, LOCK M [1], *C, *B, *A, UNLOCK M [1], LOCK M [2], *H, *F, *G, UNLOCK M [2], *D 但是,假设CPU 1先得到锁,CPU 3将不会看到任何下面的场景: *B, *C, *D, *F, *G or *H 在 LOCK M [1] 之前 *A, *B or *C 在 UNLOCK M [1] 之后 *F, *G or *H 在 LOCK M [2] 之前 *A, *B, *C, *E, *F or *G 在 UNLOCK M [2] 之后 |