使用管道需要注意以下4種特殊情況(默認都是阻塞I/O操作,沒有設置O_NONBLOCK標志):
1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
2. 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大于0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據并返回。
3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為0),這時有進程向管道的寫端write,那么該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。
4. 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大于0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據并返回。
總結:
讀管道:1.管道中有數據,read返回實際讀到的字節數。2.管道中無數據:管道寫端被全部關閉,read返回0(好像讀到文件結尾);寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)。
寫管道:1.管道讀端全部被關閉,進程異常終止(也可使用捕捉SIGPIPE信號,使進程不終止)。2. 管道讀端沒有全部關閉:管道已滿,write阻塞;管道未滿,write將數據寫入,并返回實際寫入的字節數。
重點注意:
如果寫入的數據大小n<=PIPE_BUF時,linux保證寫入的原子性,即要么不寫,要么全寫入。如果沒有足夠的空間供n個字節全部寫入,則會阻塞直到有足夠空間供n個字節全部寫入;如果寫入的數據大小n>PIPE_BUF時,寫入不再具有原子性,可能中間有其它進程穿插寫入,其自身也會阻塞,直到將n字節全部寫入在才返回寫入的字節數,否則阻塞等待。
讀數據時,如果請求讀取的數據(read函數的緩沖區)大小>=PIPE_BUF,則直接返回管道中現有的數據字節數(即將管道中的數據全部讀出);如果< PIPE_BUF,則返回管道中現有的數據字節數(此時管道中的實際數據量<=請求的數據量大小),或者返回請求數據量的大小。
練習1:父子進程使用管道通信,父寫入字符串,子進程讀出并打印到屏幕。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{int ret,fd1;char *p="zhangshuxiong\n";int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0) {sleep(3); //子進程睡3秒close(fd[1]); //子進程關閉寫端char buff[1024]={0};ret = read(fd[0],buff,1024); //子進程讀數據if(ret == -1){perror("read");exit(1);}else if(ret == 0) {printf("父進程沒有向管道里寫入數據\n");}else {int res= write(STDOUT_FILENO,buff,ret); //將讀出的數據輸出到屏幕if(res == -1){perror("write");exit(1);}}close(fd[0]); //子進程結束前關閉掉文件描述符}else {close(fd[0]);int rer = write(fd[1],p,strlen(p)); //父進程寫入數據if(rer == -1){perror("write");exit(1);}close(fd[1]); //父進程結束前關閉掉文件描述符wait( NULL ); //父進程回收(阻塞等待)}return 0;
}
[root@localhost pipe]# ./pip
zhangshuxiong
[root@localhost pipe]#???? //可見,如果沒有wait,則父進程會先結束,正因為有了wait,父進程會等待子進程結束,最后shell進程才會收回前臺,等待與用戶交互。注意,即使沒有sleep函數,依然能保證子進程運行時一定會讀到數據,因為是阻塞讀。
?
練習2:使用管道實現父子進程間通信,完成:ls | wc –l。假定父進程實現ls,子進程實現wc。
[root@localhost pipe]# ls
makefile? pip? pip.c? pipe? pipe1? pipe1.c? pipe2? pipe2.c? pipe3? pipe3.c? pipe.c? pipe_test? pipe_test.c? test
[root@localhost pipe]# ls | wc –l? ?//統計文件的字數
14
其實 ls | wc –l命令執行后,shell進程會創建兩個子進程,并創建一個管道,用于兩子進程通信,下面給出詳細實現過程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(void)
{int ret,fd1;int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0) {close(fd[1]);int as = dup2(fd[0],STDIN_FILENO); //將標準輸入重定向到管道讀端if(as == -1){perror("dup2");exit(1);}close(fd[0]); //只是關了fd[0],不關也可以,進程結束會自動關閉execlp("wc","wc","-l",NULL); //該命令從標準輸入讀取文本}else {close(fd[0]);int as = dup2(fd[1],STDOUT_FILENO); //將標準輸出重定向到管道寫端if(as == -1){perror("dup2");exit(1);}execlp("ls","ls",NULL); ///該命令結果會寫到標準輸出}return 0;
}
[root@localhost pipe]# ./pip
14???????????????????? ?//可見,跟ls | wc –l的結果一樣
注意,上述程序并沒有考慮到子進程的回收問題,如果父進程比子進程先結束,子進程會被init進程回收;后結束,子進程會先變為僵尸進程,等父進程結束了,再被init進程回收。
ls命令正常會將結果集寫出到stdout,但現在會寫入管道的寫端;wc –l 正常應該從stdin讀取數據,但此時會從管道的讀端讀。
也有可能會出現這種情況:程序執行,發現程序執行結束,shell還在阻塞等待用戶輸入。這是因為,shell → fork → ./pipe1, 程序pipe1的子進程將stdin重定向給管道,父進程執行的ls會將結果集通過管道寫給子進程。若父進程在子進程打印wc的結果到屏幕之前被shell調用wait回收,shell就會先輸出$提示符。
?
練習3:使用管道實現兄弟進程間通信。 兄:ls? 弟: wc -l? 父:等待回收子進程。要求,使用“循環創建N個子進程”模型創建兄弟進程,使用循環因子i標示。注意管道讀寫行為。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main(void)
{int i,ret,fd1;int n=2;int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}for(i=0;i<n;i++){fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0)break;}if(i == n){close(fd[0]);close(fd[1]); //特別強調,父進程不用管道,必須要關掉,否則運行出錯(為了維護管道的單向通信)int status;do {pid_t pid=waitpid(-1,&status,0);if(pid > 0)n--;if(pid == -1){perror("waitpid");exit(1);}if(WIFEXITED(status))printf("the child process of exit with %d\n",WEXITSTATUS(status));else if(WIFSIGNALED(status))printf("the child process was killed by %dth signal\n",WTERMSIG(status));}while(n>0);}else if(i == 1) {close(fd[1]);int as = dup2(fd[0],STDIN_FILENO);if(as == -1){perror("dup2");exit(1);}close(fd[0]);execlp("wc","wc","-l",NULL);}else {close(fd[0]);int as = dup2(fd[1],STDOUT_FILENO);if(as == -1){perror("dup2");exit(1);}execlp("ls","ls",NULL);}return 0;
}
[root@localhost pipe]# ./pip
14
the child process of exit with 0
the child process of exit with 0
強調一點:在使用管道傳遞數據之前,不用的管道讀或寫端都必須要關閉,這是為了維護管道的正常運行(單向通信)。
?
測試:是否允許,一個pipe有一個寫端,多個讀端呢?是否允許有一個讀端多個寫端呢?
#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;
}
如果父進程不睡眠:
[root@localhost pipe]# ./pipe3
2.world
1.hello
[root@localhost pipe]# ./pipe3
1.hello
[root@localhost pipe]# ./pipe3
2.world
可見:三個進程的執行順序是隨機的,如果兩個子進程在父進程讀之前,都先寫入,那么兩個都會讀出。為了確保兩個都讀出,可以使用讀兩次的方法,也可以讓父進程先睡眠一會,如下:
如果父進程睡眠:
[root@localhost pipe]# ./pipe3
1.hello
2.world
[root@localhost pipe]# ./pipe3
1.hello
2.world
?
最終練習:統計當前系統中進程ID大于10000的進程個數。
提示: 采用awk命令,可以統計文本中符合條件列的個數及和。運用ps aux和管道。