内存屏障与InvalidQueue

内存屏障与InvalidQueue

内存屏障与InvalidQueue #

你先来看看下面这个代码:

// CPU0
void foo() {
    a = 1;
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    assert(a == 1);
}

假如,CPU0 和 CPU1 的缓存中都有变量 a 的副本,也就是说变量 a 所对应的缓存行在 CPU0 和 CPU1 中都是 Share 状态。CPU1 中没有变量 b 的副本,b 所对应缓存在 CPU0 中是 Exclusive 状态。

当 CPU0 在将变量 a 写入缓存的时候,会产生 Invalid 消息,这个消息到达 CPU1 以后,CPU1 不再立即处理它了,而是将这个消息放入 invalid queue,并且立即向 CPU0 回复了 invalid acknowledgement 消息。

CPU0 在得到这个确认消息以后,就可以独占该缓存了,直接将这块缓存变为 Modified 状态,然后把 a 写入。在 a 写入以后,foo 函数中的内存屏障就可以顺利通过了,接下来就可以写入变量 b 的新值。由于 b 是 Exclusive 的,它的更新比较简单,你可以自己思考一下。

接下来我们再看 CPU1 中的操作。

当 CPU1 发起对 b 的请求时,由于 b 不在缓存中,所以它会向总线发出 BusRd 请求,总线会把 CPU0 缓存中的 b 的新值 1 更新到 CPU1。同时,b 所在的缓存行在两个 CPU 中都变为 Share 状态。

CPU1 得到了 b 的新值以后,就可以退出第 10 行的 while 循环,然后对 a 的值进行判断。但是由于 a 的 Invalid 消息还在 invalid queue 里,没有被及时处理,CPU1 还是会使用自己的 Cache 中的 a 的原来的值,也就是 0,这就出错了。

你会发现,在这个过程中,虽然 CPU1 并没有乱序执行两条读指令,但是实际产生的效果却好像是先读到了 b 的值,后读到了 a 的值。如果是在严格遵守 MESI 协议的 CPU 中,CPU0 一定要确保 a 的值先更新到 CPU1,然后才能继续对 b 赋值。但是放宽了缓存一致性以后,这段代码就有问题了。

解决的方法和写屏障的思路是一样的,我们需要引入一个内存屏障,它会让 CPU 暂停执行,直到它处理完 invalid queue 中的失效消息之后,CPU 才会重新开始执行,例如:

// CPU0
void foo() {
    a = 1;
    smp_mb();
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    smp_mb();
    assert(a == 1);
}

你看,这样在 bar 函数里增加了内存屏障以后,我们就可以保证 a 的新值是一定能读到的了。可见 smp_mb 可以同时对 store buffer 和 invalid queue 施加影响。

Viewpoint #

From #

16 | 内存模型:有了MESI为什么还需要内存屏障?