大多數編程語言默認提供了i/o緩沖特性,因為這會使得輸出更加有效率。這些緩沖功能大都是默默工作“Just work out of the box”(譯者注:參考out of box.)——直到某天他們不在正常工作,“不正常工作”是說該輸出的數據不立即顯示出來。這些問題大多可以調用fflush函數解決。例如sys.stdout.flush() in Python, fflush(3) in C, or std::flush in C++。
人們經常搞不清楚緩沖的規則,於是他們頻繁的使用flush,這正是 cargo-cult programming.(譯者注:直譯是貨物崇拜編程,看看挺有意思的,RMS先使用的)。在這篇文章中我會解釋輸出流的規則,你看過后就不會再感到困惑了;)
緩沖為什么存在?
正如上面談到的,緩沖區可能導致輸出延遲,那么它為什么存在呢?
在系統底層調用這個層次,數據是用write+文件描述符寫入的,這種方法將數據寫入到文件描述符對應的一個字節緩沖中。
大多數語言有着非常快的函數調用,在C/C++這種語言中調用一個函數可能只需要幾個cpu周期,時間開銷幾乎可以忽略不計(只有在近端的情況下才會使用inline.)。然而,一個系統調用時間開銷是非常可觀的。在Linux上的一個系統調用可能會花費幾千個cpu周期並摻雜着上下文轉化.所以系統調用比用戶空間里的函數調用花的時間多得多。
緩沖存在的主要目的就是為用戶空間函數抵消調用系統函數的開銷。當函數做很多寫入操作時這非常重要——否則系統調用的時間會占程序運行時間的主要部分。
讓我們考慮一下當你使用 grep在一個文件或者stdin中搜索字段。假設你在nginx日志中找一個IP地址,而在nginx日志里一行大概有100個字符。如果不使用緩沖的話,就意味着只要grep遇到了想要的ip地址,它就會調用write()
.,而這會接連不斷的發生,並且每次寫入的字符大概都在100字節。如果使用默認緩沖4096字節的話,grep會等到緩沖區滿以后再調用write()
清除緩沖,這大概會讀入40行才發生一次(譯者注:緩沖策略是寫入流/文件的充分條件,不是必要的。緩沖區存在的意義就是使用“Stream-level I/O”時從緩沖區進行異步塊寫入/讀出,這樣可以在設備堵塞或者大量的寫入操作的時候加快效率。如果一次只寫入少量數據,內核一看沒有堵塞,“干脆”就把緩沖區的內容寫入了,反正放着也是放着。滿緩沖在gnu library c 中定義為以任意大小的塊寫入流。),所以會減少大概40倍在系統調用上花的時間。不錯!
如果grep程序向標准輸出寫入大量的數據的話,你可能都不會注意到緩沖區造成的延遲。並且如果grep只是找一個簡單的字段,通常它在輸出上花的時間會比尋找該字段畫的時間更多。但是假如grep匹配字段發生的非常緩慢,例如每十秒鍾才發生一次,那么我們可能要等400秒才能得到一個輸出!(即便在開始的時候已經匹配到幾個字段)
緩沖產生的問題
緩沖可能會在不知覺中造成一些問題,特別是在unix下使用管道的時候,例如,假設我們想要打印出在一個日志文件中匹配到的第一個字段,命令可能是這樣的:
# BAD: grep will buffer output before sending it to head
grep RAREPATTERN /var/log/mylog.txt | head -n 1
依據前面舉得例子,我們只是想要grep立即輸出它第一次匹配到的字段,但是由於緩沖區的存在,grep可能要等緩沖區滿后才輸出給head,這會花費更多的時間。
在很多情況下,如果輸出是間斷的,那么緩沖區的存在可能會嚴重影響程序的性能,更不用說提升效率了。
什么時候程序使用緩沖
There are typically three modes for buffering:一般有三種緩沖策略:
- 無緩沖(unbuffered),任何讀寫都會立刻發生(並且會產生阻塞)。
- 滿緩沖(fully-buffered),會使用固定大小的緩沖區,讀寫都是從緩沖區發生的。緩沖區不會被清除直到它被填滿。
- 行緩沖,緩沖數據直到遇見一個換行符或者達到緩沖區的固定大小。
GNU libc (glibc) 使用以下的緩沖規則:
Stream | Type | Behavior |
---|---|---|
stdin | input | line-buffered |
stdout (TTY) | output | line-buffered |
stdout (not a TTY) | output | fully-buffered |
stderr | output | unbuffered |
正如你所看到的,stdout緩沖的策略有些特別:取決於流是不是一個交互設備(tty)。理由在於,如果stdout是一個終端的話,用戶通常希望看着命令運行並等待結果輸出,因此及時輸出數據是必要的。另一方面,如果輸出流不是終端,意味着輸出可以后期輸出,因此效率更加重要(滿緩沖)。
其他的編程語言大多有着相同的規則,要么是因為這些語言是用了libc命令,或者它們遵循了相同的邏輯。
更多關於grep的例子
Grep對於緩沖來說是一個特殊的例子,因為它能夠將一個大量數據轉化為緩慢並且小規模的輸出。因此grep對於緩沖非常敏感。
****正如上面所說,如果grep的輸出是一個交互設備(例如tty),那么輸出流將會是行緩沖,如果輸出到一個文件或者一個管道,那么將會是滿緩沖的。
下面這個grep命令的輸出將會是行緩沖,因為輸出是一個交互設備(終端):
# line-buffered
grep RAREPATTERN /var/log/mylog.txt
如果stdout被重定向到一個文件而非交互設備,那么輸出將會是滿緩沖的。通常情況下這都是可行的:
# fully-buffered
grep RAREPATTERN /var/log/mylog.txt >output.txt
在一種情況下前面的例子不會顯得理想:如果你在另一個終端輸出中嘗試使用tail -f
檢測輸出文件。
假設我們想要逆序通過管道+grep匹配子段,因為grep是最后一個命令,所以它的輸出還是一個交互設備:
(譯者注:tac - concatenate and print files in reverse
)
# line-buffered
tac /var/log/mylog.txt | grep RAREPATTERN
但是如果我們想要過濾grep的輸出會怎樣?如果我們使用一個管道,這會使得grep輸出變為滿緩沖。例如:
# fully-buffered
grep RAREPATTERN /var/log/mylog.txt | cut -f1
在這種情況下,grep的輸出將變為pipe的文件描述符。管道不是ttys, 所以輸出將變為滿緩沖。
For the grep command the solution is to use the --line-buffered
option to force line-buffering:
# forced line-buffering
grep --line-buffered RAREPATTERN /var/log/mylog.txt | cut -f1
如前面提到的,你可能需要使用line-buffered參數當你重定向grep到一個文件並在另一個會話使用 tail -f
的時候。
Setbuf
如果你在寫C,你可以通過使用setbuf(3)(譯者注:推薦setvbuf,參見 C語言 流緩沖.)強制改變標准流的緩沖策略,你也可以在磁盤文件上使用這個函數,這樣你就可以在使用像fprintf
這樣的函數的時候自動是行緩沖。
Stdbuf
GNU的源代碼包中有一個 stdbuf 程序,這個程序允許你改變那些你無法控制源代碼的程序的緩沖策略,但是也有幾個限制:這個程序必須使用的是C的文件指針流即FILE*
,並且這個程序不能使用了專門的緩沖策略函數例如上面提到的Stdbuf
.
C++ IOStreams
在C++程序里經常會看到另外一個問題,很多C++程序員習慣於使用 std::endl 來輸出新的行,例如:
// Two ways to print output with a newline ending.
std::cout << "Hello, world!\n";
std::cout << "Hello, world!" << std::endl;
這兩行是不一樣的。關鍵在於std::endl
會自動強制清除輸出流緩沖,不管之前它采取的是什么緩沖策略。
// Subject to normal buffering rules.
std::cout << "Hello, world!\n";
// These are equivalent and are *always* line-buffered.
std::cout << "Hello, world!\n" << std::flush;
std::cout << "Hello, world!" << std::endl;
因此,如果你大量使用 std::endl
,那么實際上緩沖策略並沒有起作用—— std::endl
每次都在強制清除緩沖區!如果你在寫一個很注重性能的程序這可能會很重要,因為這會不經意間導致緩沖不起作用。
我的建議是,僅僅在你真的需要清除緩沖區輸出數據的時候才使用 std::endl
,如果你不認為必須清除緩沖區,那么就使用正常的輸出和一個\n
吧。