赞
踩
下面将会讨论这样三个问题:
解答一: 相信大家这样的代码应该一点都不会陌生。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<fcntl.h> int main() { int mypipe[2],pid,test; pipe(mypipe); pid=fork(); if(pid!=0) { int i; printf("i'm parent\n"); // close(mypipe[1]) for(i=0;i<3;i++) { read(mypipe[0],&test,sizeof(int)); printf("test=%d\n",test); } close(mypipe[0]); return 0; } else{ printf("i am child\n"); close(mypipe[0]); int var=5; write(mypipe[1],&var,sizeof(int)); close(mypipe[1]); printf("child finsh\n"); return 0; } }
就像这个样子,主程序就会一直被阻塞在那里。但是如果我现在将父进程的 close(mypipe[1]);这条语句的注释放开,就会使其不会阻塞的。如下图所示:
这个就是内核设计管道的时候的奥秘了。这里给出我自己的理解(大家在网络上也是可以自己找到类似的答案,但是大家都是你复制我的,我复制你的)。
因为fork是在pipe()函数之后,所以使得父进程和子进程 拥有了相同的文件描述符。这里引用UNIX环境高级编程上面的一句话:“fork的一个特性是父进程所打开的文件描述符都被复制到了子进程中。我们说的“复制”是因为对每一个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开的文件描述符指向同一个文件表项。”所以,在父进程和子进程都没有关闭各自的文件描述符的时候,mypipe[0]引用计数为2,mypipe[1]引用计数也为2。这个引用计数,学过java的可能好理解一点。java中的垃圾回收机制,就是去判断这个对象是否还有引用,如果没有引用,就将其内存回收,避免浪费空间。这里也是一样的,因为父进程在等待子进程写数据,父进程去读。所以,父进程一定后于子进程退出。但是子进程退出后,mypipe[1]的引用计数仍然为1,因为父进程这里还有一个引用。这个时候内核就会觉得应该还会有数据被写入(因为mypipe[1]引用计数不为0),所以,当read再次去读取数据的时候,就会被阻塞。这也就是大家在网络上面看到的这样一段话:“如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。 ”如果之前父进程将mypipe[1]关闭了,此时mypipe[1]引用计数为0,所以说,如果再去读取文件的信息,导致read返回0,就像读到了文件末尾。(这个也是管道读取的结束标志)这里也引用一下Unix/Linux编程实践教程上面的一句话:“当所有的写者关闭了管道的写数据端时,试图从管道读取数据的调用返回0,这意味着文件的结束。”
大家应该可以看到我上面那个“再次”被注明了黄色,那是因为我在程序里面写的是for循环3次,也就是说会read三次。但是你如果仅仅read一次,是不会阻塞的。因为当时管道里面是有是数据的。
那我现在再使用另一种情况来试试,代码如下,仔细比对:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<fcntl.h> int main() { int mypipe[2],pid,test; pipe(mypipe); pid=fork(); if(pid!=0) { printf("i'm parent\n"); close(mypipe[1]); FILE* read=fdopen(mypipe[0],"r"); fscanf(read,"%d",&test); printf("test=%d\n",test); fclose(read); close(mypipe[0]); return 0; } else{ printf("i am child\n"); close(mypipe[0]); int var=5; FILE *mywrite=fdopen(mypipe[1],"w"); fprintf(mywrite,"%d",var); close(mypipe[1]); printf("child finsh\n"); return 0; } }
可以看到,我这里在父进程一开始就关闭父进程不用的管道端口mypipre[1],也并没有使用for循环来读取数据,也仅仅只读取了一次数据,那为什么会阻塞捏?原因在于你使用的写入的函数不同。可以看到子进程使用的是标准库函数—fpritnf();因为这种标准的库函数都是设计了缓存的,而且因为这里是与标准输出设备连接,所以一般来说应该是行缓冲。这里根据Unix环境高级编程5.4节中提到:“很多系统默认使用下面的类型缓冲:标准错误是不带缓冲的;若是执先终端设备的流,则是行缓冲,否则是全缓冲。”
所以,回到这个问题上面,就是因为子进程使用了行缓冲的fprintf(),而你输入的数据中没有‘\n’,导致数据被存入缓冲区,所以管道里面暂时也没有数据,fscanf()被阻塞。那么解决文体的简单方式就是把fprintf()换成这条语句:fprintf(mypipe[1],“%d\n”,var);
解答二: 这个例子是我在Unix环境高级编程这本书上面看到的,因为觉得之前都没有注意到这个问题,所以觉得有必要写下来:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<errno.h> int globvar=6; char buf[]="a write to stdout\n"; int main() { int var; pid_t pid; var=88; if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1) printf("write error"); printf("before fork\n"); /*we don't flush stdout*/ if((pid=fork())<0) { printf("fork error"); } else if(pid==0) { globvar++; var++; } else { sleep(2); } printf("pid=%ld,glob=%d,var=%d\n",(long)getpid(),globvar,var); return 0; }
可以看到,这个仅仅输出了一个before fork这条语句,但是如果我将标准输出重定向到一个文件的时候,却会得到两条这个语句:
原因和上一个问题中的输入输出缓冲问题有关。因为fork()在prinf(“before fork\n”);这条语句的后面,在是标准输出连接到终端设备的时候,是行缓冲,所以这条语句没有保存在缓冲区,直接打印出来。但是输出重定向的时候,导致缓冲为全缓冲,所以这条语句的内容在缓冲区存着,并且后面fork()语句的执行,导致子进程也复制了父进程的缓冲区的内容,所以最终会打印两遍。
**解答三:**使用fork的时候,通常我们是要解决僵尸进程的问题。这里,使用两次fork()来很好的达到解决僵尸进程的问题。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; if((pid=fork())<0) { printf("fork error\n"); return -1; } else if(pid==0) { if((pid=fork())<0) //第一个孩子fork自己的孩子 { printf("fork error\n"); } else if(pid>0) exit(0); //第一个孩子结束自己 sleep(2); printf("sencond child,parent pid=%ld\n",(long)getppid()); exit(0); } if(waitpid(pid,NULL,0)!=pid) { printf("waitpid error\n"); return -1; } exit(0); }
为什么可以起到防止僵尸进程的产生捏?首先,fork产生僵尸进程的原因就是因为子进程先退出,它会被立即从内存中移除,但是一些文件描述符是仍然在内存中的。如果父进程没有调用wait()和waitpid()函数,就会子进程的一些内容没有完全在内存中被移除。如果这样的程序多了,就会发生大麻烦。所以,这里的思路就是:父进程A fork出子进程B,子进程B fork出子进程C,然后父进程A去waitpid()B,子进程B结束自己,然后子进程C变成了孤儿进程(就是父进程先于子进程消失),被Init进程接收,之后因为进程A与进程B没有直接的关系,可以自己做自己的事情,就不用担心僵尸进程的问题了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。