linuxfork函数和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信号处理函数调⽤wait或waitpid()等待⼦进程结束,⼜没有显式忽略该信号,那么它就⼀直保持僵⼫状态; 存在的问题:如果⽗进程是⼀个循环,不会结束,那么⼦进程就会⼀直保持僵⼫状态,这就是为什么系统中有时会有很多的僵⼫进程,系统的性能可能会收到影响。 ** 如果这时⽗进程结束了,那么init进程⾃动会接⼿这个⼦进程,为它收⼫,它还是能被清除的。
⼦进程结束后为什么要进⼊僵⼫状态?
* 因为⽗进程可能要取得⼦进程的退出状态等信息。
僵⼫状态是每个⼦进程⽐经过的状态吗?
是的。 * 任何⼀个⼦进程(init除外)在exit()之后,并⾮马上就消失掉,⽽是留下⼀个称为僵⼫进程(Zombie)的数据结构,等待⽗进程处理。这是每个 ⼦进程在结束时都要经过的阶段。如果⼦进程在exit()之后,⽗进程没有来得及处理,这时⽤ps命令就能看到⼦进程的状态
是“Z”。如果⽗进程能及时 处理,可能⽤ps命令就来不及看到⼦进程的僵⼫状态,但这并不等于⼦进程不经过僵⼫状态。 *如果⽗进程在⼦进程结束之前退出,则⼦进程将由init接管。init将会以⽗进程的⾝份对僵⼫状态的⼦进程进⾏处理。
如何查看僵⼫进程:
linux下的sleep函数$ 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会被设置成相应的值以指⽰错误所在;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论