上週碰到一個 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 進行輸入的處理).