我發現找出標准流用的是什么緩沖是一件困難的事。
例如下面這個使用unix shell 管道的例子:
$ command1 | command2
下圖顯示了shell fork了兩個進程並通過一個管道將他們聯系起來。在這個連接中移動使用了三個緩沖.
內核中的緩沖區室友pipe系統函數生成的,它的大小取決於操作系統的頁大小。我們無法也沒必要控制這個緩沖區的大小,因為它會立即轉送數據(至少在linux上是這樣)。[更新:這個pipe buffer 已經變化為 circular buffers (16 x 4KiB)並且有一個新的 proposed patch 使得它的大小是動態的]
另外兩個緩沖是關於流的,為了提高效率,僅僅在第一次使用流的時候申請緩沖區空間。三個標准流(stdin, stdout, stderr)會在幾乎所有的unix GNU C程序開始執行自動被創建,新的流也可以被創建用來連接文件、套接字、管道等等。你可以通過控制緩沖策略(無緩沖,行緩沖,滿緩沖)來控制數據的讀寫方法。我使用這個程序來確定標准流的默認緩沖區的特性:
/* Output info about the default buffering parameters
* applied by libc to stdin, stdout and stderr.
* Note the info is sent to stderr, as redirecting it
* makes no difference to its buffering parameters.
* Note gnulib has fbufmode() to make this portable.
*/
#include <stdio_ext.h>
#include <unistd.h>
#include <stdlib.h>
FILE* fileno2FILE(int fileno){
switch(fileno) {
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default: return NULL;
}
}
const char* fileno2name(int fileno){
switch(fileno) {
case 0: return "stdin";
case 1: return "stdout";
case 2: return "stderr";
default: return NULL;
}
}
int main(void)
{
if (isatty(0)) {
fprintf(stderr,"Hit Ctrl-d to initialise stdin\n");
} else {
fprintf(stderr,"Initialising stdin\n");
}
char data[4096];
fread(data,sizeof(data),1,stdin);
if (isatty(1)) {
fprintf(stdout,"Initialising stdout\n");
} else {
fprintf(stdout,"Initialising stdout\n");
fprintf(stderr,"Initialising stdout\n");
}
fprintf(stderr,"Initialising stderr\n"); //redundant
int i;
for (i=0; i<3; i++) {
fprintf(stderr,"%6s: tty=%d, lb=%d, size=%d\n",
fileno2name(i),
isatty(i),
__flbf(fileno2FILE(i))?1:0,
__fbufsize(fileno2FILE(i)));
}
return EXIT_SUCCESS;
}
默認緩沖策略:
- stdin總是緩沖的
- stderr總是無緩沖的
- 如果stdout是終端的話緩沖是行緩沖的,否則是滿緩沖的。(補充一下,GNU里面定義的可交互設備,顯然終端是可交互設備)
默認緩沖大小:
- 緩沖大小只會直接影響緩沖策略
- 內核的pipe buffer 已經變化為 circular buffers (16 x 4KiB)並且有一個新的 proposed patch 使得它的大小是動態的
- 如果stdin/stdout 連接的是交互設備那么默認大小是1024,否則是4096
stdio 輸出緩沖的問題
現在來考慮一個問題:數據源的信息是間隔發送的,並且接受者希望立即收到新產生的數據。
例如,一個人想要過濾 tcpdump -l
或者 tail -f
的輸出等等(注意有一些過濾器比如sort要求一次緩存所有數據到內部,所以這里不能使用)。
考慮下面這個操作,從動態網絡日志終端數據中過濾出不一樣的IP地址:
$ tail -f access.log | cut -d' ' -f1 | uniq
問題在於,如果按照上面這個命令,我們將不能實時的看到增加的主機IP,示例圖如下:
高亮的緩沖區導致了問題的發生。由於該緩存區連接了一個管道緩沖區,他會等到數據達到4096字節后再作為一個塊傳送給uniq。注意到tail的標准輸出也有這個問題,但是tail -f
調用會自動清除緩沖區當有新的數據輸入時,所以這里不會產生影響( tcpdump -l
, grep --line-buffered
sed --unbuffered
也是這樣)。另外,由於uniq標准輸出連接的是一個可交互設備,所以當有新的一行數據到達時也會自動清除緩沖區,不會產生影響。
stdio 輸入緩沖問題
正如向stdout一樣,stdin也使用緩沖區以增加效率。
如果一個一個字節的讀入顯然會有更多控制的空間,但是這樣是不實際的。
考慮以下命令:
$ printf "one\ntwo\nthree\n" | ( sed 1q ; sed 1q ; sed 1q )
one
(譯者注:這里的q是sed流編輯器的退出命令,1q表示當輸出到達第一行結束時退出。參考The q or quit command)
正如你所見到的,第一個sed進程讀取了所有數據,導致后面的sed沒辦法讀入數據。注意僅僅將stdin緩沖區設置為行緩沖是沒有用的,因為只有當輸出緩沖區被清除的時候才會產生控制效果(譯者注,如果沒有輸出的話,第一個sed還是會”一行一行的把輸入數據讀完)。以上的sed標准輸入都是行緩沖Reading lines from stdin.通常你只能控制一個進程能否從stdin讀入數據,或者讀入特定規模的數據然后禁止讀入。以下是這樣的一個例子:
$ printf "one\ntwo\nthree\n" | ( ssh localhost printf 'zero\\n' ; cat )
zero
(譯者注:后面的cat命令用於從stdin中讀取數據輸出到屏幕,防止printf的輸出存儲在緩沖區中。)
這個遠程printf命令並不會從stdin讀取數據(譯者注:'zero\n'是參數),但是ssh client並不知道這個,所以他會讀取前面printf傳入的數據即stdin中讀取數據。為了告訴ssh遠程命令不需要讀入數據,可以加上-n
這個參數:
$ printf "one\ntwo\nthree\n" | ( ssh -n localhost printf 'zero\n' ; cat )
zero
one
two
three
常見的經歷是你想要吧ssh放在后台當你知道遠程命令不會讀取數據的時候(利於常見的圖像化程序),設置ssh client阻止讀入數據可以防止遠程應用程序停滯。你可以通過-f
參數告訴ssh忽略stdin並且fork到后台。例如:ssh -fY localhost xterm
(譯者注:-Y Enables trusted X11 forwarding
)。
stdio 緩沖控制
省略...(關鍵詞:stdbuf, BUF_X_=Y where X = 0 (stdin), 1 (stdout), 2 (stderr) )