是什麼阻止 stdout/stderr 交錯?
假設我執行一些程序:
#!/usr/bin/env bash foo & bar & baz & wait;
我像這樣執行上面的腳本:
foobarbaz | cat
據我所知,當任何程序寫入 stdout/stderr 時,它們的輸出永遠不會交錯 - stdio 的每一行似乎都是原子的。這是如何運作的?什麼實用程序控制每行的原子性?
他們確實交錯了!您只嘗試了短暫的輸出突發,它保持未拆分,但實際上很難保證任何特定輸出保持未拆分。
輸出緩衝
這取決於程序如何緩衝其輸出。大多數程序在編寫時使用的stdio 庫使用緩衝區來提高輸出效率。當程序呼叫庫函式寫入文件時,函式不會立即輸出數據,而是將此數據儲存在緩衝區中,並且只有在緩衝區填滿後才實際輸出數據。這意味著輸出是分批完成的。更準確地說,有三種輸出模式:
- 無緩衝:數據立即寫入,不使用緩衝區。如果程序將其輸出寫入小塊,例如逐個字元,這可能會很慢。這是標準錯誤的預設模式。
- 完全緩衝:僅當緩衝區已滿時才寫入數據。這是寫入管道或正常文件時的預設模式,stderr 除外。
- 行緩衝:數據在每個換行符之後寫入,或者當緩衝區已滿時寫入。這是寫入終端時的預設模式,stderr 除外。
程序可以重新程式每個文件以使其行為不同,並且可以顯式刷新緩衝區。當程序關閉文件或正常退出時,緩衝區會自動刷新。
如果寫入同一管道的所有程序要麼使用行緩沖模式,要麼使用非緩沖模式並通過對輸出函式的一次呼叫來寫入每一行,並且如果行足夠短以寫入單個塊,則輸出將是整行的交錯。但是,如果其中一個程序使用完全緩沖模式,或者行太長,那麼您會看到混合行。
這是一個範例,我將兩個程序的輸出交錯。我在 Linux 上使用了 GNU coreutils;這些實用程序的不同版本可能表現不同。
yes aaaa``aaaa
以基本上等同於行緩沖模式的方式永久寫入。該yes
實用程序實際上一次寫入多行,但每次發出輸出時,輸出都是整數行。echo bbbb; done | grep b``bbbb
在完全緩沖模式下永久寫入。它使用大小為 8192 的緩衝區,每行 5 個字節長。由於 5 不除 8192,因此寫入之間的邊界一般不在行邊界處。讓我們一起推銷它們。
$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba bbaaaa bbbbaaaa baaaa bbbaaaa bbaaaa bbbaaaa ab bbbbaaa
如您所見,是的,有時會中斷 grep,反之亦然。只有大約 0.001% 的線路被中斷,但它確實發生了。輸出是隨機的,因此中斷的次數會有所不同,但我每次都至少看到一些中斷。如果行更長,則中斷行的比例會更高,因為隨著每個緩衝區的行數減少,中斷的可能性也會增加。
有幾種方法可以調整輸出緩衝。主要有:
stdbuf -o0
在使用GNU coreutils 和其他一些系統(如 FreeBSD)中的程序的情況下,關閉使用 stdio 庫的程序中的緩衝,而不更改其預設設置。您也可以使用 切換到行緩衝stdbuf -oL
。- 通過使用
unbuffer
. 某些程序可能在其他方面表現不同,例如grep
,如果其輸出是終端,則預設使用顏色。- 配置程序,例如通過傳遞
--line-buffered
給 GNU grep。讓我們再看一遍上面的程式碼片段,這一次兩邊都有行緩衝。
{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb abbbb
所以這次yes從來沒有打斷過grep,但是grep有時會打斷yes。我稍後會談到為什麼。
管道交錯
只要每個程序一次輸出一行,並且行足夠短,輸出行就會被整齊地分開。但是這條線可以工作多長時間是有限制的。管道本身有一個傳輸緩衝區。當程序輸出到管道時,數據從寫入程序複製到管道的傳輸緩衝區,然後再從管道的傳輸緩衝區復製到讀取程序。(至少在概念上——核心有時可能會將其優化為單個副本。)
如果要複製的數據多於管道傳輸緩衝區的容量,則核心一次複製一個緩衝區。如果多個程序正在寫入同一個管道,並且核心選擇的第一個程序想要寫入多個緩衝區,則不能保證核心會再次選擇相同的程序第二次。例如,如果P是緩衝區大小,
foo
想要寫入 2* P個字節並且bar
想要寫入 3 個字節,那麼一種可能的交錯是來自 的P字節foo
,然後來自 的 3 個字節bar
,以及來自 的P字節foo
。回到上面的 yes+grep 範例,在我的系統上,
yes aaaa
碰巧一次寫入了可以容納在 8192 字節緩衝區中的盡可能多的行。由於要寫入 5 個字節(4 個可列印字元和換行符),這意味著它每次寫入 8190 個字節。管道緩衝區大小為 4096 字節。因此,可以從 yes 獲取 4096 字節,然後從 grep 獲取一些輸出,然後從 yes 獲取其餘的寫入(8190 - 4096 = 4094 字節)。4096 字節為 819 行留出了空間,aaaa
並帶有一個單獨的a
. 因此,一行帶有這個單獨的行,a
然後是一個來自 grep 的寫入,給出一行帶有abbbb
.如果您想查看正在發生的事情的詳細資訊,那麼
getconf PIPE_BUF .
會告訴您系統上的管道緩衝區大小,並且您可以看到每個程序進行的系統呼叫的完整列表strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba
如何保證乾淨的線交錯
如果行長度小於管道緩衝區大小,則行緩衝保證輸出中不會有任何混合行。
如果行長可以更大,當多個程序寫入同一個管道時,就無法避免任意混合。為了確保分離,你需要讓每個程序寫入不同的管道,並使用一個程序來組合這些行。例如GNU Parallel預設執行此操作。