管道的读写行为
使用管道需要注意以下4種特殊情況(默認(rèn)都是阻塞I/O操作,沒有設(shè)置O_NONBLOCK標(biāo)志):
1. 如果所有指向管道寫端的文件描述符都關(guān)閉了(管道寫端引用計數(shù)為0),而仍然有進(jìn)程從管道的讀端讀數(shù)據(jù),那么管道中剩余的數(shù)據(jù)都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
2. 如果有指向管道寫端的文件描述符沒關(guān)閉(管道寫端引用計數(shù)大于0),而持有管道寫端的進(jìn)程也沒有向管道中寫數(shù)據(jù),這時有進(jìn)程從管道讀端讀數(shù)據(jù),那么管道中剩余的數(shù)據(jù)都被讀取后,再次read會阻塞,直到管道中有數(shù)據(jù)可讀了才讀取數(shù)據(jù)并返回。
3. 如果所有指向管道讀端的文件描述符都關(guān)閉了(管道讀端引用計數(shù)為0),這時有進(jìn)程向管道的寫端write,那么該進(jìn)程會收到信號SIGPIPE,通常會導(dǎo)致進(jìn)程異常終止。當(dāng)然也可以對SIGPIPE信號實施捕捉,不終止進(jìn)程。具體方法信號章節(jié)詳細(xì)介紹。
4. 如果有指向管道讀端的文件描述符沒關(guān)閉(管道讀端引用計數(shù)大于0),而持有管道讀端的進(jìn)程也沒有從管道中讀數(shù)據(jù),這時有進(jìn)程向管道寫端寫數(shù)據(jù),那么在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數(shù)據(jù)并返回。
總結(jié):
讀管道:1.管道中有數(shù)據(jù),read返回實際讀到的字節(jié)數(shù)。2.管道中無數(shù)據(jù):管道寫端被全部關(guān)閉,read返回0(好像讀到文件結(jié)尾);寫端沒有全部被關(guān)閉,read阻塞等待(不久的將來可能有數(shù)據(jù)遞達(dá),此時會讓出cpu)。
寫管道:1.管道讀端全部被關(guān)閉,進(jìn)程異常終止(也可使用捕捉SIGPIPE信號,使進(jìn)程不終止)。2. 管道讀端沒有全部關(guān)閉:管道已滿,write阻塞;管道未滿,write將數(shù)據(jù)寫入,并返回實際寫入的字節(jié)數(shù)。
重點注意:
如果寫入的數(shù)據(jù)大小n<=PIPE_BUF時,linux保證寫入的原子性,即要么不寫,要么全寫入。如果沒有足夠的空間供n個字節(jié)全部寫入,則會阻塞直到有足夠空間供n個字節(jié)全部寫入;如果寫入的數(shù)據(jù)大小n>PIPE_BUF時,寫入不再具有原子性,可能中間有其它進(jìn)程穿插寫入,其自身也會阻塞,直到將n字節(jié)全部寫入在才返回寫入的字節(jié)數(shù),否則阻塞等待。
讀數(shù)據(jù)時,如果請求讀取的數(shù)據(jù)(read函數(shù)的緩沖區(qū))大小>=PIPE_BUF,則直接返回管道中現(xiàn)有的數(shù)據(jù)字節(jié)數(shù)(即將管道中的數(shù)據(jù)全部讀出);如果< PIPE_BUF,則返回管道中現(xiàn)有的數(shù)據(jù)字節(jié)數(shù)(此時管道中的實際數(shù)據(jù)量<=請求的數(shù)據(jù)量大小),或者返回請求數(shù)據(jù)量的大小。
練習(xí)1:父子進(jìn)程使用管道通信,父寫入字符串,子進(jìn)程讀出并打印到屏幕。
#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); //子進(jìn)程睡3秒close(fd[1]); //子進(jìn)程關(guān)閉寫端char buff[1024]={0};ret = read(fd[0],buff,1024); //子進(jìn)程讀數(shù)據(jù)if(ret == -1){perror("read");exit(1);}else if(ret == 0) {printf("父進(jìn)程沒有向管道里寫入數(shù)據(jù)\n");}else {int res= write(STDOUT_FILENO,buff,ret); //將讀出的數(shù)據(jù)輸出到屏幕if(res == -1){perror("write");exit(1);}}close(fd[0]); //子進(jìn)程結(jié)束前關(guān)閉掉文件描述符}else {close(fd[0]);int rer = write(fd[1],p,strlen(p)); //父進(jìn)程寫入數(shù)據(jù)if(rer == -1){perror("write");exit(1);}close(fd[1]); //父進(jìn)程結(jié)束前關(guān)閉掉文件描述符wait( NULL ); //父進(jìn)程回收(阻塞等待)}return 0; }[root@localhost pipe]# ./pip
zhangshuxiong
[root@localhost pipe]#???? //可見,如果沒有wait,則父進(jìn)程會先結(jié)束,正因為有了wait,父進(jìn)程會等待子進(jìn)程結(jié)束,最后shell進(jìn)程才會收回前臺,等待與用戶交互。注意,即使沒有sleep函數(shù),依然能保證子進(jìn)程運行時一定會讀到數(shù)據(jù),因為是阻塞讀。
?
練習(xí)2:使用管道實現(xiàn)父子進(jìn)程間通信,完成:ls | wc –l。假定父進(jìn)程實現(xiàn)ls,子進(jìn)程實現(xiàn)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? ?//統(tǒng)計文件的字?jǐn)?shù)
14
其實 ls | wc –l命令執(zhí)行后,shell進(jìn)程會創(chuàng)建兩個子進(jìn)程,并創(chuàng)建一個管道,用于兩子進(jìn)程通信,下面給出詳細(xì)實現(xiàn)過程:
#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); //將標(biāo)準(zhǔn)輸入重定向到管道讀端if(as == -1){perror("dup2");exit(1);}close(fd[0]); //只是關(guān)了fd[0],不關(guān)也可以,進(jìn)程結(jié)束會自動關(guān)閉execlp("wc","wc","-l",NULL); //該命令從標(biāo)準(zhǔn)輸入讀取文本}else {close(fd[0]);int as = dup2(fd[1],STDOUT_FILENO); //將標(biāo)準(zhǔn)輸出重定向到管道寫端if(as == -1){perror("dup2");exit(1);}execlp("ls","ls",NULL); ///該命令結(jié)果會寫到標(biāo)準(zhǔn)輸出}return 0; }[root@localhost pipe]# ./pip
14???????????????????? ?//可見,跟ls | wc –l的結(jié)果一樣
注意,上述程序并沒有考慮到子進(jìn)程的回收問題,如果父進(jìn)程比子進(jìn)程先結(jié)束,子進(jìn)程會被init進(jìn)程回收;后結(jié)束,子進(jìn)程會先變?yōu)榻┦M(jìn)程,等父進(jìn)程結(jié)束了,再被init進(jìn)程回收。
ls命令正常會將結(jié)果集寫出到stdout,但現(xiàn)在會寫入管道的寫端;wc –l 正常應(yīng)該從stdin讀取數(shù)據(jù),但此時會從管道的讀端讀。
也有可能會出現(xiàn)這種情況:程序執(zhí)行,發(fā)現(xiàn)程序執(zhí)行結(jié)束,shell還在阻塞等待用戶輸入。這是因為,shell → fork → ./pipe1, 程序pipe1的子進(jìn)程將stdin重定向給管道,父進(jìn)程執(zhí)行的ls會將結(jié)果集通過管道寫給子進(jìn)程。若父進(jìn)程在子進(jìn)程打印wc的結(jié)果到屏幕之前被shell調(diào)用wait回收,shell就會先輸出$提示符。
?
練習(xí)3:使用管道實現(xiàn)兄弟進(jìn)程間通信。 兄:ls? 弟: wc -l? 父:等待回收子進(jìn)程。要求,使用“循環(huán)創(chuàng)建N個子進(jìn)程”模型創(chuàng)建兄弟進(jìn)程,使用循環(huán)因子i標(biāo)示。注意管道讀寫行為。
#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]); //特別強(qiáng)調(diào),父進(jìn)程不用管道,必須要關(guān)掉,否則運行出錯(為了維護(hù)管道的單向通信)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
強(qiáng)調(diào)一點:在使用管道傳遞數(shù)據(jù)之前,不用的管道讀或?qū)懚硕急仨氁P(guān)閉,這是為了維護(hù)管道的正常運行(單向通信)。
?
測試:是否允許,一個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]); //父進(jìn)程關(guān)閉寫端,留讀端讀取數(shù)據(jù) //sleep(1); //這條語句是很關(guān)鍵的n = read(fd[0], buf, 1024); //從管道中讀數(shù)據(jù)write(STDOUT_FILENO, buf, n);for(i = 0; i < 2; i++) //兩個兒子wait兩次wait(NULL);}return 0; }如果父進(jìn)程不睡眠:
[root@localhost pipe]# ./pipe3
2.world
1.hello
[root@localhost pipe]# ./pipe3
1.hello
[root@localhost pipe]# ./pipe3
2.world
可見:三個進(jìn)程的執(zhí)行順序是隨機(jī)的,如果兩個子進(jìn)程在父進(jìn)程讀之前,都先寫入,那么兩個都會讀出。為了確保兩個都讀出,可以使用讀兩次的方法,也可以讓父進(jìn)程先睡眠一會,如下:
如果父進(jìn)程睡眠:
[root@localhost pipe]# ./pipe3
1.hello
2.world
[root@localhost pipe]# ./pipe3
1.hello
2.world
?
最終練習(xí):統(tǒng)計當(dāng)前系統(tǒng)中進(jìn)程ID大于10000的進(jìn)程個數(shù)。
提示: 采用awk命令,可以統(tǒng)計文本中符合條件列的個數(shù)及和。運用ps aux和管道。
總結(jié)
- 上一篇: annimate只能执行一次 财富
- 下一篇: 管道缓冲区大小