接上文原理和实战解析Linux中如何正确地使用内存屏障(上)
实战一:运行Linux的多核通过中断通信
它的一般模式是:CPU0在DDR填入一段数据,然后通过store指令写INTR的寄存器向CPU1发送中断。
实战二:写入数据到内存后,发起DMA
下面我们把需求变更为,CPU写入一段数据后,写Ethernet控制器与CPU之间的doorbell,发起DMA操作发包。
我们还是套一下三要素:
a. 谁和谁保序? -> CPU和EMAC的DMA保序,DMA和CPU显然不是inner
b. 在哪里保序? -> 只需要EMAC的DMA看到CPU写入发包数据后,再看到它写doorbell
c. 朝哪个方向保序? -> CPU写入一段数据,然后写入doorbell,只需要在st方向保序。
于是,我们得出正确的barrier应该是:dmb + osh + st,为什么是dmb呢,因为doorbell也是store写的。我们来看看Yunsheng Lin童鞋的这个commit,它把用力过猛的wmb(),替换成了用writel()来写doorbell:
在ARM64平台下,writel内嵌了一个dmb + osh + st,这个从代码里面可以看出来:
同样的逻辑也可能发生在CPU与其他outer组件之间,比如CPU与ARM64的SMMU:
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
实战三:CPU与MCU通过共享内存和hwspinlock通信
下面我们把场景变更为主CPU和另外一个cortex-m的MCU通过一片共享内存通信,对这片共享内存的访问透过硬件里面自带的hwspinlock(hardware spinlock)来解决。
我们想象CPU持有了hwspinlock,然后读取对方cortex-m给它写入共享内存的数据,并写入一些数据到共享内存,然后解锁spinlock,通知cortex-m,这个时候cortex-m很快就可以持有锁。
我们还是套一下三要素:
a. 谁和谁保序? -> CPU和Cortex-M保序
b. 在哪里保序? -> CPU读写共享内存后,写入hwspinlock寄存器解锁,需要cortex-m看到同样的顺序
c. 朝哪个方向保序? -> CPU读写数据,然后释放hwspinlock,我们要保证,CPU的写入对cortex-m可见;我们同时要保证,CPU放锁前的共享内存读已经完成,如果我们不能保证解锁之前CPU的读已经完成,cortex-m很可能马上写入新数据,然后CPU读到新的数据。所以这个保序是双向的。
里面用的是mb(),这是一个dsb+full system+ld+st,读代码的注释也是一种享受。
实战四:S MMU与CPU通过一个queue通信
现在我们把场景切换为,SMMU与CPU之间,通过一片放入共享内存的queue来通信,比如SMMU要通知CPU一些什么event,它会把event放入queue,放完了SMMU会更新另外一个pointer内存,表示queue增长到哪里了。
然后CPU通过这样的逻辑来工作
这是一种典型的控制依赖,而控制依赖并不能被硬件自动保序,CPU完全可以在if(pointer满足什么条件)满足之前,投机load了queue的内容,从而load到了错误的queue内容。
我们还是套一下三要素:
a.谁和谁保序? -> CPU和SMMU保序
b.在哪里保序? -> 要保证CPU先读取SMMU的pointer后,再读取SMMU写入的queue;
c.朝哪个方向保序? -> CPU读pointer,再读queue内容,在load方向保序
于是,我们得出正确的barrier应该是:dmb + osh + ld,我们来看看wangzhou童鞋的这个修复:
ARM64平台的readl()也内嵌了dmb + osh + ld屏障。显然这个修复的价值是非常大的,这是一个由弱变强的过程。前面我们说过,由强变弱是性能问题,而由弱变强则往往修复的是稳定性问题。也就是这种用错了弱barrier的场景,往往bug非常难再现,需要很长时间的测试才再现一次。
实战五:修改页表PTE后刷新tlb
现在我们的故事演变成了,CPU0修改了页表PTE,然后通知其他所有CPU,PTE应该被更新,其他CPU需要刷新TLB。
它的一般流程是CPU调用set_pte_at()修改了内存里面的PTE,然后进行tlbi等动作。这里就变地非常复杂了:
我们看看barrier1,它在屏障store和tlbi之间,由于二者一个是狗狗,一个是消杀烟雾,显然不能是dmb,只能是dsb;我们需要CPU1看到set_pte_at的动作先于tlbi的动作,所以这个屏障的范围应该是ISH;由于屏障需要保障的是set_pte_at的store,而不是load,所以方向是st,由此我们得出第一个barrier应该是:dsb + ish + st。
详细的流程我们可以参考下如下代码:
barrier2用的是dsb(ish),它保证了inner内的CPU都先看到了tlbi的完成;barrier3用的isb(),它保证了CPU fetch到PTE修正之后的指令。
结语
本文对Linux内核的内存屏障的原理和用法进行一些分析和实战,它并未覆盖内存屏障的全部知识,但是应该可应付工程里面90%以上的迷惘和困惑。