进程的生命周期可以用这样一个形象的比喻:
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信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态; 存在的问题:如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程,系统的性能可能会收到影响。 ** 如果这时父进程结束了,那么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);