Author:Echo Chen(陈斌)
Email:[email protected]
Date:September 30th, 2014
来自一篇墙外的文章,要了解怎样使用memory barrier,最好的方法是明确它为什么存在。CPU硬件设计为了提高指令的运行速度,增设了两个缓冲区(store buffer, invalidate queue)。这个两个缓冲区能够避免CPU在某些情况下进行不必要的等待,从而提快速度,可是这两个缓冲区的存在也同一时候带来了新的问题。
要细致分析这个问题须要先了解cache的工作方式。
眼下CPU的cache的工作方式非常像软件编程所使用的hash表,书上说“N路组相联(N-way set associative)”,当中的“组”就是hash表的模值,即hash链的个数,而常说的“N路”,就是每一个链表的最大长度。链表的表项叫做 cache-line,是一段固定大小的内存块。读操作非常直接,不再赘述。假设某个CPU要写数据项,必须先将该数据项从其它CPU的cache中移出, 这个操作叫做invalidation。当invalidation结束,CPU就能够安全的改动数据了。假设数据项在该CPU的cache中,可是是仅仅 读的,这个过程叫做”write miss”。一旦CPU将数据从其它CPU的cache中移除,它就能够反复的读写该数据项了。假设此时其它CPU试图訪问这个数据项,将产生一 次”cache miss”,这是由于第一个CPU已经使数据项无效了。这样的类型的cache-miss叫做”communication miss”,由于产生这样的miss的数据项一般是做在CPU之间沟通之用,比方锁就是这样一种数据项。
为了保证在多处理器的环境下cache仍然一致,须要一种协议来防止数据不一致和丢失。眼下经常使用的协议是MESI协议。MESI是 Modified,Exclusive, Shared, Invalid这四种状态的首字母的组合。使用该协议的cache,会在每一个cache-line前加一个2位的tag,标示当前的状态。
modified状态:该cache-line包括改动过的数据,内存中的数据不会出如今其它CPU-cache中,此时该CPU的cache中包括的数据是最新的 exclusive状态:与modified类似,可是数据没有改动,表示内存中的数据是最新的。假设此时要从cache中剔除数据项,不须要将数据写回内存 shared状态:数据项可能在其它CPU中有反复,CPU必须在查询了其它CPU之后才干够向该cache-line写数据 invalid状态:表示该cache-line空 |
MESI使用消息传递的方式在上述几种状态之间切换,详细转换过程參见[1]。假设CPU使用共享BUS,以下的消息足够:
read: 包括要读取的CACHE-LINE的物理地址 read response: 包括READ请求的数据,要么由内存满足要么由cache满足 invalidate: 包括要invalidate的cache-line的物理地址,全部其它cache必须移除对应的数据项 invalidate ack: 回复消息 read invalidate: 包括要读取的cache-line的物理地址,同一时候使其它cache移除该数据。须要read response和invalidate ack消息 writeback:包括要写回的数据和地址,该状态将处于modified状态的lines写回内存,为其它数据腾出空间 |
引用[1]中的话:
Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.
尽管该协议能够保证数据的一致性,可是在某种情况下并不高效。举例来说,假设CPU0要更新一个处于CPU1-cache中的数据,那么它必须等待 cache-line从CPU1-cache传递到CPU0-cache,然后再运行写操作。cache之间的传递须要花费大量的时间,比运行一个简单的 操作寄存器的指令高出几个数量级。而其实,花费这个时间根本毫无意义,由于不论从CPU1-cache传递过来的数据是什么,CPU0都会覆盖它。为了 解决问题,硬件设计者引入了store buffer,该缓冲区位于CPU和cache之间,当进行写操作时,CPU直接将数据写入store buffer,而不再等待还有一个CPU的消息。可是这个设计会导致一个非常明显的错误情况。
试考虑例如以下代码:
1: a = 1;
2: b = a + 1;
3: assert(b == 2);
如果初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果依照以下流程运行这段代码:
1 CPU0运行a=1; 2 由于a在CPU1-cache中,所以CPU0发送一个read invalidate消息来占有数据 3 CPU0将a存入store buffer 4 CPU1接收到read invalidate消息,于是它传递cache-line,并从自己的cache中移出该cache-line 5 CPU0開始运行b=a+1; 6 CPU0接收到了CPU1传递来的cache-line,即“a=0” 7 CPU0从cache中读取a的值,即“0” 8 CPU0更新cache-line,将store buffer中的数据写入,即“a=1” 9 CPU0使用读取到的a的值“0”,运行加1操作,并将结果“1”写入b(b在CPU0-cache中,所以直接进行) 10 CPU0运行assert(b == 2); 失败 |
出现故障的原因是我们有两份”a”的拷贝,一份在cache-line中,一份在store buffer中。硬件设计师的解决的方法是“store forwarding”,当运行load操作时,会同一时候从cache和store buffer里读取。也就是说,当进行一次load操作,假设store-buffer里有该数据,则CPU会从store-buffer里直接取出数 据,而不经过cache。由于“store forwarding”是硬件实现,我们并不须要太关心。
另一中错误情况,考虑以下的代码:
1: void foo(void)
2: {
3: a = 1;
4: b = 1;
5: }
6:
7: void bar(void)
8: {
9: while (b == 0) continue;
10: assert(a == 1);
11: }
如果变量a在CPU1-cache中,b在CPU0-cache中。CPU0运行foo(),CPU1运行bar(),程序运行的顺序例如以下:
1 CPU0运行 a = 1; 由于a不在CPU0-cache中,所以CPU0将a的值放到store-buffer里,然后发送read invalidate消息 2 CPU1运行while(b == 0) continue; 可是由于b不再CPU1-cache中,所以它会发送一个read消息 3 CPU0运行 b = 1;由于b在CPU0-cache中,所以直接存储b的值到store-buffer中 4 CPU0收到 read 消息,于是它将更新过的b的cache-line传递给CPU1,并标记为shared 5 CPU1接收到包括b的cache-line,并安装到自己的cache中 6 CPU1如今能够继续运行while(b == 0) continue;了,由于b=1所以循环结束 7 CPU1运行assert(a == 1);由于a本来就在CPU1-cache中,并且值为0,所以断言为假 8 CPU1收到read invalidate消息,将并将包括a的cache-line传递给CPU0,然后标记cache-line为invalid。可是已经太晚了 |
就是说,可能出现这类情况,b已经赋值了,可是a还没有,所以出现了b = 1, a = 0的情况。对于这类问题,硬件设计者也爱莫能助,由于CPU无法知道变量之间的关联关系。所以硬件设计者提供了memory barrier指令,让软件来告诉CPU这类关系。解决方法是改动代码例如以下:
1: void foo(void)
2: {
3: a = 1;
4: smp_mb();
5: b = 1;
6: }
smp_mb()指令能够迫使CPU在进行兴许store操作前刷新store-buffer。以上面的程序为例,添加memory barrier之后,就能够保证在运行b=1的时候CPU0-store-buffer中的a已经刷新到cache中了,此时CPU1-cache中的a 必定已经标记为invalid。对于CPU1中运行的代码,则能够保证当b==0为假时,a已经不在CPU1-cache中,从而必须从CPU0- cache传递,得到新值“1”。详细过程见[1]。
上面的样例是使用memory barrier的一种环境,还有一种环境涉及到还有一个缓冲区,确切的说是一个队列——“Invalidate Queues”。
store buffer一般非常小,所以CPU运行几个store操作就会填满。这时候CPU必须等待invalidation ACK消息,来释放缓冲区空间——得到invalidation ACK消息的记录会同步到cache中,并从store buffer中移除。相同的情形发生在memory barrier运行以后,这时候全部兴许的store操作都必须等待invalidation完毕,不论这些操作是否导致cache-miss。解决的方法 非常easy,即使用“Invalidate Queues”将invalidate消息排队,然后立即返回invalidate ACK消息。只是这样的方法有问题。
考虑以下的情况:
1: void foo(void)
2: {
3: a = 1;
4: smp_mb();
5: b = 1;
6: }
7:
8: void bar(void)
9: {
10: while (b == 0) continue;
11: assert(a == 1);
12: }
a处于shared状态,b在CPU0-cache内。CPU0运行foo(),CPU1运行函数bar()。运行操作例如以下:
1 CPU0运行a=1。由于cache-line是shared状态,所以新值放到store-buffer里,并传递invalidate消息来通知CPU1 2 CPU1运行 while(b==0) continue;可是b不再CPU1-cache中,所以发送read消息 3 CPU1接受到CPU0的invalidate消息,将其排队,然后返回ACK消息 4 CPU0接收到来自CPU1的ACK消息,然后运行smp_mb(),将a从store-buffer移到cache-line中 5 CPU0运行b=1;由于已经包括了该cache-line,所以将b的新值写入cache-line 6 CPU0接收到了read消息,于是传递包括b新值的cache-line给CPU1,并标记为shared状态 7 CPU1接收到包括b的cache-line 8 CPU1继续运行while(b==0) continue;由于为假所以进行下一个语句 9 CPU1运行assert(a==1),由于a的旧值依旧在CPU1-cache中,断言失败 10 虽然断言失败了,可是CPU1还是处理了队列中的invalidate消息,并真的invalidate了包括a的cache-line,可是为时已晚 |
能够看出出现故障的原因是,当CPU排队某个invalidate消息后,在它还没有处理这个消息之前,就再次读取该消息相应的数据了,该数据此时本应该已经失效的。
解决方法是在bar()中也添加一个memory barrier:
1: void bar(void)
2: {
3: while (b == 0) continue;
4: smp_mb();
5: assert(a == 1);
6: }
此处smp_mb()的作用是处理“Invalidate Queues”中的消息,于是在运行assert(a==1)时,CPU1中的包括a的cache-line已经无效了,新的值要又一次从CPU0-cache中读取。
memory bariier还能够细分为“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb仅仅处理Invalidate Queues,wmb仅仅处理store buffer。
能够使用rmb和wmb重写上面的样例:
1: void foo(void)
2: {
3: a = 1;
4: smp_wmb();
5: b = 1;
6: }
7:
8: void bar(void)
9: {
10: while (b == 0) continue;
11: smp_rmb();
12: assert(a == 1);
13: }
最后提一下x86的mb。x86CPU会自己主动处理store顺序,所以smp_wmb()原语什么也不做,可是load有可能乱序,smp_rmb()和smp_mb()展开为lock;addl。
[1] http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
[2] http://en.wikipedia.org/wiki/Memory_barrier
[3] http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt
[4]http://sstompkins.wordpress.com/2011/04/12/why-memory-barrier%EF%BC%9F/
-
Echo Chen:Blog.csdn.net/chen19870707
-