1. 程式人生 > 其它 >玩轉 SHELL 指令碼之:Shell 命令 Buffer 知多少?

玩轉 SHELL 指令碼之:Shell 命令 Buffer 知多少?

1、問題:

下午有同學問了這麼一個問題:

tail -n +$(tail -n1 /root/tmp/n) -F /root/tmp/ip.txt 2>&1| awk 'ARGIND==1{i=$0;next}{i++;if($0~/檔案已截斷/){i=0};print $1"---"i;print i >> "/root/tmp/n"}' /root/tmp/n -  

seq 10 > /root/tmp/ip.txt && tail -f /root/tmp/n

把這兩條語句分別在同一臺機器的兩個終端上執行,你會發現第二條語句的 tail 跟蹤不到結果,而第一條語句明明是有結果輸出的。

在往下細說之前,咱們先簡單介紹下第一個語句幹嘛的:

這個語句是實時 tail 一份日誌,並實現了兩個小功能:

當檔案被重寫的時候將檔案的行號置 0,並且當程序掛掉後,重啟程序時,能從上次掛掉的地方開始 tail 起,類似“斷點續傳”。

不熟悉 awk 的同學看起來估計比較費勁,沒關係,咱們簡化下場景,寫個簡單的 test case 模擬上面的語句(1):

{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"}'

你會發現確實是當螢幕輸出了 21 的時候, n 的值沒有變化,但是當整個 echo 執行完成時,n 的值卻一起變化了,輸出了 21、22。

對此,你很容易寫出另一個 test case:

while [[ $i -lt 10 ]]; do ((i++)); echo $i|awk '{print >> "/root/tmp/n"}'; sleep 2; done

那為什麼這個 case 能實時看到 n 的值在變化呢?別急,讀完本文,你自會找到答案。^ _ ^

其實語句(1)的問題在於 shell 下的一個概念引發的:buffer

寫過程式的同學應該知道 磁碟與記憶體,記憶體與CPU 的 IO 互動速度都不在一個量級上,那麼為了提高資料的存取效率,一般都會在軟體程式、硬體設計中採用 buffer 的設計,當 buffer 滿了才會請求一次 IO 操作,而不是一個字元或者一個位元組的方式請求 IO 操作,具體說來一般是互動式的會無 buffer 或者小 buffer,非互動式的操作一般 buffer 都會比較大,因為對使用者來說“實時性”要求不是那麼高了嘛~

語句 command1 | command2 大體的流程圖如下:

例如如下語句的流程圖如下:

tail -f access.log | cut -d' ' -f1 | uniq

語句(1) 的重定向就是一個典型的非互動式操作,會由於 buffer 的原因,使用者無法實時的看到日誌中資料的變化。

2、解決方案:

知道原因了,咱們可以有如下幾種方式,讓 awk 中的重定向變得無 buffer 實時輸出:

{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; fflush("")}'
{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; system("")}'
{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; close("/root/tmp/n")}'
{ echo 21;sleep 10;echo 22; }|awk '{system("echo "$0" >> /root/tmp/n")}'
{ echo 21;sleep 10;echo 22; }|awk '{print |"cat - >> /root/tmp/n"}'

關於 fflush 的說明如下:

fflush([file]) Flush any buffers associated with the open output file or pipe file. If file is missing, then standard output is flushed. If file is the null string, then all open output files and pipes have their buffers flushed.

說道這兒,有同學或許會有疑問:還有什麼辦法去驗證是 buffer 的原因呢?

其實你調大你的輸出就行了:

{ seq 5000;sleep 10;seq 1000; }|awk '{print >> "/root/tmp/n"}'

3、推而廣之

其實 linux shell 下的眾多命令都採用了 buffer 的設計,例如 grep,比如就曾經有同學問過我:

tail -f logfile | grep 'ooxx' 為什麼看不到結果呢?日誌中明明就有的呀? 等等。。。

那本文在此稍稍總結下常用命令的 buffer 問題以及應對措施:

grep (e.g. GNU version 2.5.1)

--line-buffered

sed (e.g. GNU version 4.0.6)

-u,--unbuffered

awk (GNU awk)

use the fflush() function

awk (mawk)

-W interactive

tcpdump, tethereal

-l

例如上文提到的 grep buffer 問題:

tail -f /var/log/foo | grep --line-buffered

也有專門的命令或者工具包來解決這個問題,比如 unbuffer、stdbuf,或者直接呼叫 c 語言庫禁用 buffer:

setvbuf(stdout, 0, _IONBF, 0);

4、Refer:

[1] 9.1.4 Input/Output Functions

https://www.gnu.org/software/gawk/manual/html_node/I_002fO-Functions.html

[2] What is buffering? Or, why does my command line produce no output: tail -f logfile | grep 'foo bar' | awk ...

http://mywiki.wooledge.org/BashFAQ/009

[3] How to fix stdio buffering

http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

[4] buffering in standard streams

http://www.pixelbeat.org/programming/stdio_buffering/

[5] 關於awk中通過管道執行shell後的管道關閉問題

http://hi.baidu.com/leejun_2005/item/26a5f8273e7e3555c28d5970

[6] Why does awk do full buffering when reading from a pipe

http://unix.stackexchange.com/questions/33650/why-does-awk-do-full-buffering-when-reading-from-a-pipe

[7] tailf and tail -f

http://blogread.cn/it/article/6892?f=wb