这是阅读这篇文章的一个笔记。该文从CPU乱序执行和缓存实现的角度来说明内存屏障是怎么回事。
CPU为了优化性能会同时执行多条指令,有可能后面的指令会先完成。CPU会保证从软件的角度来看,这个乱序执行和顺序执行的可观察结果是一样的,但这个仅仅限于单个CPU的情况。多个CPU的时候乱序执行有时不是程序想要的,而CPU没有相关的信息做判断,这时要软件通过内存屏障强制CPU做些特殊处理。
CPU(或者CPU核,下同)有自己的缓存(Cache)。但是也要让所有CPU看到一个一致的内存数据,各个CPU和内存控制器通过共享的总线运行一套协议(MESI协议及变种)来实现这点。
最主要的是当一个CPU要写数据的时候,发送Invalidate消息给所有的其他CPU,其他的CPU要回应InvalidateAck,当所有的InvalidateAck都收到的时候,就可以写数据到自己缓存。收到Invalidate的CPU要把这个数据从它自己的缓存中清除出去(如果有的话),这样当这个CPU要读取这个数据的时候,会发出Read消息,刚才写数据的CPU会响应最新的数据。
但是上面的协议有两个性能上的问题需要进行优化,而这两个优化导致需要内存屏障。
性能问题1 - 引入Store Buffer
写数据的CPU要等所有的InvalidateAck收到是需要等很久的,所以它先把数据暂时写到Store Buffer里面,然后继续执行后面的指令。在Store Buffer里面的数据别人都读不到,只有它自己能读。等收到所有InvalidateAck的时候再把Store Buffer里面的数据更新到自己缓存,此时别的CPU才能通过Read消息读到。
有了Store Buffer之后下面的程序会表现奇怪:
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 }
设想CPU1执行foo,CPU2执行bar。CPU1执行完成b=1的时候(InvalidateAck b都收到),a=1的执行结果还在Store Buffer中(有的InvalidateAck还没有收到)。而CPU2此时没有收到Invalidate a(或者收到Invalidate a,但还没有从缓存清除数据a),那么第10行的assert会fail。CPU2先观察到b==1,退出循环,但是此时第10行读取的a还是初始值0。
解决方法是在a=1和b=1之间插入写内存屏障smp_wmb,其作用就是等待内存屏障之前Store Buffer清空,就是说所有的InvalidateAck a要收到,然后把a的最新值写到缓存。而CPU2执行assert的时候必然已经发过InvalidateAck,再读取a时需要时发送Read请求,CPU1可以响应a的最新值1。
性能问题2 - 引入Invalidate Queue
CPU在收到Invalidate后,要把数据从缓存中清除出去这个操作很慢,从而发送InvalidateAck延迟,进而导致写操作变慢。解决方法是收到Invalidate之后立刻回应InvalidateAck,但是把Invalidate请求放在Invalidate Queue中慢慢处理。这时候上面的程序还有问题(尽管加了写内存屏障),尽管在执行第10行的时候,CPU2收到了Invalidate a,但是还放在队列中没有执行,读到的还是之前缓存的初始值0。解决方法是在第9行和第10行之间插入读内存屏障smp_rmb,其作用是执行所有Invalidate Queue中的请求,从而对于smp_rmb之后的读取时总是要发出Read请求,从而得到最新的数据。程序中执行第10行的时候,数据a已经从缓存清除出去了,再次发出Read时读取到最新值1。
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 }