为什么要两次调用fork()函数

24

Nagios让我配置child_processes_fork_twice=<0/1>

文档说:

该选项确定Nagios在执行主机和服务检查时是否会fork()子进程两次。默认情况下,Nagios会fork()两次。但是,如果启用了use_large_installation_tweaks选项,则只会fork()一次。

据我所知,fork()将生成一个新的子进程。为什么要做两次呢?


@larsmans 谢谢你提供的链接。当我第一次研究这个问题时,它不在我的列表上。我在那里学到了很多知识,并点了一些赞。 - DerMike
5个回答

79

首先,什么是僵尸进程?

它是一个已经死亡的进程,但其父进程正在忙于执行其他任务,因此无法收集子进程的退出状态。

在某些情况下,子进程运行时间非常长,父进程无法等待那么久,并会继续执行其工作(请注意,父进程不会死亡,而是继续执行其剩余任务,但不关心子进程)。

这样就创建了一个僵尸进程。


现在让我们开始正题。为什么要进行两次fork才能解决问题?

需要注意的重要事项是孙子进程执行了父进程希望其子进程执行的工作。

现在第一次调用fork时,第一个子进程只是再次调用fork并退出。这样,父进程不必等待很长时间来收集子进程的退出状态(因为子进程的唯一工作是创建另一个子进程并退出)。因此,第一个子进程不会成为僵尸进程。

至于孙子进程,其父进程已经死亡。因此,孙子进程将被init进程接管,后者始终收集其所有子进程的退出状态。现在父进程不必等待很长时间,也不会产生僵尸进程。


还有其他避免僵尸进程的方法;这只是一种常见技术。


希望这可以帮助!


9
啊,人际关系很复杂。而且init是个善良的老人,收养了孙子孙女。 - CubicleSoft

26

在Linux中,通常通过fork两次并在中间进程退出后创建守护进程。这样会导致孙子进程成为孤儿进程。因此,如果进程终止,操作系统就需要负责清理它。原因与所谓的僵尸进程有关。这些进程由于它们的父进程已经死亡,通常应该负责清理工作,因此在退出后仍然存在并消耗资源。


32
我不明白这样做比只分叉一次更好在哪里。我认为真正的原因与会话和控制终端有关,而不是孤立进程,但我可能错了... - R.. GitHub STOP HELPING ICE
1
主要原因是如果您在登录会话中启动守护进程,双重分叉将使守护进程进程的父进程为init(pid 1),当您注销会话时,SIGHUP不会杀死该进程。这与僵尸进程无关,因为通常僵尸进程的主要原因是父进程没有“wait()”终止的子进程,并且操作系统正在保留子进程的返回值,等待其父进程获取。因此,在僵尸进程中,进程已经退出,但由操作系统保留,因此并非真正死亡,因此被称为僵尸。 - user658991

5

此外,根据文档的说明:

通常情况下,当Nagios执行主机和服务检查时,它会fork()两次。这样做是为了(1)确保高度抗插件失效和段错误,并且(2)使操作系统在孙进程退出后清理孙进程。


4

Unix编程常见问题解答 §1.6.2:

1.6.2 How do I prevent them from occuring?

You need to ensure that your parent process calls wait() (or waitpid(), wait3(), etc.) for every child process that terminates; or, on some systems, you can instruct the system that you are uninterested in child exit states.

Another approach is to fork() twice, and have the immediate child process exit straight away. This causes the grandchild process to be orphaned, so the init process is responsible for cleaning it up. For code to do this, see the function fork2() in the examples section.

To ignore child exit states, you need to do the following (check your system's manpages to see if this works):

     struct sigaction sa;
     sa.sa_handler = SIG_IGN;
 #ifdef SA_NOCLDWAIT
     sa.sa_flags = SA_NOCLDWAIT;
 #else
     sa.sa_flags = 0;
 #endif
     sigemptyset(&sa.sa_mask);
     sigaction(SIGCHLD, &sa, NULL);

If this is successful, then the wait() functions are prevented from working; if any of them are called, they will wait until all child processes have terminated, then return failure with errno == ECHILD.

The other technique is to catch the SIGCHLD signal, and have the signal handler call waitpid() or wait3(). See the examples section for a complete program.


9
只有在子进程(或孙子进程)启动后,父进程还将继续运行时,这种方法才有意义。如果整个程序是守护进程并且父进程无论如何都会立即退出,那么至少就孤立进程而言,我认为双重fork没有任何好处。 - R.. GitHub STOP HELPING ICE

2
这段代码演示了如何使用双重 fork 方法,使得孙子进程能够被 init 接管,同时避免出现僵尸进程的风险。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
    pid_t p1 = fork();

    if (p1 != 0)
    {
        printf("p1 process id is %d", getpid());
        wait();
        system("ps");
    }
    else
    {
        pid_t p2 = fork();
        int pid = getpid();

        if (p2 != 0) 
        {
            printf("p2 process id is %d", pid);
        }
        else
        {
            printf("p3 process id is %d", pid);
        }

        exit(0);
    }
}

父进程将fork新的子进程,然后等待其完成。子进程将fork出一个孙进程,然后调用exit(0)退出。
在这种情况下,孙进程除了调用exit(0)不执行任何操作,但可以让它执行你希望守护进程执行的任何操作。孙进程可能存在很长时间,并且在完成时将被init进程回收。

7
可以提供一份说明。 - Scott Solmer
1
Op实际上是想知道为什么程序被写成你所写的那个样子。 - nj-ath
@MichaelGaskill 谢谢您的编辑。我会删除我的评论。 - Box Box Box Box

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接