关于fork(),关乎进程

引言

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

上图程序运行出错结果,图中可以看出父子进程一直在交替写文件,导致文件内容混乱:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值