原理和實(shí)戰(zhàn)解析Linux中如何正確地使用內(nèi)存屏障(下)
接上文原理和實(shí)戰(zhàn)解析Linux中如何正確地使用內(nèi)存屏障(上)
實(shí)戰(zhàn)一:運(yùn)行Linux的多核通過(guò)中斷通信

它的一般模式是:CPU0在DDR填入一段數(shù)據(jù),然后通過(guò)store指令寫(xiě)INTR的寄存器向CPU1發(fā)送中斷。
store數(shù)據(jù)
barrier?
store intr寄存器
中間應(yīng)該用什么barrier?我們來(lái)回憶一下三要素:
a. 誰(shuí)和誰(shuí)保序? -> CPU0和CPU1這2個(gè)observer之間看到保序
b. 在哪里保序? -> 只需要CPU1看到CPU0寫(xiě)入DDR和intr寄存器是保序的
c. 朝哪個(gè)方向保序? -> CPU0寫(xiě)入一段數(shù)據(jù),然后寫(xiě)入intr寄存器,只需要在st方向保序。
由此,我們得出結(jié)論,應(yīng)該使用的barrier是:dmb + ish + st,顯然就是smp_wmb。內(nèi)核代碼drivers/irqchip/irq-bcm2836.c也可以證實(shí)這一點(diǎn):

里面的注釋非常清晰,smp_wmb()保證了發(fā)起IPI之前,其他CPU應(yīng)該先觀察到內(nèi)存的數(shù)據(jù)在位。
現(xiàn)在我們把INTR換成gic-v3,就會(huì)變地tricky很多。gic-v3的IPI寄存器并不是映射到內(nèi)存空間的,而是一個(gè)sys寄存器,通過(guò)MSR來(lái)寫(xiě)入。前面我們說(shuō)過(guò)DMB只能搞定load/store之間,搞不定load/store與其他東西之間。
最開(kāi)始的gic-v3驅(qū)動(dòng)的作者其實(shí)也誤用了smp_wmb,造成了該驅(qū)動(dòng)的穩(wěn)定性問(wèn)題。于是Shanker Donthineni童鞋進(jìn)行了一個(gè)修復(fù),這個(gè)修復(fù)的commit如下:

這個(gè)commit解釋了我們不能用dmb搞定memory和sysreg之間的事情,于是這個(gè)patch替換為了更強(qiáng)力的wmb(),那么這個(gè)替換是正確的嗎?
我們還是套一下三要素:
a. 誰(shuí)和誰(shuí)保序? -> CPU0和CPU1保序
b. 在哪里保序? -> 只需要CPU1看到CPU0寫(xiě)入DDR后,再看到它寫(xiě)sysreg
c. 朝哪個(gè)方向保序? -> CPU0寫(xiě)入一段數(shù)據(jù),然后寫(xiě)入sysreg寄存器,只需要在st方向保序。
我們要進(jìn)行保序的是CPU0和CPU1之間,顯然他們屬于inner。于是,我們得出正確的barrier應(yīng)該是:dsb + ish + st,wmb()屬于用力過(guò)猛了,因?yàn)閣mb = dsb(st),保序范圍是full system?;诖?,筆者再次在主線內(nèi)核對(duì)Shanker Donthineni童鞋的“修復(fù)”進(jìn)行了“修復(fù)”,縮小屏障的范圍,提升性能:

實(shí)戰(zhàn)二:寫(xiě)入數(shù)據(jù)到內(nèi)存后,發(fā)起DMA
下面我們把需求變更為,CPU寫(xiě)入一段數(shù)據(jù)后,寫(xiě)Ethernet控制器與CPU之間的doorbell,發(fā)起DMA操作發(fā)包。

我們還是套一下三要素:
a. 誰(shuí)和誰(shuí)保序? -> CPU和EMAC的DMA保序,DMA和CPU顯然不是inner
b. 在哪里保序? -> 只需要EMAC的DMA看到CPU寫(xiě)入發(fā)包數(shù)據(jù)后,再看到它寫(xiě)doorbell
c. 朝哪個(gè)方向保序? -> CPU寫(xiě)入一段數(shù)據(jù),然后寫(xiě)入doorbell,只需要在st方向保序。
于是,我們得出正確的barrier應(yīng)該是:dmb + osh + st,為什么是dmb呢,因?yàn)閐oorbell也是store寫(xiě)的。我們來(lái)看看Yunsheng Lin童鞋的這個(gè)commit,它把用力過(guò)猛的wmb(),替換成了用writel()來(lái)寫(xiě)doorbell:

在ARM64平臺(tái)下,writel內(nèi)嵌了一個(gè)dmb + osh + st,這個(gè)從代碼里面可以看出來(lái):

同樣的邏輯也可能發(fā)生在CPU與其他outer組件之間,比如CPU與ARM64的SMMU:

實(shí)戰(zhàn)三:CPU與MCU通過(guò)共享內(nèi)存和hwspinlock通信
下面我們把場(chǎng)景變更為主CPU和另外一個(gè)cortex-m的MCU通過(guò)一片共享內(nèi)存通信,對(duì)這片共享內(nèi)存的訪問(wèn)透過(guò)硬件里面自帶的hwspinlock(hardware spinlock)來(lái)解決。

我們想象CPU持有了hwspinlock,然后讀取對(duì)方cortex-m給它寫(xiě)入共享內(nèi)存的數(shù)據(jù),并寫(xiě)入一些數(shù)據(jù)到共享內(nèi)存,然后解鎖spinlock,通知cortex-m,這個(gè)時(shí)候cortex-m很快就可以持有鎖。
我們還是套一下三要素:
a. 誰(shuí)和誰(shuí)保序? -> CPU和Cortex-M保序
b. 在哪里保序? -> CPU讀寫(xiě)共享內(nèi)存后,寫(xiě)入hwspinlock寄存器解鎖,需要cortex-m看到同樣的順序
c. 朝哪個(gè)方向保序? -> CPU讀寫(xiě)數(shù)據(jù),然后釋放hwspinlock,我們要保證,CPU的寫(xiě)入對(duì)cortex-m可見(jiàn);我們同時(shí)要保證,CPU放鎖前的共享內(nèi)存讀已經(jīng)完成,如果我們不能保證解鎖之前CPU的讀已經(jīng)完成,cortex-m很可能馬上寫(xiě)入新數(shù)據(jù),然后CPU讀到新的數(shù)據(jù)。所以這個(gè)保序是雙向的。

里面用的是mb(),這是一個(gè)dsb+full system+ld+st,讀代碼的注釋也是一種享受。
實(shí)戰(zhàn)四:S MMU與CPU通過(guò)一個(gè)queue通信
現(xiàn)在我們把場(chǎng)景切換為,SMMU與CPU之間,通過(guò)一片放入共享內(nèi)存的queue來(lái)通信,比如SMMU要通知CPU一些什么event,它會(huì)把event放入queue,放完了SMMU會(huì)更新另外一個(gè)pointer內(nèi)存,表示queue增長(zhǎng)到哪里了。

然后CPU通過(guò)這樣的邏輯來(lái)工作

這是一種典型的控制依賴(lài),而控制依賴(lài)并不能被硬件自動(dòng)保序,CPU完全可以在if(pointer滿(mǎn)足什么條件)滿(mǎn)足之前,投機(jī)load了queue的內(nèi)容,從而load到了錯(cuò)誤的queue內(nèi)容。
我們還是套一下三要素:
a.誰(shuí)和誰(shuí)保序? -> CPU和SMMU保序
b.在哪里保序? -> 要保證CPU先讀取SMMU的pointer后,再讀取SMMU寫(xiě)入的queue;
c.朝哪個(gè)方向保序? -> CPU讀pointer,再讀queue內(nèi)容,在load方向保序
于是,我們得出正確的barrier應(yīng)該是:dmb + osh + ld,我們來(lái)看看wangzhou童鞋的這個(gè)修復(fù):

ARM64平臺(tái)的readl()也內(nèi)嵌了dmb + osh + ld屏障。顯然這個(gè)修復(fù)的價(jià)值是非常大的,這是一個(gè)由弱變強(qiáng)的過(guò)程。前面我們說(shuō)過(guò),由強(qiáng)變?nèi)跏切阅軉?wèn)題,而由弱變強(qiáng)則往往修復(fù)的是穩(wěn)定性問(wèn)題。也就是這種用錯(cuò)了弱barrier的場(chǎng)景,往往bug非常難再現(xiàn),需要很長(zhǎng)時(shí)間的測(cè)試才再現(xiàn)一次。
實(shí)戰(zhàn)五:修改頁(yè)表PTE后刷新tlb
現(xiàn)在我們的故事演變成了,CPU0修改了頁(yè)表PTE,然后通知其他所有CPU,PTE應(yīng)該被更新,其他CPU需要刷新TLB。

它的一般流程是CPU調(diào)用set_pte_at()修改了內(nèi)存里面的PTE,然后進(jìn)行tlbi等動(dòng)作。這里就變地非常復(fù)雜了:

我們看看barrier1,它在屏障store和tlbi之間,由于二者一個(gè)是狗狗,一個(gè)是消殺煙霧,顯然不能是dmb,只能是dsb;我們需要CPU1看到set_pte_at的動(dòng)作先于tlbi的動(dòng)作,所以這個(gè)屏障的范圍應(yīng)該是ISH;由于屏障需要保障的是set_pte_at的store,而不是load,所以方向是st,由此我們得出第一個(gè)barrier應(yīng)該是:dsb + ish + st。
詳細(xì)的流程我們可以參考下如下代碼:

barrier2用的是dsb(ish),它保證了inner內(nèi)的CPU都先看到了tlbi的完成;barrier3用的isb(),它保證了CPU fetch到PTE修正之后的指令。
結(jié)語(yǔ)
本文對(duì)Linux內(nèi)核的內(nèi)存屏障的原理和用法進(jìn)行一些分析和實(shí)戰(zhàn),它并未覆蓋內(nèi)存屏障的全部知識(shí),但是應(yīng)該可應(yīng)付工程里面90%以上的迷惘和困惑。
原文作者:內(nèi)核工匠
