输出流缓冲的意义 何时缓冲 Stdout Buffering
From : https://eklitzke.org/stdout-buffering
譯者:李秋豪
大多數(shù)編程語言默認(rèn)提供了i/o緩沖特性,因?yàn)檫@會使得輸出更加有效率。這些緩沖功能大都是默默工作“Just work out of the box”(譯者注:參考o(jì)ut of box.)——直到某天他們不在正常工作,“不正常工作”是說該輸出的數(shù)據(jù)不立即顯示出來。這些問題大多可以調(diào)用fflush函數(shù)解決。例如sys.stdout.flush() in Python, fflush(3) in C, or std::flush in C++。
人們經(jīng)常搞不清楚緩沖的規(guī)則,于是他們頻繁的使用flush,這正是 cargo-cult programming.(譯者注:直譯是貨物崇拜編程,看看挺有意思的,RMS先使用的)。在這篇文章中我會解釋輸出流的規(guī)則,你看過后就不會再感到困惑了;)
緩沖為什么存在?
正如上面談到的,緩沖區(qū)可能導(dǎo)致輸出延遲,那么它為什么存在呢?
在系統(tǒng)底層調(diào)用這個層次,數(shù)據(jù)是用write+文件描述符寫入的,這種方法將數(shù)據(jù)寫入到文件描述符對應(yīng)的一個字節(jié)緩沖中。
大多數(shù)語言有著非??斓暮瘮?shù)調(diào)用,在C/C++這種語言中調(diào)用一個函數(shù)可能只需要幾個cpu周期,時間開銷幾乎可以忽略不計(jì)(只有在近端的情況下才會使用inline.)。然而,一個系統(tǒng)調(diào)用時間開銷是非??捎^的。在Linux上的一個系統(tǒng)調(diào)用可能會花費(fèi)幾千個cpu周期并摻雜著上下文轉(zhuǎn)化.所以系統(tǒng)調(diào)用比用戶空間里的函數(shù)調(diào)用花的時間多得多。
緩沖存在的主要目的就是為用戶空間函數(shù)抵消調(diào)用系統(tǒng)函數(shù)的開銷。當(dāng)函數(shù)做很多寫入操作時這非常重要——否則系統(tǒng)調(diào)用的時間會占程序運(yùn)行時間的主要部分。
讓我們考慮一下當(dāng)你使用 grep在一個文件或者stdin中搜索字段。假設(shè)你在nginx日志中找一個IP地址,而在nginx日志里一行大概有100個字符。如果不使用緩沖的話,就意味著只要grep遇到了想要的ip地址,它就會調(diào)用write().,而這會接連不斷的發(fā)生,并且每次寫入的字符大概都在100字節(jié)。如果使用默認(rèn)緩沖4096字節(jié)的話,grep會等到緩沖區(qū)滿以后再調(diào)用write()清除緩沖,這大概會讀入40行才發(fā)生一次(譯者注:緩沖策略是寫入流/文件的充分條件,不是必要的。緩沖區(qū)存在的意義就是使用“Stream-level I/O”時從緩沖區(qū)進(jìn)行異步塊寫入/讀出,這樣可以在設(shè)備堵塞或者大量的寫入操作的時候加快效率。如果一次只寫入少量數(shù)據(jù),內(nèi)核一看沒有堵塞,“干脆”就把緩沖區(qū)的內(nèi)容寫入了,反正放著也是放著。滿緩沖在gnu library c 中定義為以任意大小的塊寫入流。),所以會減少大概40倍在系統(tǒng)調(diào)用上花的時間。不錯!
如果grep程序向標(biāo)準(zhǔn)輸出寫入大量的數(shù)據(jù)的話,你可能都不會注意到緩沖區(qū)造成的延遲。并且如果grep只是找一個簡單的字段,通常它在輸出上花的時間會比尋找該字段畫的時間更多。但是假如grep匹配字段發(fā)生的非常緩慢,例如每十秒鐘才發(fā)生一次,那么我們可能要等400秒才能得到一個輸出!(即便在開始的時候已經(jīng)匹配到幾個字段)
緩沖產(chǎn)生的問題
緩沖可能會在不知覺中造成一些問題,特別是在unix下使用管道的時候,例如,假設(shè)我們想要打印出在一個日志文件中匹配到的第一個字段,命令可能是這樣的:
# BAD: grep will buffer output before sending it to head grep RAREPATTERN /var/log/mylog.txt | head -n 1依據(jù)前面舉得例子,我們只是想要grep立即輸出它第一次匹配到的字段,但是由于緩沖區(qū)的存在,grep可能要等緩沖區(qū)滿后才輸出給head,這會花費(fèi)更多的時間。
在很多情況下,如果輸出是間斷的,那么緩沖區(qū)的存在可能會嚴(yán)重影響程序的性能,更不用說提升效率了。
什么時候程序使用緩沖
There are typically three modes for buffering:一般有三種緩沖策略:
- 無緩沖(unbuffered),任何讀寫都會立刻發(fā)生(并且會產(chǎn)生阻塞)。
- 滿緩沖(fully-buffered),會使用固定大小的緩沖區(qū),讀寫都是從緩沖區(qū)發(fā)生的。緩沖區(qū)不會被清除直到它被填滿。
- 行緩沖,緩沖數(shù)據(jù)直到遇見一個換行符或者達(dá)到緩沖區(qū)的固定大小。
GNU libc (glibc) 使用以下的緩沖規(guī)則:
| stdin | input | line-buffered |
| stdout (TTY) | output | line-buffered |
| stdout (not a TTY) | output | fully-buffered |
| stderr | output | unbuffered |
正如你所看到的,stdout緩沖的策略有些特別:取決于流是不是一個交互設(shè)備(tty)。理由在于,如果stdout是一個終端的話,用戶通常希望看著命令運(yùn)行并等待結(jié)果輸出,因此及時輸出數(shù)據(jù)是必要的。另一方面,如果輸出流不是終端,意味著輸出可以后期輸出,因此效率更加重要(滿緩沖)。
其他的編程語言大多有著相同的規(guī)則,要么是因?yàn)檫@些語言是用了libc命令,或者它們遵循了相同的邏輯。
更多關(guān)于grep的例子
Grep對于緩沖來說是一個特殊的例子,因?yàn)樗軌驅(qū)⒁粋€大量數(shù)據(jù)轉(zhuǎn)化為緩慢并且小規(guī)模的輸出。因此grep對于緩沖非常敏感。
****正如上面所說,如果grep的輸出是一個交互設(shè)備(例如tty),那么輸出流將會是行緩沖,如果輸出到一個文件或者一個管道,那么將會是滿緩沖的。
下面這個grep命令的輸出將會是行緩沖,因?yàn)檩敵鍪且粋€交互設(shè)備(終端):
# line-buffered grep RAREPATTERN /var/log/mylog.txt如果stdout被重定向到一個文件而非交互設(shè)備,那么輸出將會是滿緩沖的。通常情況下這都是可行的:
# fully-buffered grep RAREPATTERN /var/log/mylog.txt >output.txt在一種情況下前面的例子不會顯得理想:如果你在另一個終端輸出中嘗試使用tail -f檢測輸出文件。
假設(shè)我們想要逆序通過管道+grep匹配子段,因?yàn)間rep是最后一個命令,所以它的輸出還是一個交互設(shè)備:
(譯者注:tac - concatenate and print files in reverse)
# line-buffered tac /var/log/mylog.txt | grep RAREPATTERN但是如果我們想要過濾grep的輸出會怎樣?如果我們使用一個管道,這會使得grep輸出變?yōu)闈M緩沖。例如:
# fully-buffered grep RAREPATTERN /var/log/mylog.txt | cut -f1在這種情況下,grep的輸出將變?yōu)閜ipe的文件描述符。管道不是ttys, 所以輸出將變?yōu)闈M緩沖。
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參數(shù)當(dāng)你重定向grep到一個文件并在另一個會話使用 tail -f的時候。
Setbuf
如果你在寫C,你可以通過使用setbuf(3)(譯者注:推薦setvbuf,參見 C語言 流緩沖.)強(qiáng)制改變標(biāo)準(zhǔn)流的緩沖策略,你也可以在磁盤文件上使用這個函數(shù),這樣你就可以在使用像fprintf這樣的函數(shù)的時候自動是行緩沖。
Stdbuf
GNU的源代碼包中有一個 stdbuf 程序,這個程序允許你改變那些你無法控制源代碼的程序的緩沖策略,但是也有幾個限制:這個程序必須使用的是C的文件指針流即FILE* ,并且這個程序不能使用了專門的緩沖策略函數(shù)例如上面提到的Stdbuf.
C++ IOStreams
在C++程序里經(jīng)常會看到另外一個問題,很多C++程序員習(xí)慣于使用 std::endl 來輸出新的行,例如:
// Two ways to print output with a newline ending. std::cout << "Hello, world!\n"; std::cout << "Hello, world!" << std::endl;這兩行是不一樣的。關(guān)鍵在于std::endl會自動強(qiáng)制清除輸出流緩沖,不管之前它采取的是什么緩沖策略。
// 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 ,那么實(shí)際上緩沖策略并沒有起作用—— std::endl 每次都在強(qiáng)制清除緩沖區(qū)!如果你在寫一個很注重性能的程序這可能會很重要,因?yàn)檫@會不經(jīng)意間導(dǎo)致緩沖不起作用。
我的建議是,僅僅在你真的需要清除緩沖區(qū)輸出數(shù)據(jù)的時候才使用 std::endl ,如果你不認(rèn)為必須清除緩沖區(qū),那么就使用正常的輸出和一個\n吧。
轉(zhuǎn)載于:https://www.cnblogs.com/liqiuhao/p/7669074.html
總結(jié)
以上是生活随笔為你收集整理的输出流缓冲的意义 何时缓冲 Stdout Buffering的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言笔记:格式化输入输出(fprint
- 下一篇: 某法院HP-P4500存储数据恢复案例