標准輸入輸出 stdio 流緩沖 buffering in standard streams


**From : http://www.pixelbeat.org/programming/stdio_buffering/ ** **譯者:李秋豪**

我發現找出標准流用的是什么緩沖是一件困難的事。

例如下面這個使用unix shell 管道的例子:

$ command1 | command2

下圖顯示了shell fork了兩個進程並通過一個管道將他們聯系起來。在這個連接中移動使用了三個緩沖.

buffers used in simple unix shell pipeline

內核中的緩沖區室友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,示例圖如下:

buffering problem in unix shell pipeline

高亮的緩沖區導致了問題的發生。由於該緩存區連接了一個管道緩沖區,他會等到數據達到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) )


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM