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函數,但依然能保證子進程運行時一定會讀到數據呢?
- #include <unistd.h>
- #include <string.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/wait.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- int main(void)
- {
- pid_t pid;
- char buf[1024];
- int fd[2];
- char *p = "test for pipe\n";
- if (pipe(fd) == -1)
- sys_err("pipe");
- pid = fork();
- if (pid < 0) {
- sys_err("fork err");
- } else if (pid == 0) {
- close(fd[1]);
- int len = read(fd[0], buf, sizeof(buf));
- write(STDOUT_FILENO, buf, len);
- close(fd[0]);
- } else {
- close(fd[0]);
- write(fd[1], p, strlen(p));
- wait(NULL);
- close(fd[1]);
- }
- return 0;
- }
管道的讀寫行爲
使用管道需要注意以下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】
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/wait.h>
- int main(void)
- {
- pid_t pid;
- int fd[2];
- pipe(fd);
- pid = fork();
- if (pid == 0) { //child
- close(fd[1]); //子進程從管道中讀數據,關閉寫端
- dup2(fd[0], STDIN_FILENO); //讓wc從管道中讀取數據
- execlp("wc", "wc", "-l", NULL); //wc命令默認從標準讀入取數據
- } else {
- close(fd[0]); //父進程向管道中寫數據,關閉讀端
- dup2(fd[1], STDOUT_FILENO); //將ls的結果寫入管道中
- execlp("ls", "ls", NULL); //ls輸出結果默認對應屏幕
- }
- return 0;
- }
- /*
- * 程序不時的會出現先打印$提示符,再出程序運行結果的現象。
- * 這是因爲:父進程執行ls命令,將輸出結果給通過管道傳遞給
- * 子進程去執行wc命令,這時父進程若先於子進程打印wc運行結果
- * 之前被shell使用wait函數成功回收,shell就會先於子進程打印
- * wc運行結果之前打印$提示符。
- * 解決方法:讓子進程執行ls,父進程執行wc命令。或者在兄弟進程間完成。
- */
程序執行,發現程序執行結束,shell還在阻塞等待用戶輸入。這是因爲,shell → fork → ./pipe1, 程序pipe1的子進程將stdin重定向給管道,父進程執行的ls會將結果集通過管道寫給子進程。若父進程在子進程打印wc的結果到屏幕之前被shell調用wait回收,shell就會先輸出$提示符。
練習:使用管道實現兄弟進程間通信。 兄:ls 弟: wc -l 父:等待回收子進程。
要求,使用“循環創建N個子進程”模型創建兄弟進程,使用循環因子i標示。注意管道讀寫行爲。 【pipe2.c】
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/wait.h>
- int main(void)
- {
- pid_t pid;
- int fd[2], i;
- pipe(fd);
- for (i = 0; i < 2; i++) {
- if((pid = fork()) == 0) {
- break;
- }
- }
- if (i == 0) { //兄
- close(fd[0]); //寫,關閉讀端
- dup2(fd[1], STDOUT_FILENO);
- execlp("ls", "ls", NULL);
- } else if (i == 1) { //弟
- close(fd[1]); //讀,關閉寫端
- dup2(fd[0], STDIN_FILENO);
- execlp("wc", "wc", "-l", NULL);
- } else {
- close(fd[0]);
- close(fd[1]);
- for(i = 0; i < 2; i++) //兩個兒子wait兩次
- wait(NULL);
- }
- return 0;
- }
測試:是否允許,一個pipe有一個寫端,多個讀端呢?是否允許有一個讀端多個寫端呢? 【pipe3.c】
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <stdlib.h>
- int main(void)
- {
- pid_t pid;
- int fd[2], i, n;
- char buf[1024];
- int ret = pipe(fd);
- if(ret == -1){
- perror("pipe error");
- exit(1);
- }
- for(i = 0; i < 2; i++){
- if((pid = fork()) == 0)
- break;
- else if(pid == -1){
- perror("pipe error");
- exit(1);
- }
- }
- if (i == 0) {
- close(fd[0]);
- write(fd[1], "1.hello\n", strlen("1.hello\n"));
- } else if(i == 1) {
- close(fd[0]);
- write(fd[1], "2.world\n", strlen("2.world\n"));
- } else {
- close(fd[1]); //父進程關閉寫端,留讀端讀取數據
- // sleep(1);
- n = read(fd[0], buf, 1024); //從管道中讀數據
- write(STDOUT_FILENO, buf, n);
- for(i = 0; i < 2; i++) //兩個兒子wait兩次
- wait(NULL);
- }
- return 0;
- }