设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Linux内核的内存屏障

2013-7-3 14:02| 发布者: 红黑魂| 查看: 2851| 评论: 0|来自: ifeve

摘要: 原文链接作者:David Howells、Paul E. McKenney译者:曹姚君校对:丁一内容:抽象的内存访问模型设备操作保障什么是内存屏障?内存屏障的种类什么是内存屏障不能确保的?数据依赖屏障控制依赖SMP屏障配对内存屏障顺 ...

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内核有多种不同的屏障,工作在不同的层上:

  • 编译器屏障。
  • CPU内存屏障。
  • MMIO write屏障。

编译器屏障

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内核有很多锁结构:

  • 自旋锁
  • R / W自旋锁
  • 互斥
  • 信号量
  • R / W信号量
  • RCU

所有的情况下,它们都是LOCK操作和UNLOCK操作的变种。这些操作都隐含着特定的屏障:

  1. LOCK操作的含义:
  2. 在LOCK操作之后的内存操作将会在LOCK操作结束之后完成;

    在LOCK操作之前的内存操作可能在LOCK操作结束之后完成;

  3. UNLOCK操作的含义:
  4. 在UNLOCK操作之前的内存操作将会在UNLOCK操作结束之前完成;

    在UNLOCK操作之后的内存操作可能在UNLOCK操作结束之前完成;

  5. LOCK与LOCK的含义:
  6. 在一个LOCK之前的其它LOCK操作一定在该LOCK结束之前完成;

  7. LOCK与UNLOCK的含义:
  8. 在某个UNLOCK之前的所有其它LOCK操作一定在该UNLOCK结束之前完成;

    在某个LOCK之前的所有其它UNLOCK操作一定在该LOCK结束之前完成;

  9. 失败的有条件锁的含义:
  10. 某些锁操作的变种可能会失败,要么是由于无法立即获得锁,要么是在休眠等待锁可用的同时收到了一个解除阻塞的信号。失败的锁操作并不暗含任何形式的屏障。

因此,根据(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);

其它函数

其它暗含内存屏障的函数:

  • schedule()以及类似函数暗含了完整内存屏障。

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] 之后



酷毙
1

雷人

鲜花

鸡蛋
1

漂亮

刚表态过的朋友 (2 人)

  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部