下面會(huì)涉及到一些底層的函數(shù)庫(kù)以及系統(tǒng)調(diào)用,不想看過(guò)程的直接跳到最后看結(jié)論好了。
一段代碼,通過(guò) tail -f 看打的 log,發(fā)現(xiàn)很長(zhǎng)時(shí)間都沒(méi)有輸出,然后突然一下子輸出了好多條,猜想可能跟 buffer 之類(lèi)的有關(guān)系。這個(gè)問(wèn)題其實(shí)很早就遇到過(guò),最初以為是什么 bug,直到看到自己寫(xiě)的代碼也出現(xiàn)類(lèi)似的現(xiàn)象之后才決定看看是怎么回事。
先來(lái)看看下面這一小段代碼。
$ cat demo1.py
import time, sys
for i in range(50):
sys.stdout.write("test")
time.sleep(0.2)
$ python demo1.py
testtesttesttesttesttes……ttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest$
可以看到,這堆 test 字符串是等了若干秒之后一下子輸出的。
如果我們把 sys.stdout.write("test") 改為 sys.stdout.write("testn") 即加上換行符號(hào),或者使用 print 函數(shù)來(lái)輸出,發(fā)現(xiàn)現(xiàn)象不一樣了:
$ cat demo2.py
import time, sys
for i in range(50):
sys.stdout.write("testn")
time.sleep(0.2)
$ python demo2.py
test
test
test
test
test
…
$ cat demo3.py
import time, sys
for i in range(50):
print "test"
time.sleep(0.2)
發(fā)現(xiàn)不管是 demo2 還是 demo3,屏幕上均以平均 0.2s 的頻率輸出 test 字符。
把 demo3 的 print "test" 換成 print "test",(結(jié)尾加一個(gè)半角逗號(hào))再看看是什么現(xiàn)象。
再用 python3 的 print("test") 試試,嘗試加上 end 參數(shù)比如,print("test", end="n"), print("test", end="t"),print("test", end="") 再試試有什么不同的結(jié)果。
再來(lái)看一個(gè) demo:
$ cat demo4.py
import time, sys
for i in range(50):
sys.stdout.write("test")
sys.stdout.flush()
time.sleep(0.2)
加上 sys.stdout.flush() 看看跟上面的比有什么不同的效果。
最后一個(gè),代碼是 demo3.py,但是運(yùn)行的方式不同:
$ python demo3.py > output
注意實(shí)時(shí)觀察 output 文件的大小,發(fā)現(xiàn)并沒(méi)有隨時(shí)間而增大,而是 demo3.py 運(yùn)行結(jié)束了之后才變化的。
上面就是之前遇到的一些現(xiàn)象,這里面涉及到其實(shí)是 UNIX 下面的 STDIO buffer 問(wèn)題。下面會(huì)深入現(xiàn)象揭開(kāi)本質(zhì),沒(méi)時(shí)間的看最后的結(jié)論即可。
IOS C 標(biāo)準(zhǔn)定義了一套叫標(biāo)準(zhǔn) I/O 的庫(kù),也叫 buffered I/O,這套庫(kù)被包括 UNIX 在內(nèi)的系統(tǒng)所實(shí)現(xiàn),包括我們?nèi)粘J褂玫谋姸喟l(fā)行版本。而大家熟知的 open, read, write, lseek, close 這些 I/O 系統(tǒng)調(diào)用函數(shù)則是 POSIX 定義的,他們通常稱(chēng)為 unbuffered I/O,就是為了跟標(biāo)準(zhǔn) I/O 庫(kù)作出區(qū)分。這些底層的系統(tǒng)調(diào)用函數(shù),大多都是圍繞 fd 展開(kāi),而標(biāo)準(zhǔn) I/O 則是圍繞著 STREAM 展開(kāi),標(biāo)準(zhǔn) I/O 庫(kù)其實(shí)可以理解為對(duì)系統(tǒng) I/O 函數(shù)的封裝,因?yàn)闃?biāo)準(zhǔn) I/O 庫(kù)最終還是要調(diào)用對(duì)應(yīng)的這些系統(tǒng) I/O 函數(shù),可以通過(guò) fileno(FILE *FP) 獲取到 STREAM 對(duì)應(yīng)的 fd。
為什么說(shuō)標(biāo)準(zhǔn) I/O 庫(kù)是 buffered I/O 了,因?yàn)樗麜?huì)自動(dòng)的幫你處理 buffer 分配以及 I/O chunks 的選擇,這樣就不再需要為選擇 block size 而操心了,這個(gè)在使用系統(tǒng) I/O 調(diào)用的時(shí)候無(wú)法避免,比如 read/write 都需要考慮 buffer 地址以及讀取寫(xiě)入的 buffer size,通常你需要在調(diào)用 read 時(shí)候定義一個(gè) buffer size 的宏:
# define BUFFSIZE 4096
buffered I/O 的主要目的就是為了降低 read/write 這類(lèi)的系統(tǒng)調(diào)用以及自動(dòng)的為程序分配 buffer。但是他分為了下面三種類(lèi)似的 buffering:
1. full buffer,當(dāng)標(biāo)準(zhǔn) I/O buffer 滿(mǎn)了時(shí)候發(fā)生一次 flush 操作,可以調(diào)用 fflush() 來(lái)完成,他將 buffer 里面的數(shù)據(jù) flush 到內(nèi)核緩沖區(qū)中。
2. line buffer,遇到換行符(一般就是 "n") 也就是寫(xiě)完一行的時(shí)候發(fā)生一次 flush,
3. unbuffered,有多少讀寫(xiě)多少。
Linux 一般是這樣實(shí)現(xiàn)的:
1. stderr 是 unbuffered,這會(huì)讓錯(cuò)誤信息及時(shí)的出現(xiàn)。
2. stdin/stdout stream 如果不跟終端相關(guān)聯(lián),比如 pipe,redirect,fopen 打開(kāi)的文件,則是 full buffer;如果跟終端相關(guān)聯(lián),則是 line buffer
上面這兩條規(guī)則其實(shí)就是速度跟系統(tǒng)之間的一個(gè) tradoff,很好理解。
可以通過(guò) setbuf/setvbuf 來(lái)修改 buffer 的模式,具體的使用方式 man 2,需要注意的是,這兩個(gè)函數(shù)要在 stream 打開(kāi)之后其余 I/O 操作之前調(diào)用,讓然,如果你需要做一些特殊的事情,完全可以在昨晚某些 I/O 操作之后再調(diào)用,比如下面要舉的第二個(gè) demo。setvbuf 比 setbuf 有更大的優(yōu)勢(shì),比如可以修改 buffer 的大小等等。
關(guān)于 STREM 對(duì)應(yīng)的 buffer 類(lèi)型,其大小可以通過(guò)這段代碼來(lái)做一個(gè)驗(yàn)證,比如我的機(jī)器的幾個(gè) buffer size 都是 8KB。
而 int fflush(FILE *fp) 這個(gè)函數(shù)就是解決我們上面問(wèn)題的核心了,該函數(shù)會(huì)將當(dāng)前 STREAM 中的數(shù)據(jù) flush 到內(nèi)核緩沖區(qū),如果 fp 是 NULL,則 stdout 流被 flush 一次。準(zhǔn)確的說(shuō),fflush 只能用于輸出流,而不能用于輸入流,具體的原因見(jiàn)這里。
這里的一個(gè) demo 很好的解釋了 fflush/setvbuf 做的事情,嘗試把 setvbuf 中的 size_t size 參數(shù)從原先的 1024 調(diào)小到 20 試試看。
很明顯,通過(guò)這種 buffer 的方式,把一部分的寫(xiě)先 buffer 起來(lái)然后統(tǒng)一調(diào)用一次系統(tǒng)調(diào)用,可以大量的減少 user space 跟 kernel space 之間的切換。
可能會(huì)有人想到 fsync 這個(gè)系統(tǒng)調(diào)用,它跟 fflush 做的事情好像是一樣的,其實(shí)仔細(xì)辨別的,二者做的事情根本不在一個(gè)平面上。
fflush(FILE *stream) 作用的是 FILE*,對(duì)于 stdout 來(lái)說(shuō),他是將標(biāo)準(zhǔn) IO 流的 buffer 從用戶(hù)空間 flush 到內(nèi)核緩存中。這也是調(diào)用 exit 要做的事情。
fsync(int fd) 控制的是何時(shí)將 data&metadata 從內(nèi)核緩沖區(qū) flush 到磁盤(pán)中,他的傳入?yún)?shù)是一個(gè) fd。對(duì) fsync 來(lái)說(shuō),F(xiàn)ILE* 是透明的也就是所他并不知道 FILE* 的存在,一個(gè)是在 user space 一個(gè)是在 kernel space。
所以,如果我們不想有 full/line buffer 而是盡可能快的獲取到輸出流的話(huà),就需要通過(guò)調(diào)用 fflush(stdout) 指明。
上面解釋的僅僅是 C 的,對(duì)于 Python 而言,底層調(diào)用的東西幾乎一樣,Python 它自己通過(guò) C 實(shí)現(xiàn)了 fflush(),具體的代碼可以看這里。其實(shí)不單單是 fflush,不少包括 read/write 在內(nèi)的底層調(diào)用 Python 都是用 C 實(shí)現(xiàn)的。
對(duì)用到 Python 的 fflush 則是 sys.stdout.flush()。
不管是 fflush() 還是 sys.stdout.flush(),都需要對(duì)立即返回的 stdout 手動(dòng)的調(diào)用,比較麻煩。所幸的,上面提到的 setvbuf 就可以直接幫我們做這件事,在 stream 打開(kāi)后調(diào)用 setvbuf() 即可,其 mode 參數(shù)可以選擇下面三種:
1. _IOLBF,line buffer
2. _IOFBF, full buffer
3. _IONBF,no buffer
要完全禁用的話(huà)按照下面這種方式調(diào)用:
setvbuf(stdout, 0, _INNBF, 0);
對(duì)應(yīng)到 python 的,至少還有下面的幾種方式可以避免此類(lèi)問(wèn)題:
1. 直接關(guān)閉 stdout 的 buffer,類(lèi)似 setvbuf:
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
2. 有個(gè)比較 ugly 的方式,把輸出流改到 stderr 上,這樣不管什么時(shí)候都能保證盡快的輸出。
3. 直接腳本的時(shí)候加上 -u 參數(shù)。但是需要注意下,xreadlines(), readlines() 包含一個(gè)內(nèi)部 buffer,不受 -u 影響,因此如果通過(guò) stdin 來(lái)遍歷會(huì)出現(xiàn)問(wèn)題,可以看看這兩個(gè)連接提供的內(nèi)容(1, 2)。
4. 將其 stream 關(guān)聯(lián)到 pseudo terminal(pty) 上,script 可以做這事情的:
script -q -c "command1" /dev/null | command2
或者通過(guò) socat 這個(gè)工具實(shí)現(xiàn),
再來(lái)看個(gè)跟 pipe 相關(guān)的問(wèn)題, 這個(gè)命令常常回車(chē)之后沒(méi)有反應(yīng):
$ tail -f logfile | grep "foo" | awk {print $1}
tail 的 stdout buffer 默認(rèn)會(huì)做 full buffer,由于加上了 -f,表示會(huì)調(diào)用 fflush() 對(duì)輸出流進(jìn)行 flush,所以 tail -f 這部分沒(méi)什么問(wèn)題。關(guān)鍵在 grep 的 stdout buffer,因此它存在一個(gè) 8KB stdout buffer,要等該 buffer 滿(mǎn)了之后 awk 才會(huì)接收到數(shù)據(jù)。awk 的 stdout buffer 跟終端相關(guān)聯(lián),所有默認(rèn)是 line buffer。怎么解決這個(gè)問(wèn)題了,其實(shí) grep 提供了 –line-buffered 這個(gè)選項(xiàng)來(lái)做 line buffer,這會(huì)比 full buffer 快的多:
tail -f logfile | grep –line-buffered "foo" | awk {print $1}
除了 grep,sed 有對(duì)應(yīng)的 -u(–unbuffered),awk(我們默認(rèn)的是 mawk) 有 -W 選項(xiàng),tcpdump 有 -l 選項(xiàng)來(lái)將 full buffer 變成 line 或者 no buffer。
不僅僅是 stdin/stdout/stderr 有 buffer 問(wèn)題,pipe 同樣有 buffer 的問(wèn)題,相關(guān)的文檔可以看這里(1, 2)。
上面的方式都涉及到了具體的函數(shù)調(diào)用,修改參數(shù)的不具有普遍原理,對(duì)于普通用戶(hù)來(lái)說(shuō),不大可能這么操作。其實(shí) coreutils 已經(jīng)給我們提供了一個(gè)叫 stdbuf 的工具。expect 還提供了一個(gè)叫 unbuffer 的工具,通過(guò)它可以將輸出流的 buffer 給禁止掉,另外,在 pipe 的應(yīng)用中,可能會(huì)出現(xiàn)一些問(wèn)題,具體的 man 一下。因此,上面的問(wèn)題可以更具有普遍性:
tail -f logfile | stdbuf -oL grep "foo" | awk {print $1}
看到這里最上面的幾個(gè)問(wèn)題現(xiàn)在應(yīng)該非常容易回答了。
ref:
更多信息請(qǐng)查看IT技術(shù)專(zhuān)欄