原理和实战解析Linux中如何正确地使用内存屏障(下)

接上文原理和实战解析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内核源码技术学习路线+视频教程内核源码

学习直通车: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%以上的迷惘和困惑。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值