LWN: 對 edge-trigger 的誤解!
關(guān)注了就能看到更多這么棒的文章哦~
The edge-triggered misunderstanding
By Jonathan Corbet
August 5, 2021
DeepL assisted translation
https://lwn.net/Articles/864947/
安卓 12 beta release 將于今年 5 月首次公布。當然,每次發(fā)布新版本的時候都宣稱 "安卓歷史上最大的設(shè)計改動"。如果不要求用戶重新學(xué)習(xí)一切,那還配稱自己為一個新的安卓版本嗎?不過,這一歷史性事件并不打算包含一個許多測試者都注意到了的一個改動,這就是一個破壞了大量應(yīng)用程序的內(nèi)核 regression 問題。這個問題剛剛被修復(fù),但它是一個很好的例子,說明為什么如此難于阻止 regression 問題的出現(xiàn),以及當 regression 發(fā)生時,內(nèi)核項目是如何應(yīng)對的。
早在 2019 年底的時候,David Howells 對 pipe 管道相關(guān)代碼進行了一些修改來解決一些問題。不幸的是,這項工作引起了內(nèi)核社區(qū)認為最不可接受的那一類 regression:它讓內(nèi)核的編譯過程變慢了(甚至可能完全停止)!經(jīng)過廣泛的討論,終于查出來一個對 GNU make job server 的影響,Linus Torvalds 合入了一個 fix,問題就消失了。不久之后,5.5 版本的內(nèi)核發(fā)布了,內(nèi)核構(gòu)建的速度又加快了,人們認為這個問題應(yīng)該已經(jīng)解決了。
Not done yet
7 月底,Sandeep Patil 通知到社區(qū),雖然 GNU make 的問題可能已經(jīng) fix 了,但這個 fix 產(chǎn)生了一個新問題。他附上了一個 patch 將 Torvalds 的 fix 進行 revert 操作。很明顯,這個 patch 不可能被直接合入,因為內(nèi)核開發(fā)者完全不愿意再經(jīng)歷緩慢的 kernel build 過程,但這引發(fā)了對真正問題的調(diào)查。
2019 年的 pipe 的 rework 工作,以及后來的 fix 都讓 Torvalds 經(jīng)歷了意料之外的痛苦,所以他對代碼的結(jié)構(gòu)和行為都做了一些改動。具體來說,對 pipe 與 epoll_wait()、poll()和 select() 等系統(tǒng)調(diào)用的工作方式都進行了重要修改。如果希望進行的 I/O 操作不可以允許阻塞的話,這些調(diào)用就會把進程放到一個等待隊列中去。當情況發(fā)生變化時(比如可以開始讀取或?qū)懭霐?shù)據(jù)了),那么相應(yīng)的等待隊列上的進程就會被喚醒,它們也就可以繼續(xù)進行相應(yīng)的 I/O 請求了。
2019 年的 fix 改變了完成喚醒的方式。以前,向 pipe 寫東西會無條件地喚醒所有在等待的 reader 角色進程,事實上,在一次系統(tǒng)調(diào)用中可能會多次喚醒它們。這次 fix 改變了這個行為,變成只在操作開始時 pipe buffer 是為空的情況才會進行 wakeup 喚醒操作。也就是說,如果在寫入的目標 pipe 內(nèi)已經(jīng)有待讀取的數(shù)據(jù)的話,那么就僅僅只添加新的數(shù)據(jù),而不會進行 wakeup 操作。這個改動的邏輯很明確:如果數(shù)據(jù)已經(jīng)可以 read 了,那么 polling 類型的系統(tǒng)調(diào)用將立即返回,所以只要 pipe 里有可用的數(shù)據(jù)了,就不應(yīng)該有任何進程在繼續(xù)等待了。
On the edge
然而,epoll_wait() 有一種叫做 "邊緣觸發(fā)"(edge triggered, 或 EPOLLET)的模式,它的行為有點不同尋常。如果有可用的數(shù)據(jù)的話,申請進行 edge-trigged wait 的進程將不會像 epoll_wait() 那樣立即返回。相反,它會一直 wait 直到情況再次發(fā)生變化。至少,在 2019 年的 patch 之前是這樣的行為模式。所以,如果 pipe 驅(qū)動程序在數(shù)據(jù)到達時不再進行 waktup (當已經(jīng)有數(shù)據(jù)可用的情況下),在進行 edge-trigged wait 的進程將不會看到 "edge",因此也不會被 wakeup。
我們很有理由懷疑這種問題是否真的會出現(xiàn)。pipe_write() 的上一個版本的注釋就表達了這個觀點:
/* Always wake up, even if the copy fails. Otherwise
we lock up (O_NONBLOCK-)readers that sleep due to
syscall merging.
FIXME! Is this really true?
*/
事實證明,確實會有這種情況。有一些 Android 庫,比如 Realm,哪怕在 epoll_wait() 調(diào)用之前 pipe 中已經(jīng)有數(shù)據(jù)在等待了,也會要依賴 edge-triggered wakeup。顯然這里的目的是希望一直等待到 pipe buffer 全滿了,然后一次性地讀取到所有的數(shù)據(jù)。當 5.10 內(nèi)核跟安卓 12 beta 配合起來時,這些 library 就不再正常了,因此一組應(yīng)用程序也隨之無法正常工作了。此后,Realm 已經(jīng)解決了這個問題,但正如 Patil 指出的,很多進程 bundle 在一起就意味著 "等所有應(yīng)用程序都使用了更新過的 library 的話還會需要不少時間"。如果在 kernel 里面進行 fix 的話就可以為所有應(yīng)用程序修復(fù)這個問題。
人們似乎普遍認為,這些 library 實際上是誤解了 "edge-triggered" 的含義,并且錯誤地使用了這種模式。正如 Torvalds 所解釋的:
這實際上是對 epoll() 中的 "edge" 的含義的誤解。
edge 并不是指 "有人寫入了更多的數(shù)據(jù)"。edge 的意思是 "以前沒有數(shù)據(jù),現(xiàn)在有數(shù)據(jù)了"。
而一個 level triggered 事件 也不是 指 "有人寫了更多的數(shù)據(jù)"。它只是表示 "這里有數(shù)據(jù)"。
請注意,edge 和 level 都沒有提到 "更多的數(shù)據(jù)" 這個信息。其中一個是指從 "沒有數(shù)據(jù)"->"有數(shù)據(jù) "的這個變化,而另一個只是表示 "有數(shù)據(jù)"。
然而,pipe 的 edge-triggered 操作的實現(xiàn)方式并沒有做成這個樣子。不出所料,在 Hyrum 法則的作用下,應(yīng)用程序們開始根據(jù)系統(tǒng)實際實現(xiàn)出來的行為來實現(xiàn)了,而不是根據(jù)原本定義的語義。epoll() man page 與 Torvalds 的描述一致,介紹了這種阻塞行為(這些無法工作的應(yīng)用程序所面臨的情況)。如果是很久以前的話,內(nèi)核開發(fā)者們可能只是說一句 “這些庫做錯了”。但現(xiàn)在內(nèi)核不是這樣處理這類問題的了,因此,Torvalds 繼續(xù)說:
但是我們的 regression 是這么定義的:哪怕是參照文檔修改的,或者改成了正確的行為,也不是它可以不被稱為 regression 的理由。
regression 是指某個用戶的應(yīng)用程序是否可以被觀察到發(fā)生了破壞。
基于這種對 "regression" 的解釋,那么大家就需要 fix 這個問題。事實上,在 7 月底已經(jīng)完成了一個新的 patch,被合并到 5.14-rc4 中,并被包含在 5.10.56 和 5.13.8 的 stable update 中了。這個補丁并沒有完全恢復(fù)之前的行為,具體來說,它在每個寫操作中只會進行一次 wakeup 動作。不過,似乎確實解決了這個問題。
Problem solved?
5.5 內(nèi)核是在 2020 年 1 月發(fā)布的,當時我們之中很少有人意識到我們后續(xù)會面對這個世界會變成什么樣,像這個比較嚴重的內(nèi)核 regression 只是又一個意外而已。這個 regression 一直存在了一年半的時間,并在去年 12 月的時候進入了 5.10 long-term-stable release。現(xiàn)在才浮出水面,這說明在某些用戶場景的測試存在遺漏。令人高興的是,它在下一個安卓版本最終確定之前就被發(fā)現(xiàn)了。
不過,不確定在這一年半的時間里是否有任何應(yīng)用程序已經(jīng)開始依賴更新過的語義了。事實上,已經(jīng)有一份報告(來自英特爾的自動測試系統(tǒng))顯示,在合入了最新的 fix 后,hackbench 這個 benchmark 產(chǎn)生了將近 13% 的性能下降。Torvalds 回應(yīng)說,他 "不確定 hackbench 到底有多重要",也許這種性能下降 "可能一點都不重要"。即便如此,他還是發(fā)布了一個新的 patch,提供了更接近于舊有行為的實現(xiàn),但也僅當 pipe 是用這些 polling 函數(shù)族中的其中一個時才有效。如果事實證明 hackbench 的性能下降確實是很重要的問題,那么至少我們手頭會有一個 fix 已經(jīng)準備好了。
如果最新的 fix 還破壞了其他東西,那么內(nèi)核開發(fā)者可能會面臨一個兩難選擇??赡芫蜔o法在不導(dǎo)致應(yīng)用程序被破壞的情況下繼續(xù)推進了,這就是為什么需要盡量早抓到這些 regression。希望我們運氣足夠好,不會有這種兩難選擇拋給我們,并且這個意料之外的故事能終結(jié)在這個地方。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
