;
b=1;
}
public void print(){
while(b==0)
;
assert a==1;
}
smb_mb()
就是内存屏障指令,英文memory barries。它的作用,是在后续的store动作之前,将sotre buffer中的内容刷新到cacheline。这个操作的效果是让本地的cacheline的操作顺序和代码的顺序一致,也就是让其他cpu观察到的该cpu的cacheline操作顺序被分为smp_mb()之前和之后。要达到这个目的有两种方式
- 遇到smp_mb()指令时,暂停cpu执行,将当前的store_buffer全部刷新到cacheline中,完成后cpu继续执行
- 遇到smp_mb()指令时,cpu继续执行,但是所有后续的store操作都进入到了store buffer中,直到store buffer之前的内容都被刷新到cacheline,即使此时需要store的内容的cacheline是M或者E状态,也只能先写入store buffer中。这样的策略,既可以提升cpu效率,也保证了正确性。当之前store buffer的内容被刷新到cacheline完成后,后面新增加的内容也会有合适的时机刷新到cacheline。把store buffer想象成一个FIFO的队列就可以了。
下面来看,当有了smp_mb()
之后,程序的执行情况。所有的初始假设与上面相同。
序号 |
cpu0的步骤(执行set) |
cpu1的步骤(执行print) |
1 |
想写入a=1,但是由于a不在自身的cacheline中,向cpu1发送read invalidate消息 |
执行while(b==0),由于b不在自身的cacheline中,向cpu0发送read消息 |
2 |
向store buffer中写入a=1 |
等待cpu0响应的read response消息 |
3 |
遇到smp_mb(),等待直到可以将store buffer中的内容刷新到cacheline |
等待cpu0响应的read response消息 |
4 |
等待直到可以将store buffer中的内容刷新到cacheline |
收到cpu0发来的read invalidate消息,发送a=0的值,同时将自身a所在的cacheline修改为invalidate状态 |
5 |
收到cpu1响应的read response和invalidate ack消息,将a=0的值设置到cacheline,随后store buffer中a=1的值刷新到cacheline,设置cacheline状态为M |
等待cpu0响应的read response消息 |
6 |
由于b就在自身的cacheline中,并且状态为M或者E,设置值为b=1 |
等待cpu0响应的read response消息 |
7 |
收到cpu1的read请求,将b=1的值传递回去,同时设置该cacheline状态为s |
等待cpu0响应的read response消息 |
8 |
无 |
收到cpu0的read response信息,将b设置为1,程序跳出循环 |
9 |
无 |
由于a所在的cacheline被设置为invalidate,因此向cpu0发送read请求 |
10 |
收到cpu1的read请求,以a=1响应,并且将自身的cacheline状态修改为s |
等待cpu0的read response响应 |
11 |
无 |
收到read response请求,将a设置为1,执行程序判断,结果为真 |
可以看到,在有了内存屏障之后,程序的真实结果就和我们的预期结果相同了。
invalidate queue
使用了store buffer后,cpu的store性能会提升很多。然后store buffer的容量是很小的(越快的东西,成本就越高,一定就越小),cpu以中等的频率填充store buffer。如果不幸发生比较多的cache miss,那么很快store buffer就被填满了,cpu只能等待。又或者程序中调用了smp_mb()
指令,这样后续的操作都只能进入store buffer,而不管相关cacheline是否处于M或者E状态。
store buffer很容易满的原因是因为收到其他cpu的invalidate ack的速度太慢。而cpu发送invalidate ack的速度太慢是因为cpu要等到将对应的cacheline设置为invalidate后才能发送invalidate ack。有的时候太多invalidate请求,cpu的处理速度就跟不上。为了加速这个流程,硬件设计者设计了invaldate queue来加速这个过程。收到的invalidate请求先放入invalidate queue,然后之后立刻响应invalidate ack消息。而cpu可以在随后慢慢的处理这些invalidate消息。当然,这里必须不能太慢。也就是说,cpu实际上给出了一个承诺,如果一个invalidatge请求在invalidate queue中,那么对于这个请求相关的cacheline,在该请求被处理完成前,cpu不会再发送任何与该cacheline相关的MESI消息。在有了store buffer和invalidate queue后,cpu的处理速度又可以更高。下面是结构图。
但是在引入了invalidate queue又会导致另外一个问题。下面先来看代码
public void set(){
a=1;
smp_mb();
b=1;
}
public void print(){
while(b==0)
;
assert a==1;
}
代码与上面的例子相同,但是初始条件不同了。这次a同时存在于cpu0和cpu1之中,状态为s。b是cpu0独享,状态为E或者M。
序号 |
cpu0的步骤(执行set) |
cpu1的步骤(执行print) |
1 |
想写入a=1,但是由于a的状态是s,向cpu1发送invalidate消息 |
执行while(b==0),由于b不在自身的cacheline中,向cpu0发送read消息 |
2 |
向store buffer中写入a=1 |
收到cpu0的invalidate消息,放入invalidate queue,响应invalidate ack消息。 |
3 |
遇到smp_mb(),等待直到可以将store buffer中的内容刷新到cacheline。立刻收到cpu0的invalidate ack,将store buffer中的a=1写入到cacheline,并且修改状态为M |
等待cpu0响应的read response消息 |
4 |
由于b就在自己的cacheline中,写入b=1,修改状态为M |
等待cpu0响应的read response消息 |
5 |
收到cpu1响应的read请求,将b=1作为响应回传,同时将cacheline的状态修改为s。 |
等待cpu0响应的read response消息 |
6 |
无 |
收到read response,将b=1写入cacheline,程序跳出循环 |
7 |
无 |
由于a所在的cacheline还未失效,load值,进行比对,assert失败 |
8 |
无 |
cpu处理invalidate queue的消息,将a所在的cachel |