当前位置:   article > 正文

fork父子进程的一些细节问题_fork 父子进程通信

fork 父子进程通信

    下面将会讨论这样三个问题:

  1. 为什么在使用fork来实现管道之间的通信的时候,通常父子进程会关闭一个自己用不到的文件描述符
  2. 对使用了fork的程序重定向输出后某条语句打印两遍
  3. 如何使用两次fork来解决僵尸进程的问题

    解答一: 相信大家这样的代码应该一点都不会陌生。

#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;
	}
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在这里插入图片描述
就像这个样子,主程序就会一直被阻塞在那里。但是如果我现在将父进程的 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;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在这里插入图片描述
        可以看到,我这里在父进程一开始就关闭父进程不用的管道端口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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在这里插入图片描述
        可以看到,这个仅仅输出了一个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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

        为什么可以起到防止僵尸进程的产生捏?首先,fork产生僵尸进程的原因就是因为子进程先退出,它会被立即从内存中移除,但是一些文件描述符是仍然在内存中的。如果父进程没有调用wait()和waitpid()函数,就会子进程的一些内容没有完全在内存中被移除。如果这样的程序多了,就会发生大麻烦。所以,这里的思路就是:父进程A fork出子进程B,子进程B fork出子进程C,然后父进程A去waitpid()B,子进程B结束自己,然后子进程C变成了孤儿进程(就是父进程先于子进程消失),被Init进程接收,之后因为进程A与进程B没有直接的关系,可以自己做自己的事情,就不用担心僵尸进程的问题了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/210696
推荐阅读
相关标签
  

闽ICP备14008679号