linux fork函数和wait系函数详解

进程的生命周期可以用这样一个形象的比喻:


随着一句fork,一个新进程呱呱落地,但它这时只是老进程的一个克隆。
然后随着exec,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。
人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个”}”,从容地离我们而去;也可以是自杀,自杀有2种方式,一种 是调用 exit函数,一种是在main函数内使用return,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过 另外一些方式结束他的生命。
进程死掉以后,会留下一具僵尸,wait和waitpid充当了殓尸工,把僵尸推去火化,使其最终归于无形。

1.fork函数:

pid_t fork(void);

返回值:如果返回值小于0,则表示新创建的进程失败。

如果返回值等于0,则表示在新创建的进程中。

如果返回值大于0,则表示在父进程中,返回新创建的子进程号。

例如:

pid_t pid;

if((pid = fork()) < 0)
{
    /*fork函数的错误处理*/
}
else if(pid == 0)
{
   /*fork函数的新创建的进程*/
}
else
{
  /*父进程中*/
}
当在执行fork的代码的时候,fork返回2次,在子进程中fork返回值为0,在父进程中fork的返回值为子进程的pid,所以,上面代码中的else if 和else 一般情况下(fork不出错)都会被执行。


fork出错的情况:

(1)系统中的进程太多

(2)每个实际用户ID进程数超过了系统限制。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>


int main () 
{ 
	pid_t pid; 

	pid=fork(); 

	if (pid < 0) 
		printf("error in fork!"); 

	else if (pid == 0) 
		printf("i am the child process, my process id is %d\n",getpid()); 

	else 
		printf("i am the parent process, my process id is %d\n",getpid()); 
	waitpid(pid, NULL, 0);
	return 0;

} 

输出为:

[root@zhangpeng code]# ./test_fork.out 
i am the parent process, my process id is 14942
i am the child process, my process id is 14943


 被fork创建的新进程叫做自进程。fork函数被调用一次,却两次返回。返回值唯一的区别是在子进程中返回0,而在父进程中返回子进程的pid

在父进程中要返回子进程的pid的原因是父进程可能有不止一个子进程,而一个进程又没有任何函数可以得到他的子进程的pid

子进程和父进程都执行在fork函数调用之后的代码,子进程是父进程的一个拷贝。例如,父进程的数据空间、堆栈空间都会给子进程一个拷贝,而不是共享这些内存。



详解fork之后数据段代码段堆栈段的变化


#include <unistd.h>

#include <stdio.h>

  int main(void)

  {

  pid_t pid;

  int count=0;

  /*此处,执行fork调用,创建了一个新的进程, 这个进程共享父进程的数据和堆栈空间等,这之后的代码指令为子进程创建了一个拷贝。 fock 调用是一个复制进程,fock 不象线程需提供一个函数做为入口, fock调用后,新进程的入口就在 fock的下一条语句。*/

  pid = fork();

  /*此处的pid的值,可以说明fork调用后,目前执行的是父进程还是子进程*/

  printf( "Now, the pid returned by calling fork() is%d\n", pid );

  if ( pid>0 )

  {

  /*当fork在子进程中返回后,fork调用又向父进程中返回子进程的pid, 如是该段代码被执行,但是注意的事,count仍然为0, 因为父进程中的count始终没有被重新赋值,  这里就可以看出子进程的数据和堆栈空间和父进程是独立的,而不是共享数据*/

  printf( "This is the parent process,the child hasthe pid:%d\n", pid );

  printf( "In the parent process,count = %d\n",count );

  }

  else if ( !pid )

  { /*在子进程中对count进行自加1的操作,但是并没有影响到父进程中的count值,父进程中的count值仍然为0*/

  printf( "This is the child process.\n");

  printf( "Do your own things here.\n" );

  count++;

  printf( "In the child process, count = %d\n",count );

  }

  else

  {

  printf( "fork failed.\n" );

  }

  return 0;

  }

  也就是说,在Linux下一个进程在内存里有三部分的数据,就是"代码段"、"堆栈段"和"数据段"。"代码段",顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

  仔细分析后,我们就可以知道:

  一个程序一旦调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。


2.wait系函数(wait、waitpid)

pid_t wait(int *status);

当进程正常或者非正常结束的时候,内核向父进程发一个异步信号(SIGCHLD),当然父进程可以选择忽略此信号或者handle此信号。

UNIX系统中,一个进程结束了,但是他的父进程没有等待(调用wait/ waitpid),那么他将变成一个僵尸进程. 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程,因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init来接管他,成为他的父进程

调用wait产生的结果:

(1)如果此时程序没有子进程,那么调用wait或者waitpid则将会报错返回。

(2)如果一个子进程已经终止,正等待父进程获取终止状态,调用wait系函数返回子进程终止装态。

(3)如果子进程正在运行,则阻塞。

系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号

还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。下面的代码就是fork 2次避免僵尸进程的出现。


#include "apue.h"
#include <sys/wait.h>

int
main(void)
{
	pid_t	pid;

	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid == 0) {		/* first child */
		if ((pid = fork()) < 0)
			err_sys("fork error");
		else if (pid > 0)
			exit(0);	/* parent from second fork == first child */

		/*
		 * We're the second child; our parent becomes init as soon
		 * as our real parent calls exit() in the statement above.
		 * Here's where we'd continue executing, knowing that when
		 * we're done, init will reap our status.
		 */
		sleep(2);
		printf("second child, parent pid = %d\n", getppid());
		exit(0);
	}

	if (waitpid(pid, NULL, 0) != pid)	/* wait for first child */
		err_sys("waitpid error");

	/*
	 * We're the parent (the original process); we continue executing,
	 * knowing that we're not the parent of the second child.
	 */
	exit(0);
}

这个程序第一个子进程结束,通过第一个子进程fork出来的进程的父进程变成init,即
printf("second child, parent pid = %d\n", getppid());这个输出的是1.孙进程将会被init进程回收。

	if (waitpid(pid, NULL, 0) != pid)	/* wait for first child */
		err_sys("waitpid error");这段代码是wait子进程。如果没有这段代码,在父进程没有结束的前提下,子进程将会变成僵尸进程。

僵尸进程的处理: 它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用waitwaitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态; 存在的问题:如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程,系统的性能可能会收到影响。 ** 如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。

子进程结束后为什么要进入僵尸状态?

 * 因为父进程可能要取得子进程的退出状态等信息。

僵尸状态是每个子进程比经过的状态吗?

是的。 * 任何一个子进程(init除外)exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 *如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。 

如何查看僵尸进程:

$ ps -el 其中,有标记为Z的进程就是僵尸进程 S代表休眠状态;D代表不可中断的休眠状态;R代表运行状态;Z代表僵死状态;T代表停止或跟踪状态。

wait的参数

(1),WIFEXITED(status)这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值.

(2),WEXITSTATUS(status)当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7.请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义.

更多参数请看UNIX 环境高级编程2第8章。

pid_t waitpid(pid_t pid,int*status,intoptions);

options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用”|”运算符把它们连接起来使用,比如:

waitpid(17455,NULL,WNOHANG|WUNTRACED);

如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,用于跟踪调试,极少用到。
waitpid的调用比wait调用药稍微复杂一些

(1). 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
(2). 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
(3). 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;


  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值