深入linux内核架构--内存屏障

简介

之前在看volatile 可见性的时候,经常会看到内存屏障,但是对于其基本原理似懂非懂,也对于内存屏障是如何保障多个CPU之间的数据可见性保持好奇,网上的博客基本上只是停留于表面,导致我产生了几个误区:
1. CPU之间内存数据可见性问题是由于cpu cache没及时同步数据导致的。
2. 内存屏障为啥能通过防止指令重排序,就能让cpu cache及时同步数据?
不过最近发现这只是表面现象!所以今天写一下这篇文章来彻底介绍一下内存屏障

CPU cache

虽然内存可见性问题不是直接由cpu cache导致的,还是与cpu cache是有密切联系的,所以我们先来简单介绍一下cpu cache。
在我之前的文章中有提到内核对于小块内存是通过slab来管理的,slab将内存划分为小块,每一块为2的一个指数级,而这个内存块是与cpu cacheline一一对应的,cacheline的大小在一个16字节到256字节的范围,通过一些缓存失效机制来进行替换,比如FIFO,LRU之类的。


cpu cache

内核其实是有机制保证CPU之间数据一致性的,我们称这种缓存一致性协议为MESI 状态机。
MESI是一个比较啰嗦的机制,其主要作用就是通过四种状态之间的切换来保证同一数据在不同CPU中的一致性,这和内存屏障其实关系不大,所以我下面只做简单介绍,如果有兴趣详细了解的,见文章最后的参考资料。
M即"modify",表示一个CPU line独占当前变量数据,且该变量未同步回memory,所以更新该变量时需要同步回内存,或转发给其他cpu。
E即"exclusive",它和modify的唯一区别是,已经同步回memory,因而可以直接操作该line(读写),而不需要和其他cpu或者memory进行其他交互。
S即"share",表示该line至少被一个cpu共享着,如果需要操作该line需要通知其他也共享该line的cpu。
I即"invalid",表示空line或者无效line。


MESI state diagram

各个状态之间可以通过一些操作来切换,这些操作我称他为”消息调用“或组合,其实跟rpc调用之类的相似。
MESI message:
Read:从一个指定cache line物理地址读取数据

Read response: 返回数据
Invalidate: 将一个指定cache line物理地址标记为无效,所有相关CPU都要删除对应的cache line
Invalidate response: 收到invalidate消息的cpu要返回确认。
Read Invalidate: 原子性的发送 Read + Invalidate
Writeback: 将指定cache line中的数据写回memory
CPU通过MESI中一套严密的事务操作来保障操作同一变量,在各个CPU中都是一致的。这里状态切换,特别繁杂,就不一一介绍了,就举一个简单列子来说明一下两个CPU中是如何保障操作同一变量的一致性吧:
有如下三条指令

1 a=1;
2 b=a+1;
3 assert(b == 2);

其中a存在cpu1的cache中,b存在cpu0的cache中,现在在cpu0中执行上述指令:
那么操作过程为:

  1. cpu 0 发现a不在缓存中,且自己需要read-modify-write,所以就会发起一个Read Invalidate message,cpu1将a对应的cpu line置为无效,并返回ack。
  2. cpu 0 等待 Invalidate ACK,直至收到ACK后,才执行后续指令
  3. b就在cpu 0的cache line中,所以直接操作让b = 2;
  4. 指令3assert成功。
  5. 如果cpu1上的进程如果读a的话,会向cpu0发送read请求

如果按照上面的逻辑走的话,即使没有mb(memory barrier),也不会有任何问题。但是有经验的同学会发现在步骤2中,必须要等待CPU1返回ACK后才能执行后续的指令,非常低效,因此CPU 引入了store buffer的机制,来提高CPU性能。

Unnecessary Stall

store buffer

在上述的例子中,可以发现CPU 0 其实并不依赖其他CPU中a的值,所以完全可以不用等待ack也能保障CPU 0中指令的执行正确性,通过把a的值存入store buffer就可以接着跑后续的指令了,性能大大提升。
但是这种有些特殊场景却无法涵盖,比如上述指令中对于变量a,Read Invalidate调用会将CPU 0 的cache line置为0(从CPU1读过来的),而store buffer中的值为1,如果CPU只拿cache line中的值的话,会有一致性问题,所以store buffer还需要与store forwarding机制一起合作。也就是如果store buffer中有值,就从store buffer中拿。


Store Forwarding

memory barrier

引入了store buffer 及 store forwarding 后看上去解决了cpu cache一致性问题,且性能大大提升,但是这仅仅保证了当前CPU的指令正确性。我们来看另外一个例子:

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 }

在CPU 0中执行foo,CPU 1中执行bar,a在CPU 1的缓存中, b 在CPU 0的缓存中,执行序列如下:

  1. CPU0 执行 a=1,将a的值存入store buffer,发现a不在缓存中,所以向发送Read Invalidate message。
  2. CPU1 执行while (b == 0) continue;,b不在CPU1cache中,发送Read message。
  3. CPU0 执行 b=1,b owned by itself,所以直接更新。
  4. CPU0 接收到b的read请求后,将更新过后的值发给CPU1。
  5. CPU1 将b的值更新到自己的缓存
  6. CPU1 执行while (b == 0) continue;中b的值变为1,可以跳出该循环了。
  7. CPU1 执行assert(a == 1);,此时CPU1中的a值可能尚未被置为无效,所以直接从cache中拿出来其,值为0,导致assert failed。
    从上可以看出,如果Read Invalidate message未被执行完时,会让两个CPU中对于同一值出现不一致的情况。按正常逻辑来说b已经更新为1了,a不可能还是0。
    单从硬件方面来说,目前没有很好的机制来解决这一类问题,所以就引入了memory barrier 指令来防止CPU cache中的这种store buffer带来的不一致性问题。但是mb相比于原始的unnecessary stall机制还是有优化的,就是当遇到memory barrier 指令时,将store buffer中的已有的所有变量做标记,后续的write操作,必须要等buffer中所有被标记数据清空后,才将未做标记的数据更新到cache。我们将上述例子改写如下:
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 }

那么CPU0执行完a=1后,将其写入store buffer,执行到smp_mb后将store buffer中的所有变量标记,执行到b=1时,写入store buffer后不做标记,且不写回cache line,在收到CPU1的请求后将b的cache line 标记为shared,当CPU0收到CPU1对于a的Invalidate ACK后,将a更新回cache line并将其置为modified,且清除store buffer中a的值,所以CPU0中store buffer没有被标记的数据了,剩余的b就可以更新会cache了,此时其是share状态所以其更新会向其share CPU发送Invalidate message,CPU1接收到message后重新Read,将b置一继续后面的逻辑,这样assert就是没问题的了。

Store Sequence Capacity

有了上续机制后,可以通过mb来禁止指令读取乱序后,解决了数据CPU间的一致性问题,但是又带了另外一个问题,store buffer的容量是很小的,如果等待的Invalidate指令迟迟不返回,那么store buffer就会被填满,而无法继续执行后续指令。
所以就需要一些机制来加速Invalidate message的执行,便引入了Invalidate Queue,这是因为Invalidate message执行慢的主要原因是需要确保对应的cache line确实是无效的,但是这个验证工作有时候会因为CPU过忙而推迟。所以有了Invalidate Queue后可以不做有效性验证,直接ACK,后续有空了再验证清除对应的cache line,这样就不会stalling 其他 CPU了。但这又产生了另外一个问题,要确保当前CPU中读取某一个值时,其是否在Invalidate Queue中,否则就会读到无效的数据。比如上述例子中的a如果只存在CPU1的Invalidate Queue中,却未被真正的清除,那么bar还是assert 失败(a == 0)的,具体过程可以自己推到。

Invalidate Queues and Memory Barriers

所以我们需要另外一种机制确保读某一个值时,其不能存在于Invalidate Queues中,所以硬件开发者又提供了一种方式来解决该问题,这种机制还是基于mb 指令,但是这次带来了一个新的限制,当遇到mb时将Invalidate Queues中的所有变量做标记,后续有读指令时,必须要等所有被标记的Invalidate Queues的数据被处理完后才能继续执行。因此我们可以将上述代码修改如下:

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 smp_mb();
12 assert(a == 1);
13 }

至此就可以保障变量a在CPU0 和 CPU1中的完全一致可见性了。

后记

由此可见,CPU之间的变量可见性不是直接由于CPU cache,也不是由于指令乱序,而是由于CPU Cache-Coherence Protocols性能优化中store buffer带来的数据不一致性问题带来的的cache不一致性问题,而内存屏障也不是用来禁止指令乱序,而是解决由于数据只更新到了store buffer却没有更新回cache以及Invalidate不同步问题。

参考资料
whymb

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容