I/O重定向和管道

標準I/O與重定向的若干概念

3個標準文件描述符

全部的Unix工具都使用文件描述符012。以下圖所示,標準輸入文件的描述符是0,標準輸出的文件描述符是1,標準錯誤輸出的文件描述符則是2Unix假設文件描述符012都已經被打開,能夠分別進行讀、寫和寫的操做。git

圖片描述

重定向I/O的是shell而不是程序

經過使用輸出重定向標誌,命令cmd>filename告訴shell將文件描述符1定位到文件。因而shell就將文件描述符與指定的文件鏈接起來。程序持續不斷地將數據寫到文件描述符1中,根本沒有意識到數據的目的地已經改變了。listargs.c展現了程序甚至沒有看到命令行中的重定向符號。github

#include <stdio.h>

int main(int ac, char* av[]) {
    int i;
    printf("Number of args: %d, Args are: \n", ac);
    for(i = 0; i < ac; i++) {
        printf("args[%d] %s\n", i, av[i]);
    }
    fprintf(stderr, "This message is sent to stderr.\n");
}

程序listargs將命令行參數打印到標準輸出。注意listargs並無打印出重定向符號和文件名。shell

圖片描述

如上圖所示驗證了關於shell輸出重定向的一些重要概念。編程

  • shell並不將重定向標記和文件名傳遞給程序。api

  • 重定向能夠出如今命令行中的任何地方,而且在重定向標識符周圍並不須要空格來區分。例如上圖命令./listargs testing >xyz one two 2>oops也能夠寫成./listargs >xyz testing one two 2>oops,以下圖所示。數組

圖片描述

最低可用文件描述符(Lowest-Available-fd)原則

文件描述符是一個數組的索引號。每一個進程都有其打開的一組文件,這些打開的文件被保持在一個數組中。文件描述符即爲某文件在此數組中的索引。而且,當打開文件時,爲此文件安排的文件描述符老是此數組中最低可用位置的索引。函數

將stdin重定向到文件

考慮如何將標準輸入重定向以致能夠從文件中讀取數據。更加精確的說,進程並非從文件讀數據,而是從文件描述符讀取數據。若是將文件描述符0重定向到一個文件,那麼此文件就成爲標準輸入的源。工具

方法1:close-then-open

第一種放方法是close-then-open策略,具體步驟以下:oop

  • 開始時,系統中採用的是典型的設置,即三種標準流是被鏈接到終端設備上的。輸入的數據流通過文件描述符0而輸出的流通過文件描述符12學習

  • 接下來,調用close(0),將標準輸入與終端設備的鏈接切斷。

  • 最後,使用open(filename, O_RDONLY)打開一個想鏈接到stdin上的文件。當前的最低可用文件描述符是0,所以所打開的文件將被鏈接到標準輸入上。任何從標準輸入讀取數據的函數都將今後文件中讀取數據。

方法2:open-close-dup-close

Unix系統調用dup創建指向已經存在的文件描述符的第二個鏈接,這種方法須要4個步驟。

  • open(file),打開stdin將要重定向的文件。這個調用返回一個文件描述符fd,這個描述符並非0,由於0在當前已經被打開了。

  • close(0),將文件描述符0關閉,如今文件描述符0已經空閒了。

  • dup(fd),系統調用dup(fd)將文件描述符fd作了一個複製。此處複製使用最低可用的文件描述符號。所以得到的文件描述符是0。這樣,就將磁盤文件與文件描述符0鏈接在一塊兒了。

  • close(fd),使用close(fd)來關閉原始鏈接,只留下文件描述符0的鏈接。

dup在學習管道的時候很是重要,一個簡單一點的方案是將close(0)和dup(fd)結合在一塊兒做爲一個單獨的系統調用dup2

重定向I/O:who>userlist

當輸入who>userlist時,shell運行who程序,並將who的標準輸出重定向到名爲userlist的文件上。shell實現該重定向的關鍵之處在於forkexec之間的時間間隙。在fork執行完後,子進程仍然在運行父進程也就是shell程序,並準備執行execexec將替換進程中運行的程序,可是它不會改變進程的屬性和進程中全部的鏈接。也就是說,在運行exec以後,進程的用戶ID不會改變,其優先級也不會改變,而且其文件描述符也和運行exec以前同樣。所以,利用這個原則來實現重定向標準輸出。

此時who就是子進程要執行的命令,當執行fork前,父進程的文件描述符1指向終端。當執行fork以後,子進程的文件描述符也喜歡指向終端,此時,子進程嘗試執行close(1)close(1)以後,文件描述符1成爲最低未用文件描述符,子進程如今再執行creat(userlist, mode)打開文件userlist,文件描述符1被鏈接到文件userlist。所以,子進程的標準輸出被重定向到文件userlist,子進程而後調用exec執行who

子進程執行了who程序,因而子進程中的代碼和數據都被who程序的代碼和數據所替換了,然而文件描述符被保留下來。由於打開的文件並不是是程序的代碼也不是數據,它們屬於進程的屬性,所以exec調用並不改變它們。

管道編程

管道是內核中一個單向的數據通道,管道有一個讀取端和一個寫入端,能夠用來鏈接一個進程的輸出和另外一個進程的輸入。

建立管道

使用系統調用result = pipe(int array[2])來建立管道,並將其兩端鏈接到兩個文件描述符。以下圖所示,array[0]爲讀取數據端的文件描述符,而array[1]則爲寫數據端的文件描述符。相似與open調用,pipe調用也使用最低可用文件描述符。

圖片描述

程序pipedemo.c展現瞭如何建立管道並使用管道向本身發送數據。核心代碼以下:

int len, i, apipe[2];
    char buf[BUFSIZ];

    if(pipe(apipe) == -1) {
        perror("could not make pipe.");
        exit(1);
    }

    printf("Got a pipe! It is file descriptors: {%d %d}\n", apipe[0], apipe[1]);

    while(fgets(buf, BUFSIZ, stdin)) {
        len = strlen(buf);
        if(write(apipe[1], buf, len) != len) {
            perror("writing to pipe.");
            break;
        }
        for(i = 0; i < len; i++) {
            buf[i] = 'X';
        }
        len = read(apipe[0], buf, BUFSIZ);
        if(len == -1) {
            perror("reading from pipe.");
            break;
        }
        if(write(1, buf, len) != len) {
            perror("writing to stdout");
            break;
        }
    }

數據流從鍵盤到進程,從進程到管道,再從管道到進程以及從進程回到終端。

使用fork來共享管道

當進程建立一個管道以後,該進程就有了連向管道兩端的鏈接。當這個進程調用fork的時候,它的子進程也獲得了這兩個連向管道的鏈接。父進程和子進程均可以將數據寫到管道的寫數據端口,並從讀數據端口將數據讀出。可是當一個進程讀,而另外一個進程寫的時候,管道的使用效率是最高的。程序pipedemo2.c說明了如何將pipefork結合起來,建立一對經過管道來通訊的進程,核心代碼以下:

int pipefd[2];
    int len;
    char buf[BUFSIZ];
    int read_len;

    if(pipe(pipefd) == -1) {
        oops("cannot get a pipe", 1);
    }

    switch(fork()) {
        case -1:
            oops("cannot fork", 2);
        /*子進程*/
        case 0:
            len = strlen(CHILD_MESS);
            while(1) {
                if(write(pipefd[1], CHILD_MESS, len) != len) {
                    oops("write", 3);
                }
                sleep(5);
            }
        /*父進程*/
        default:
            len = strlen(PAR_MESS);
            while(1) {
                if(write(pipefd[1], PAR_MESS, len) != len) {
                    oops("write", 4);
                } 
                sleep(1);
                read_len = read(pipefd[0], buf, BUFSIZ);
                if(read_len <= 0) {
                    break;
                }
                write(1, buf, read_len);
            }
    }

技術細節

  • 從管道中讀取數據

    • 當進程試圖從管道讀取數據時,進程被掛起直到數據被寫進管道。

    • 當全部的寫進程關閉了管道的寫數據端時,試圖從管道中讀取數據的調用會返回0,這意味這文件的結束。

  • 向管道中寫數據

    • 寫入數據阻塞直到管道有空間去容納新的數據。

    • 若是全部的讀進程都已關閉了管道的讀數據端,那麼對管道的寫入調用將會執行失敗。

總結

  • Unix默認從文件描述符0讀取數據,寫數據到文件描述符1,將錯誤信息輸出到文件描述符2

  • 建立文件描述符的系統調用老是使用最低可用文件描述符號。

  • 重定向標準輸入、標準輸出和錯誤輸出意味着改變文件描述符012的鏈接。

  • 管道是內核中的一個數據隊列,其每一端鏈接一個文件描述符。程序經過pipe系統調用來建立管道。

  • 當父進程調用fork的時候,管道的兩端都被複制到子進程中。

  • 只有有共同父進程的進程之間才能夠用管道鏈接。

代碼

相關代碼見Github

參考

相關文章
相關標籤/搜索