LWN: 使用KCSAN檢測(cè)出缺失的memory barrier!
關(guān)注了就能看到更多這么棒的文章哦~
Detecting missing memory barriers with KCSAN
By Jonathan Corbet
December 2, 2021
DeepL assisted translation
https://lwn.net/Articles/877200/
編寫(xiě)并發(fā)場(chǎng)景的代碼的時(shí)候用鎖來(lái)避免 race condition ,就已經(jīng)相當(dāng)考驗(yàn)技術(shù)了。而如果目標(biāo)是使用 lockless 算法,也就是依靠 memory barrier 而不用 lock 從而避免使用鎖引入的開(kāi)銷時(shí),就更加困難了。在這種類型的代碼中,非常容易產(chǎn)生錯(cuò)誤,也很難發(fā)現(xiàn)。不過(guò),不久后可能會(huì)有一些工具可以提供幫助了,Marco Elver 的 patch set 增強(qiáng)了
Kernel Concurrency Sanitizer(KCSAN)的能力,可以檢測(cè)到某些場(chǎng)景下缺失的 memory barrier。
KCSAN 是通過(guò)觀察對(duì)指定內(nèi)存地址的那些訪問(wèn),采用統(tǒng)計(jì)學(xué)方式來(lái)分析,從而希望能檢測(cè)出可疑的 pattern。它使用的算法在之前的文章中有過(guò)介紹。不過(guò),它當(dāng)前實(shí)現(xiàn)的代碼中只能捕捉到某些特定類型的 race condition,也就是是那些 locking 錯(cuò)誤引起的競(jìng)態(tài)條件。其他類型的競(jìng)態(tài)問(wèn)題仍然無(wú)法用這個(gè)工具來(lái)檢測(cè)出來(lái),包括一些在 lockless 代碼中出現(xiàn)的一些可能的競(jìng)態(tài)問(wèn)題。KCSAN 的設(shè)計(jì)理念就導(dǎo)致了它無(wú)法發(fā)現(xiàn)由于 CPU 和內(nèi)存控制器對(duì) memory write 操作進(jìn)行重排序(reorder)時(shí)導(dǎo)致的各種問(wèn)題。
我們拿下面的代碼作為例子,它來(lái)自 Elver 的 patch set 中的文檔 patch(并進(jìn)行了簡(jiǎn)單的修改):
int x, flag;
void T1(void)
{
x = 1; // data race!
WRITE_ONCE(flag, 1); // should be: smp_store_release(&flag, 1)
}
void T2(void)
{
while (!READ_ONCE(flag)) // should be: smp_load_acquire(&flag)
;
... = x; // data race!
}
乍看之下,這段代碼像是沒(méi)有什么問(wèn)題。T1() 會(huì)將數(shù)值存儲(chǔ)到變量 x 中,然后設(shè)置 flag 來(lái)表示 x 已經(jīng)是有效的(valid)了。在 T2() 中運(yùn)行的另一個(gè)線程則確保在 flag 被置為 1 之前不會(huì)去讀取 x,所以它取到 x 的值的時(shí)候應(yīng)該可以確保都是有效的值了。只有一個(gè)小問(wèn)題:由于這里沒(méi)有添加 memory barrier 操作,CPU 完全有權(quán)力對(duì)這些操作進(jìn)行 reorder,畢竟在 CPU 看來(lái)這些操作互相是毫無(wú)關(guān)聯(lián)的。事實(shí)上,在 T1() 中對(duì) x 的 write 可能在對(duì) flag 的 write 之后才會(huì)被系統(tǒng)中的其他 CPU 真正看到。這可能導(dǎo)致 T2() 認(rèn)為它看到的 x 值是有效的,但其實(shí)這個(gè)時(shí)候這個(gè)線程取到的其實(shí)是真正的數(shù)據(jù)寫(xiě)入 x 之前的 x 的值。
要修正這部分代碼的話,需要使用 smp_store_release() 來(lái)寫(xiě)入 flag,這就會(huì)確保在這個(gè) store 操作之前所做的所有 store 操作都先完成,然后才會(huì)讓系統(tǒng)中其他部分能看到 flag 標(biāo)志變?yōu)?1 了。同樣地,需要用 smp_load_acquire() 來(lái)讀取 flag,這樣就不會(huì)把后面要進(jìn)行的 read 操作被 reorder 到此操作之前,從而避免發(fā)生提前 read。barrier 幾乎總是需要配對(duì)出現(xiàn),才能確保是正確的。省略兩個(gè)操作中的哪一個(gè),都會(huì)導(dǎo)致產(chǎn)生錯(cuò)誤的代碼。
對(duì)于實(shí)現(xiàn) lockless 算法的開(kāi)發(fā)者來(lái)說(shuō),很難保證完全不犯這種錯(cuò)誤。對(duì)于某個(gè)具體的訪問(wèn)操作來(lái)說(shuō),并不能那么容易地判斷出來(lái)這里需要 memory barrier。而帶有這種 bug 的代碼可能在開(kāi)發(fā)者的所有測(cè)試中都是能正常工作的,但在少數(shù)的生產(chǎn)系統(tǒng)所具有的一些特定條件出現(xiàn)的時(shí)候才暴露出 bug。這就是為什么一些開(kāi)發(fā)者一旦了解了 lockless 編程的難點(diǎn),就會(huì)得出結(jié)論,還是從事在 JavaScript 中實(shí)現(xiàn)那些讓人厭煩的彈出窗口這類工作更加好一些。
在目前的內(nèi)核中,KCSAN 也無(wú)法檢測(cè)到這種 race。在 KCSAN 下運(yùn)行的系統(tǒng)可能會(huì)認(rèn)為 T1() 中對(duì) x 的 store 操作很可疑,值得注意,但是 KCSAN 所采用的觀察方式會(huì)導(dǎo)致 race 競(jìng)態(tài)條件不會(huì)出現(xiàn)。因?yàn)?KCSAN 即將開(kāi)始監(jiān)視對(duì) x 的訪問(wèn)、或者在持續(xù)保持監(jiān)控的期間,它會(huì)將 T1() 的后續(xù)執(zhí)行推后,從而觀察是否有什么可疑的訪問(wèn)發(fā)生了。T1() 只有在監(jiān)控期間結(jié)束之后才會(huì)得以繼續(xù)執(zhí)行(從而來(lái)設(shè)置 flag)。因此,KCSAN 事實(shí)上延遲了對(duì) flag 的寫(xiě)入,導(dǎo)致 T2() 可以一直等待直到這個(gè)監(jiān)控期間結(jié)束。所以只要 KCSAN 在進(jìn)行監(jiān)控,就不可能發(fā)生亂序訪問(wèn)的情況,也就無(wú)法發(fā)現(xiàn)這里的問(wèn)題了。
新的代碼做了一個(gè)看似非常簡(jiǎn)單的改動(dòng),從而希望能檢測(cè)出這種問(wèn)題。盡管這種很簡(jiǎn)單的理念,其實(shí)需要 25 個(gè) patch 組合起來(lái)才能實(shí)現(xiàn)。KCSAN 在結(jié)束了監(jiān)控期之后,會(huì)在后續(xù)的每一個(gè)內(nèi)存訪問(wèn)之后重復(fù)進(jìn)行觀察(watch)一直到遇到 memory barrier 或函數(shù)返回為止,而不是簡(jiǎn)單地把 x 拋諸腦后。在上面介紹的這種情況下,KCSAN 將在向 flag 賦值之后再次開(kāi)始觀察 x,本質(zhì)上就是模擬對(duì)這兩個(gè)變量的寫(xiě)入順序的一個(gè) reorder。實(shí)質(zhì)上,這種重復(fù)進(jìn)行的觀察就是在看如果對(duì) x 的 write 被看到的時(shí)間點(diǎn)比起開(kāi)發(fā)者預(yù)想的要晚,那么會(huì)發(fā)生什么情況。在再次進(jìn)行觀察中看到的對(duì) x 的任何訪問(wèn)仍然是有競(jìng)態(tài)問(wèn)題的(racy),因?yàn)闆](méi)有進(jìn)行 memory barrier 操作來(lái)確保正確的執(zhí)行順序。所以 KCSAN 現(xiàn)在會(huì)在 T2() 中檢測(cè)到對(duì) x 的 read 操作是有競(jìng)態(tài)問(wèn)題的,并給出警告。
這個(gè)算法可以檢測(cè)到這一種由缺失障礙引起的競(jìng)態(tài)問(wèn)題,但無(wú)法找到所有的。最值得注意的是,它可以用來(lái)檢測(cè)出將一個(gè)對(duì)共享數(shù)據(jù)進(jìn)行的訪問(wèn)推遲了之后的情況——就比如上面的例子中對(duì) x 寫(xiě)入之后這個(gè)值就要過(guò)一段時(shí)間才會(huì)被其他人可以看到——但不能檢測(cè)出比開(kāi)發(fā)者預(yù)期的更早進(jìn)行了這個(gè)訪問(wèn)的效果。盡管它的覆蓋場(chǎng)景尚不完全,但是這個(gè)改進(jìn)后的 KCSAN 很可能能夠阻止一些與 barrier 有關(guān)的 bug 從而避免影響到用戶。這可能會(huì)使我們這些普通人在開(kāi)發(fā)時(shí)可以更容易接受 lockless 算法,甚至可能使他們中的一些人擺脫未來(lái)不得不去開(kāi)發(fā) JavaScript 的黯淡前途。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長(zhǎng)按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開(kāi)源社區(qū)的各種新近言論~
