標准I/O與重定向的若干概念
3個標准文件描述符
所有的Unix工具都使用文件描述符0、1和2。如下圖所示,標准輸入文件的描述符是0,標准輸出的文件描述符是1,標准錯誤輸出的文件描述符則是2。Unix假設文件描述符0、1和2都已經被打開,可以分別進行讀、寫和寫的操作。
重定向I/O的是shell而不是程序
通過使用輸出重定向標志,命令cmd>filename告訴shell將文件描述符1定位到文件。於是shell就將文件描述符與指定的文件連接起來。程序持續不斷地將數據寫到文件描述符1中,根本沒有意識到數據的目的地已經改變了。listargs.c展示了程序甚至沒有看到命令行中的重定向符號。
- #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輸出重定向的一些重要概念。
最低可用文件描述符(Lowest-Available-fd)原則
文件描述符是一個數組的索引號。每個進程都有其打開的一組文件,這些打開的文件被保持在一個數組中。文件描述符即為某文件在此數組中的索引。並且,當打開文件時,為此文件安排的文件描述符總是此數組中最低可用位置的索引。
將stdin重定向到文件
考慮如何將標准輸入重定向以至可以從文件中讀取數據。更加精確的說,進程並不是從文件讀數據,而是從文件描述符讀取數據。如果將文件描述符0重定向到一個文件,那麼此文件就成為標准輸入的源。
方法1:close-then-open
第一種放方法是close-then-open策略,具體步驟如下:
方法2:open-close-dup-close
Unix系統調用dup建立指向已經存在的文件描述符的第二個連接,這種方法需要4個步驟。
dup在學習管道的時候非常重要,一個簡單一點的方案是將close(0)和dup(fd)結合在一起作為一個單獨的系統調用dup2。
重定向I/O:who>userlist
當輸入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來共享管道
當進程創建一個管道之後,該進程就有了連向管道兩端的連接。當這個進程調用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);
- }
- }
技術細節
總結
代碼
相關代碼見Github。
參考