上週碰到一個 grep 的使用問題, 就花了點時間瞭解問題的根本.
問題是這樣的, 以下命令原本應該要每 0.5 秒輸出一次 "hello world" 的, 但卻沒有任何的輸出
$ while true; do echo hello world; sleep 0.5; done | grep h | grep o
用 strace 來觀察 grep h
, 發現有 read()
動作發生, 但沒有 write()
發生
$ while true; do echo hello world; sleep 0.5; done | strace grep h | grep o
read(0, "hello world\n", 98304) = 12
fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
read(0, "hello world\n", 98304) = 12
去掉 while
則是立即會有輸出, 因該是碰到 EOF 造成最後 flush 了
$ echo hello world | grep h | grep o
hello world
所以, 應該是 buffer 造成的. 但為什麼呢? 下載了 source code
$ apt source grep
在 grep.c
中, 配合 gdb trace 了一下, 發現其實最終都有呼叫這個 function, 不論 stdout 是什麼類型
static void
fwrite_errno (void const *ptr, size_t size, size_t nmemb)
{
if (fwrite (ptr, size, nmemb, stdout) != nmemb)
stdout_errno = errno;
}
在這個 function 中加上 fflush()
的話就能讓結果立即輸出. 確實是 buffer 造成的, 並且是 fwrite()
的原因. 在 buffer 未滿時, 就不會呼叫 syscall write()
, 所以 grep 出來的結果就全放在 buffer 中. 如果我們去掉 sleep 0.5
, 就能看到因為 buffer 滿了而輸出到 console
$ while true; do echo hello world; done | grep h | grep o
看了下 man grep
, 參數 --line-buffered
能解決這個問題, 在 grep.c
中就是在 fwrite_errno()
後確認是否有加了 --line-buffered
, 有的話就再呼叫 fflush_errno()
強制輸出, 當然, 這樣會造成效率非常的低落 (每輸出一行就 write()
一次, 很大的機會造成 context switch 到另一個 process 進行輸入的處理).