引言
fork()调用一次返回两次?
==> 表述误导人,应是父子进程分别返回一次,而不是调用一次返回两次。
僵尸进程和孤儿进程区别?
==> 僵尸有爹,但是爹不管他了,孤儿死爹了。
两次fork()可以避免产生僵尸进程?
==> 现阶段个人理解的是,不可以避免产生僵尸进程,只是让儿子升格当爷爷。
不要直接调用fork()来产生子进程?
==> 子进程不会复制父进程的文件锁之类的东西,所以可能会导致读写文件出问题。
fork()调用一次返回两次?
好多书上跟网上的描述是这样的“由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次”(摘自《Unix环境高级编程》一书),个人认为这种描述有点误导人,第一次看到这种描述的时候,我想的不是fork()是干嘛的,我想的是为啥一个函数可以返回两次。函数还有两个返回值这种操作?答案是函数只能有一个返回值,而且只能返回一次。
真实的操作是这样的:进程A调用了fork,在进程A的fork函数里面开始复制自己,进程A的复制版叫做进程B,进程B执行跟进程A一模一样的代码,也就是说,进程B现在执行到跟A一样的程度,也就是fork函数执行到一半,已经把创建副本那部分执行完了,剩下的就是返回了,返回前需要注意的是,创建出来的副本进程B的pid会在fork里面用变量id保存起来,那么,接下来的工作就是在fork里面加个判断,如果变量id==getpid(),那就是进程B,那就返回变量0;反之,如果id!=getpid(),那就是进程A,返回变量id即可。现在只需要把上边的进程A换成父进程,进程B换成子进程,就完了。因此真实的操作应该是调用一次,在两个进程里面分别执行一次,所以就会在两个进程里面分别返回一次。
僵尸进程和孤儿进程区别
孤儿进程:极端情况如下,一个进程fork()出一个子进程之后,自己马上exit(),那么他刚刚fork()出来的子进程就没有父进程了,那这个子进程就会成为孤儿进程。但是,不知道从啥时候开始的内核就开始不存在孤儿进程了,因为init进程会收养所有的孤儿进程。这里就有个问题了,init可以收养孤儿进程,那他咋知道哪个进程是孤儿进程呢。大胆猜测如下:进程调用getppid()获取自己的父进程ID,如果父进程已经挂了,在getppid()函数里面会给内核发出一个信号,内核会知道是哪个进程发出来的信号,然后再通知init进程去收养对应进程。
僵尸进程(暴尸荒野):进程的的活已经干完了,只是占用的pid等资源还没有还给系统,只等父进程回收后还给系统才算是寿终正寝了。而回收资源这个事,是由父进程调用wait()/waitpid()函数来做的,如果有的程序猿比较大意,忘了做这个事。那么,在子进程做完自己的事之后,父进程不回收子进程资源,而是继续做自己的事情,在这段时间内,子进程依旧占用着系统资源,在父进程调用wait()之前都一直占用,但是父进程直到退出都不调用,或者有的父进程压根不退出,一直在运行,那么僵尸进程会一直存在。
至此,子进程还是僵尸,接下来就是个人见解的地方了。在父进程也干完自己的事退出之后,这个未被回收资源的子进程会变成孤儿进程,而系统所有的孤儿进程都会被1号系统进程init进程所收养,init进程是一定会回收资源的,所以刚才那个变成孤儿进程的僵尸进程,会被init进程收养。而这个孤儿僵尸进程又是已经干完活的,所以init进程在收养这个子进程之后,会回收他占用的资源。所以,只要程序里面不出现while(1)或者两个锁互相等释放资源等等这种死循环操作,哪怕是忘了wait()也不会有长期存在的僵尸进程的,因为init进程会给所有进程擦屁股。
两次fork可以避免产生僵尸进程?
先看看传说中的两次fork()避免产生僵尸进程是怎么做的(数字对应代码注释中的步骤):
1、原始进程fork出子进程;
2、子进程fork出孙子进程,也就是所谓的第二次fork;
3、子进程退出,自此,子进程干了两件事:
第一,fork出孙子;
第二,自己立即退出。
这两件事导致的结果是,孙子进程称为孤儿进程,而孤儿进程会被init进程收养,所以孤儿进程退出的时候,会有init进程来回收其资源,不用担心成为僵尸进程。目前为止,好像没啥问题,孙子进程是那个真正干活的进程,自打生出来那一刻就决定了他绝对不会成为僵尸进程(一出生就死爹了,成为孤儿,被init收养),所以好像真的没问题,确实产生了一个不会成为僵尸进程的进程。问题在下边,接着看。
4、回收子进程资源,因为子进程退出了,所以在爷爷进程里面需要回收那个一调用fork自己立马退出的子进程的资源,来防止子进程成为僵尸进程。
????????????????????????????????
总结一下,这整个操作可以描述为以下过程:为了防止僵尸进程的产生,我先产生一个可能成为僵尸进程的子进程,再用这个可能成为僵尸进程的子进程产生一个孙子进程,让孙子进程称为孤儿进程,被init进程收养,这样孙子进程就不会成为僵尸进程。然后再在爷爷进程里面回收那个可能成为僵尸进程的子进程的资源。如果不调用wait()来回收这个子进程的资源,那系统里还是会多出一个僵尸进程(亲测有僵尸)。
红字部分是重点,这是什么操作,既然可以调用wait()来回收可能成为僵尸进程的子进程的资源,那为啥不直接调用wait()回收那个真正干活的进程(孙子进程)的资源,这样就不用再产生一个专门用来fork()孙子进程后自己又退出的子进程了。满脸问号???
#include <sys/wait.h>
#include "apue.h"
int main(void)
{
pid_t pid = 0;
if((pid = fork()) == 0) // 1、fork出子进程
{
if((pid = fork()) == 0) // 2、子进程fork出孙子进程
{
sleep(3); /** 孙子进程干自己的事 **/
exit(0);
}
exit(0); // 3、子进程退出
}
waitpid(pid, NULL, NULL); // 4、回收子进程资源
while(1) /** 爷爷进程干自己的事 **/
{
...
}
exit(0);
}
再来看看僵尸进程是怎么产生的,下边的代码会产生一个僵尸进程,而且一直存在,不会被init进程回收,也就是真正意义上的僵尸进程:
#include <sys/wait.h>
#include "apue.h"
int main(void)
{
pid_t pid = 0;
if((pid = fork()) == 0) // fork出子进程
{
sleep(3); /** 子进程干自己的事 **/
exit(0);
}
while(1) /** 父进程干自己的事 **/
{
...
}
}
接着来看,如何使上面的程序不产生僵尸进程,所以这段代码跟上面调用两次fork()那段代码唯一的区别就是少调用一次fork(),这样也不会产生僵尸进程:
#include <sys/wait.h>
#include "apue.h"
int main(void)
{
pid_t pid = 0;
if((pid = fork()) == 0) // 1、fork出子进程
{
sleep(3); /** 子进程干自己的事 **/
exit(0);
}
waitpid(pid, NULL, NULL); // 2、回收子进程资源
while(1) /** 父进程干自己的事 **/
{
...
}
}
总结:两次fork()并不会避免产生一个僵尸进程,因为子进程还是有成为僵尸进程的可能,爷爷进程还是需要处理这个子进程的资源回收。两次fork()干的事在我看来就只是把本来该成为儿子的进程,先变成孙子,在变成孤儿,最后交给init接管,成为爷爷进程的同级进程,也就是升格当爷爷。
不要直接调用fork()来产生子进程
其实这句话也没那么绝对,这句话的起因是这样的,因为fork()出来的子进程跟父进程之间共享文件列表。所以,如果子进程复制了父进程的正文段,然后没有调用exec()函数去执行另外的进程,而是在父进程的基础上修修补补,跟父进程干着类似的活。这样的话就有可能会出现共享文件读写错误的问题,也就是父进程在写文件的时候,子进程可能也在写,导致写进去的东西既不是父进程想要的,也不是子进程想要的东西。
如下程序在运行的时候并没有像本来预期的那样,先往文件里写10000行"mimimi"再在其后追加50000行"cicici",而是乱序的,不知道咋写的,就是因为父子进程共享了文件列表。所以fork出子进程后最好是让他直接运行exec()函数去执行另外的进程。
#include <linux/types.h>
#include <linux/hdreg.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <iostream>
#include <linux/rtnetlink.h>
#include <unistd.h>
int main()
{
pid_t pid = 0;
int fd = 0;
std::string ccc = "cicicici\n";
std::string mmm = "mimimimi\n";
char buf_read[1024] = {0};
fd = open("/home/czp/code/unix_book/cap8/czp", O_RDWR | O_CREAT | O_APPEND);
if((pid = fork()) == 0)
{
for (int i = 0; i < 10000; i++)
{
write(fd, ccc.c_str(), ccc.size());
}
exit(0);
}
for (int i = 0; i < 50000; i++)
{
write(fd, mmm.c_str(), mmm.size());
}
exit(0);
}
上图程序运行出错结果,图中可以看出父子进程一直在交替写文件,导致文件内容混乱: