linux管道pipe詳解

linux管道pipe詳解

http://blog.csdn.net/oguro/article/details/53841949


管道

管道的概念:

管道是一種最基本的IPC機制,作用於有血緣關係的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:

1. 其本質是一個僞文件(實爲內核緩衝區)

2. 由兩個文件描述符引用,一個表示讀端,一個表示寫端。

3. 規定數據從管道的寫端流入管道,從讀端流出。

管道的原理: 管道實爲內核使用環形隊列機制,藉助內核緩衝區(4k)實現。

管道的侷限性:

① 數據自己讀不能自己寫。

② 數據一旦被讀走,便不在管道中存在,不可反覆讀取。

③ 由於管道採用半雙工通信方式。因此,數據只能在一個方向上流動。

④ 只能在有公共祖先的進程間使用管道。

常見的通信方式有,單工通信、半雙工通信、全雙工通信。

pipe函數

創建管道

    int pipe(int pipefd[2]); 成功:0;失敗:-1,設置errno

函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] → r; fd[1] → w,就像0對應標準輸入,1對應標準輸出一樣。向管道文件讀寫數據其實是在讀寫內核緩衝區。

管道創建成功以後,創建該管道的進程(父進程)同時掌握着管道的讀端和寫端。如何實現父子進程間通信呢?通常可以採用如下步驟:

 

1. 父進程調用pipe函數創建管道,得到兩個文件描述符fd[0]、fd[1]指向管道的讀端和寫端。

2. 父進程調用fork創建子進程,那麼子進程也有兩個文件描述符指向同一管道。

3. 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以向管道中寫入數據,子進程將管道中的數據讀出。由於管道是利用環形隊列實現的,數據從寫端流入管道,從讀端流出,這樣就實現了進程間通信。

    練習:父子進程使用管道通信,父寫入字符串,子進程讀出並,打印到屏幕。 【pipe.c】

思考:爲甚麼,程序中沒有使用sleep函數,但依然能保證子進程運行時一定會讀到數據呢?

  1. #include <unistd.h>  
  2. #include <string.h>  
  3. #include <stdlib.h>  
  4. #include <stdio.h>  
  5. #include <sys/wait.h>  
  6.   
  7. void sys_err(const char *str)  
  8. {  
  9.     perror(str);  
  10.     exit(1);  
  11. }  
  12.   
  13. int main(void)  
  14. {  
  15.     pid_t pid;  
  16.     char buf[1024];  
  17.     int fd[2];  
  18.     char *p = "test for pipe\n";  
  19.       
  20.    if (pipe(fd) == -1)   
  21.        sys_err("pipe");  
  22.   
  23.    pid = fork();  
  24.    if (pid < 0) {  
  25.        sys_err("fork err");  
  26.    } else if (pid == 0) {  
  27.         close(fd[1]);  
  28.         int len = read(fd[0], buf, sizeof(buf));  
  29.         write(STDOUT_FILENO, buf, len);  
  30.         close(fd[0]);  
  31.    } else {  
  32.        close(fd[0]);  
  33.        write(fd[1], p, strlen(p));  
  34.        wait(NULL);  
  35.        close(fd[1]);  
  36.    }  
  37.       
  38.     return 0;  
  39. }  


管道的讀寫行爲

    使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置O_NONBLOCK標誌):

1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數爲0),而仍然有進程從管道的讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會返回0,就像讀到文件末尾一樣。

2. 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大於0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會阻塞,直到管道中有數據可讀了纔讀取數據並返回。

3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數爲0),這時有進程向管道的寫端write,那麼該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。

4. 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大於0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據並返回。

總結:

① 讀管道: 1. 管道中有數據,read返回實際讀到的字節數。

2. 管道中無數據:

(1) 管道寫端被全部關閉,read返回0 (好像讀到文件結尾)

  (2) 寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)

    ② 寫管道: 1. 管道讀端全部被關閉, 進程異常終止(也可使用捕捉SIGPIPE信號,使進程不終止)

2. 管道讀端沒有全部關閉:

(1) 管道已滿,write阻塞。

(2) 管道未滿,write將數據寫入,並返回實際寫入的字節數。

    練習:使用管道實現父子進程間通信,完成:ls | wc –l。假定父進程實現ls,子進程實現wc。

ls命令正常會將結果集寫出到stdout,但現在會寫入管道的寫端;wc –l 正常應該從stdin讀取數據,但此時會從管道的讀端讀。      【pipe1.c】

  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <sys/wait.h>  
  4.   
  5. int main(void)  
  6. {  
  7.     pid_t pid;  
  8.     int fd[2];  
  9.   
  10.     pipe(fd);  
  11.     pid = fork();  
  12.   
  13.     if (pid == 0) {  //child  
  14.         close(fd[1]);                   //子進程從管道中讀數據,關閉寫端  
  15.         dup2(fd[0], STDIN_FILENO);      //讓wc從管道中讀取數據  
  16.         execlp("wc""wc""-l", NULL); //wc命令默認從標準讀入取數據  
  17.   
  18.     } else {  
  19.   
  20.         close(fd[0]);   //父進程向管道中寫數據,關閉讀端  
  21.         dup2(fd[1], STDOUT_FILENO);     //將ls的結果寫入管道中  
  22.         execlp("ls""ls", NULL);       //ls輸出結果默認對應屏幕  
  23.     }  
  24.   
  25.     return 0;  
  26. }  
  27.   
  28.   
  29.   
  30.   
  31.   
  32. /* 
  33.  *  程序不時的會出現先打印$提示符,再出程序運行結果的現象。 
  34.  *  這是因爲:父進程執行ls命令,將輸出結果給通過管道傳遞給 
  35.  *  子進程去執行wc命令,這時父進程若先於子進程打印wc運行結果 
  36.  *  之前被shell使用wait函數成功回收,shell就會先於子進程打印 
  37.  *  wc運行結果之前打印$提示符。 
  38.  *  解決方法:讓子進程執行ls,父進程執行wc命令。或者在兄弟進程間完成。 
  39.  */  


程序執行,發現程序執行結束,shell還在阻塞等待用戶輸入。這是因爲,shell → fork → ./pipe1, 程序pipe1的子進程將stdin重定向給管道,父進程執行的ls會將結果集通過管道寫給子進程。若父進程在子進程打印wc的結果到屏幕之前被shell調用wait回收,shell就會先輸出$提示符。

    練習:使用管道實現兄弟進程間通信。 兄:ls  弟: wc -l  父:等待回收子進程。

要求,使用“循環創建N個子進程”模型創建兄弟進程,使用循環因子i標示。注意管道讀寫行爲。 【pipe2.c】

  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <sys/wait.h>  
  4.   
  5. int main(void)  
  6. {  
  7.     pid_t pid;  
  8.     int fd[2], i;  
  9.       
  10.     pipe(fd);  
  11.   
  12.     for (i = 0; i < 2; i++) {  
  13.         if((pid = fork()) == 0) {  
  14.             break;  
  15.         }  
  16.     }  
  17.   
  18.     if (i == 0) {           //兄  
  19.         close(fd[0]);               //寫,關閉讀端  
  20.         dup2(fd[1], STDOUT_FILENO);       
  21.         execlp("ls""ls", NULL);     
  22.     } else if (i == 1) {    //弟  
  23.         close(fd[1]);               //讀,關閉寫端  
  24.         dup2(fd[0], STDIN_FILENO);        
  25.         execlp("wc""wc""-l", NULL);       
  26.     } else {  
  27.         close(fd[0]);  
  28.         close(fd[1]);  
  29.         for(i = 0; i < 2; i++)       //兩個兒子wait兩次  
  30.             wait(NULL);  
  31.     }  
  32.   
  33.     return 0;  
  34. }  


    測試:是否允許,一個pipe有一個寫端,多個讀端呢?是否允許有一個讀端多個寫端呢? 【pipe3.c】

  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <sys/wait.h>  
  4. #include <string.h>  
  5. #include <stdlib.h>  
  6.   
  7. int main(void)  
  8. {  
  9.     pid_t pid;  
  10.     int fd[2], i, n;  
  11.     char buf[1024];  
  12.   
  13.     int ret = pipe(fd);  
  14.     if(ret == -1){  
  15.         perror("pipe error");  
  16.         exit(1);  
  17.     }  
  18.   
  19.     for(i = 0; i < 2; i++){  
  20.         if((pid = fork()) == 0)  
  21.             break;  
  22.         else if(pid == -1){  
  23.             perror("pipe error");  
  24.             exit(1);  
  25.         }  
  26.     }  
  27.   
  28.     if (i == 0) {             
  29.         close(fd[0]);                 
  30.         write(fd[1], "1.hello\n", strlen("1.hello\n"));  
  31.     } else if(i == 1) {   
  32.         close(fd[0]);                 
  33.         write(fd[1], "2.world\n", strlen("2.world\n"));  
  34.     } else {  
  35.         close(fd[1]);       //父進程關閉寫端,留讀端讀取數據      
  36. //      sleep(1);  
  37.         n = read(fd[0], buf, 1024);     //從管道中讀數據  
  38.         write(STDOUT_FILENO, buf, n);  
  39.   
  40.         for(i = 0; i < 2; i++)       //兩個兒子wait兩次  
  41.             wait(NULL);  
  42.     }  
  43.   
  44.     return 0;  
  45. }  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章