追踪子进程的终止

25

如何在不让父进程等待子进程被杀死的情况下追踪子进程的结束?

我正在尝试一个客户端-服务器场景,其中服务器接受来自客户端的连接并为每个接受的连接派生一个新的进程。

我忽略SIGCHLD信号以防止僵尸进程的创建。

signal(SIGCHLD, SIG_IGN);
while(1)
{
  accept();
  clients++;
  if(fork() ==0)
  {
     childfunction();
     clients--;
  }
  else
  {
  }
}
在上述情况下的问题是,如果在 childfunction()函数中杀掉子进程,则全局变量 clients 不会被减少。
注意:我正在寻找一种不使用SIGCHLD信号的解决方案... 如果可能的话。

你可以在SIGCHLD的信号处理程序中执行某些操作。 - tristan
我已经提到了... SIGCHLD信号被忽略了吗? - codingfreak
14
对于标题和开场白极具戏剧性,点赞一下。D: - user241244
4个回答

28

通常你需要为SIGCHLD编写一个处理程序,该处理程序在pid -1上调用waitpid()。您可以使用该函数的返回值来确定哪个pid已经死亡。例如:

void my_sigchld_handler(int sig)
{
    pid_t p;
    int status;

    while ((p=waitpid(-1, &status, WNOHANG)) != -1)
    {
       /* Handle the death of pid p */
    }
}

/* It's better to use sigaction() over signal().  You won't run into the
 * issue where BSD signal() acts one way and Linux or SysV acts another. */

struct sigaction sa;

memset(&sa, 0, sizeof(sa));
sa.sa_handler = my_sigchld_handler;

sigaction(SIGCHLD, &sa, NULL);

另外,您可以使用指定子进程ID的 waitpid(pid, &status, 0),同步等待其死亡。或者使用 WNOHANG 在不阻塞的情况下检查其状态。


但在我的示例中,SIGCHLD被忽略了吗? - codingfreak
1
@codingfreak,我建议你重新评估一下。你不需要忽略它来避免僵尸进程。当你使用waitpid()时,“僵尸进程”就会消失。 - asveikau
3
这正是SIGCHLD信号的用途所在。 - caf
@asveikau - 我在我的应用程序中尝试使用SIGCHLD,但它无法正确清理僵尸进程。请查看我的问题,因为我已经更新了我的示例应用程序。 - codingfreak
6
在您的chld_handler函数中,那个if需要改成while ((p = waitpid(-1, &status, WNOHANG)) > 0)。(因为如果两个或更多子进程很快相继退出,您可能只会收到一个信号)。 - caf
显示剩余2条评论

9
到目前为止,没有一种解决方案可以不使用SIGCHLD作为问题请求的方法。以下是使用poll的替代方法的实现,如此答案所述(它还解释了为什么在这种情况下应避免使用SIGCHLD):

确保您对创建的每个子进程都有一个管道。它可以是它们的stdin/stdout/stderr或只是额外的虚拟文件描述符。当子进程终止时,它的管道末端将关闭,您的主事件循环将检测到该文件描述符上的活动。从它关闭的事实中,您可以识别出子进程已经停止运行,并调用waitpid来收回僵尸进程。

(注意:出于简洁起见,我省略了一些最佳实践,例如错误检查和清理文件描述符)
/**
 * Specifies the maximum number of clients to keep track of.
 */
#define MAX_CLIENT_COUNT 1000

/**
 * Tracks clients by storing their process IDs and pipe file descriptors.
 */
struct process_table {
    pid_t clientpids[MAX_CLIENT_COUNT];
    struct pollfd clientfds[MAX_CLIENT_COUNT];
} PT;

/**
 * Initializes the process table. -1 means the entry in the table is available.
 */
void initialize_table() {
    for (int i = 0; i < MAX_CLIENT_COUNT; i++) {
        PT.clientfds[i].fd = -1;
    }
}

/**
 * Returns the index of the next available entry in the process table.
 */
int get_next_available_entry() {
    for (int i = 0; i < MAX_CLIENT_COUNT; i++) {
        if (PT.clientfds[i].fd == -1) {
            return i;
        }
    }
    return -1;
}

/**
 * Adds information about a new client to the process table.
 */
void add_process_to_table(int i, pid_t pid, int fd) {
    PT.clientpids[i] = pid;
    PT.clientfds[i].fd = fd;
}

/**
 * Removes information about a client from the process table.
 */
void remove_process_from_table(int i) {
    PT.clientfds[i].fd = -1;
}

/**
 * Cleans up any dead child processes from the process table.
 */
void reap_zombie_processes() {
    int p = poll(PT.clientfds, MAX_CLIENT_COUNT, 0);

    if (p > 0) {
        for (int i = 0; i < MAX_CLIENT_COUNT; i++) {
            /* Has the pipe closed? */
            if ((PT.clientfds[i].revents & POLLHUP) != 0) {
                // printf("[%d] done\n", PT.clientpids[i]);
                waitpid(PT.clientpids[i], NULL, 0);
                remove_process_from_table(i);
            }
        }
    }
}

/**
 * Simulates waiting for a new client to connect.
 */
void accept() {
    sleep((rand() % 4) + 1);
}

/**
 * Simulates useful work being done by the child process, then exiting.
 */
void childfunction() {
    sleep((rand() % 10) + 1);
    exit(0);
}

/**
 * Main program
 */
int main() {
    /* Initialize the process table */
    initialize_table();

    while (1) {
        accept();

        /* Create the pipe */
        int p[2];
        pipe(p);

        /* Fork off a child process. */
        pid_t cpid = fork();

        if (cpid == 0) {
            /* Child process */
            close(p[0]);
            childfunction();
        }
        else {
            /* Parent process */
            close(p[1]);
            int i = get_next_available_entry();
            add_process_to_table(i, cpid, p[0]);
            // printf("[%d] started\n", cpid);
            reap_zombie_processes();
        }
    }

    return 0;
}

以下是取消注释printf语句后运行程序的示例输出:
[31066] started
[31067] started
[31068] started
[31069] started
[31066] done
[31070] started
[31067] done
[31068] done
[31071] started
[31069] done
[31072] started
[31070] done
[31073] started
[31074] started
[31072] done
[31075] started
[31071] done
[31074] done
[31081] started
[31075] done

3

您不想要一个僵尸进程。如果子进程死亡,而父进程仍在运行但从未发出wait()/waitpid()调用以收集状态,则系统不会释放与子进程相关联的资源,留下一个僵尸/defunct进程在proc表中。

尝试将您的SIGCHLD处理程序更改为以下内容:


void chld_handler(int sig) {
    pid_t p;
    int status;

    /* loop as long as there are children to process */
    while (1) {

       /* retrieve child process ID (if any) */
       p = waitpid(-1, &status, WNOHANG);

       /* check for conditions causing the loop to terminate */
       if (p == -1) {
           /* continue on interruption (EINTR) */
           if (errno == EINTR) {
               continue;
           }
           /* break on anything else (EINVAL or ECHILD according to manpage) */
           break;
       }
       else if (p == 0) {
           /* no more children to process, so break */
           break;
       }

       /* valid child process ID retrieved, process accordingly */
       ...
    }   
}

您可以使用sigprocmask()在信号处理程序执行期间选择性地屏蔽/阻止其他SIGCHLD信号。但是,当信号处理例程完成时,必须返回被阻塞的掩码到其原始值。
如果您真的不想使用SIGCHLD处理程序,可以尝试将子进程处理循环添加到定期调用它且轮询已终止的子进程的某个位置。

根据您的建议进行更改后,我没有看到任何僵尸进程被创建。让我看看它在HTTP服务器守护程序中的表现如何... - codingfreak
我不预见在您的守护程序中使用此代码会有任何重大问题。 - jschmier

1

在fork()之后,变量'clients'位于不同的进程地址空间中,当您在子进程中减少变量时,这不会影响父进程中的值。我认为您需要处理SIGCHLD以正确处理计数。


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