全部的Unix
工具都使用文件描述符0
、1
和2
。以下圖所示,標準輸入文件的描述符是0
,標準輸出的文件描述符是1
,標準錯誤輸出的文件描述符則是2
。Unix
假設文件描述符0
、1
和2
都已經被打開,能夠分別進行讀、寫和寫的操做。git
經過使用輸出重定向標誌,命令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
,以下圖所示。數組
文件描述符是一個數組的索引號。每一個進程都有其打開的一組文件,這些打開的文件被保持在一個數組中。文件描述符即爲某文件在此數組中的索引。而且,當打開文件時,爲此文件安排的文件描述符老是此數組中最低可用位置的索引。函數
考慮如何將標準輸入重定向以致能夠從文件中讀取數據。更加精確的說,進程並非從文件讀數據,而是從文件描述符讀取數據。若是將文件描述符0
重定向到一個文件,那麼此文件就成爲標準輸入的源。工具
第一種放方法是close-then-open
策略,具體步驟以下:oop
開始時,系統中採用的是典型的設置,即三種標準流是被鏈接到終端設備上的。輸入的數據流通過文件描述符0
而輸出的流通過文件描述符1
和2
。學習
接下來,調用close(0)
,將標準輸入與終端設備的鏈接切斷。
最後,使用open(filename, O_RDONLY)
打開一個想鏈接到stdin
上的文件。當前的最低可用文件描述符是0
,所以所打開的文件將被鏈接到標準輸入上。任何從標準輸入讀取數據的函數都將今後文件中讀取數據。
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
。
當輸入who>userlist
時,shell
運行who
程序,並將who
的標準輸出重定向到名爲userlist
的文件上。shell
實現該重定向的關鍵之處在於fork
和exec
之間的時間間隙。在fork
執行完後,子進程仍然在運行父進程也就是shell
程序,並準備執行exec
。exec
將替換進程中運行的程序,可是它不會改變進程的屬性和進程中全部的鏈接。也就是說,在運行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
的時候,它的子進程也獲得了這兩個連向管道的鏈接。父進程和子進程均可以將數據寫到管道的寫數據端口,並從讀數據端口將數據讀出。可是當一個進程讀,而另外一個進程寫的時候,管道的使用效率是最高的。程序pipedemo2.c
說明了如何將pipe
和fork
結合起來,建立一對經過管道來通訊的進程,核心代碼以下:
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
。
建立文件描述符的系統調用老是使用最低可用文件描述符號。
重定向標準輸入、標準輸出和錯誤輸出意味着改變文件描述符0
、1
和2
的鏈接。
管道是內核中的一個數據隊列,其每一端鏈接一個文件描述符。程序經過pipe
系統調用來建立管道。
當父進程調用fork
的時候,管道的兩端都被複制到子進程中。
只有有共同父進程的進程之間才能夠用管道鏈接。
相關代碼見Github。