waitpid(WNOHANG) returns 0 即使 child 进程应该已经终止

waitpid(WNOHANG) returns 0 even though child process should have terminated

我有一个函数,它使用 fork()dup2()execvpe() 来执行一些外部应用程序,并为其标准文件描述符(stdin、stdout 和 stderr)提供 3 个管道. 在函数 returns 之后,parent 将使用这些管道将提供的缓冲区的内容 write() 提供给 stdin 以及 read() stdout 和 stderr 到另外两个缓冲区。 parent 在循环中执行此操作(使用 epoll_wait())直到所有三个管道 return EOF,这意味着这些文件描述符已被 child 关闭。 这一切都很好,并且从 stdout 和 stderr 管道读取的内容正是我所期望的,给定提供给 stdin 管道的输入。

然而,正如标题所暗示的,当 parent 然后尝试使用带有 WNOHANG 标志的 waitpid() 检查 child 的退出状态时,它 returns 0。 Apparent只有 child 仍然存在,即使那些文件描述符已经关闭。

这是相关代码,不幸的是,由于错误处理,代码比较长:

typedef struct
{
    int fd;
    char* name;
    char* buf;
    size_t buf_size;
    size_t buf_idx;
    bool eof;
    ssize_t (*func)(int, void*, size_t);
} pipe_info_t;

static pid_t execute(char* argv[], char* envp[], int* in, int* out, int* err);

int run_process(char* argv[], char* envp[],
                char* in_buf,
                char* out_buf, size_t* out_size,
                char* err_buf, size_t* err_size)
{
    int evt_cnt;
    int result = -1;
    int efd_pipe = -1;

    if (!argv || !in_buf
    ||  !out_buf || !out_size
    ||  !err_buf || !err_size)
    {
        fprintf(stderr, "%s() Invalid argument", __func__);
        goto ERR_ARG;
    }

    pipe_info_t in =
    {
        .name = "stdin",
        .buf = in_buf,
        .buf_size = strlen(in_buf),
        .buf_idx = 0,
        .eof = false,
        .func = (ssize_t (*)(int, void*, size_t))write
    };

    pipe_info_t out =
    {
        .name = "stdout",
        .buf = out_buf,
        .buf_size = *out_size,
        .buf_idx = 0,
        .eof = false,
        .func = read
    };

    pipe_info_t err =
    {
        .name = "stderr",
        .buf = err_buf,
        .buf_size = *err_size,
        .buf_idx = 0,
        .eof = false,
        .func = read
    };

    *out_size = 0;
    *err_size = 0;

    efd_pipe = epoll_create1(0);
    if (efd_pipe == -1)
    {
        fprintf(stderr, "%s() epoll_create1(): %s", __func__, strerror(errno));
        goto ERR_EPOLL_CREATE;
    }

    pid_t pid = execute(argv, envp, &in.fd, &out.fd, &err.fd);
    if (pid == -1)
    {
        fprintf(stderr, "%s() Failed to create child process", __func__);
        goto ERR_EXEC;
    }

    struct epoll_event in_evt = {.data.ptr = &in, .events = EPOLLOUT};
    struct epoll_event out_evt = {.data.ptr = &out, .events = EPOLLIN};
    struct epoll_event err_evt = {.data.ptr = &err, .events = EPOLLIN};
    struct epoll_event events[8];

    if (epoll_ctl(efd_pipe, EPOLL_CTL_ADD, in.fd, &in_evt)
    ||  epoll_ctl(efd_pipe, EPOLL_CTL_ADD, out.fd, &out_evt)
    ||  epoll_ctl(efd_pipe, EPOLL_CTL_ADD, err.fd, &err_evt))
    {
        fprintf(stderr, "%s() epoll_ctl(): %s", __func__, strerror(errno));
        goto ERR_EPOLL_CTL;
    }

    while (!(in.eof && out.eof && err.eof))
    {
        int n;
        evt_cnt = epoll_wait(efd_pipe, events, sizeof(events)/sizeof(events[0]), 5000);

        if (evt_cnt == -1)
        {
            fprintf(stderr, "%s() epoll_wait(): %s", __func__, strerror(errno));
            goto WAIT_CHILD;
        }

        if (evt_cnt == 0)
        {
            fprintf(stderr, "%s() epoll_wait(): timeout", __func__);
            goto WAIT_CHILD;
        }

        for (n=0; n<evt_cnt; ++n)
        {
            int size = 0;
            pipe_info_t* pipe = events[n].data.ptr;
            if (pipe->eof)
            {
                continue;
            }

            if (events[n].events & EPOLLERR)
            {
                fprintf(stderr, "%s() epoll_wait() %s error 0x%04X", __func__, pipe->name, events[n].events);
                goto WAIT_CHILD;
            }

            size = pipe->func(pipe->fd,
                              &pipe->buf[pipe->buf_idx],
                              pipe->buf_size - pipe->buf_idx);

            if (size == -1)
            {
                fprintf(stderr, "%s() %s %s", __func__, pipe->name, strerror(errno));
                goto WAIT_CHILD;
            }
            else if (!size)
            {
                pipe->eof = true;
            }

            pipe->buf_idx += size;
        }
    }

WAIT_CHILD:
    switch (waitpid(pid, &result, WNOHANG))
    {
        case -1:
            fprintf(stderr, "%s() waitpid(): %s", __func__, strerror(errno));
            result = -1;
        break;

        case 0:
            fprintf(stderr, "%s() Child process still alive", __func__);
            kill(pid, SIGKILL);
            waitpid(pid, &result, 0);
            result = -1;
        break;

        default:
            result = WEXITSTATUS(result);
        break;
    }

    *out_size = out.buf_idx;
    *err_size = err.buf_idx;

ERR_EPOLL_CTL:
    close(in.fd);
    close(out.fd);
    close(err.fd);
ERR_EXEC:
    close(efd_pipe);
ERR_EPOLL_CREATE:
ERR_ARG:
    return result;
}

static pid_t execute(char* argv[], char* envp[], int* in, int* out, int* err)
{
    pid_t pid = -1;

    int in_pipe[2];
    int out_pipe[2];
    int err_pipe[2];

    char path[strlen(argv[0])+1];
    memcpy(path, argv[0], sizeof(path));

    char* cmd = basename(path);

    if (pipe(in_pipe))
    {
        fprintf(stderr, "%s() pipe(stdin): %s", __func__, strerror(errno));
        goto ERR_STDIN;
    }

    if (pipe(out_pipe))
    {
        fprintf(stderr, "%s() pipe(stdout): %s", __func__, strerror(errno));
        goto ERR_STDOUT;
    }

    if (pipe(err_pipe))
    {
        fprintf(stderr, "%s() pipe(stderr): %s", __func__, strerror(errno));
        goto ERR_STDERR;
    }

    pid = fork();

    if (pid > 0)
    {
        close(in_pipe[0]);
        close(out_pipe[1]);
        close(err_pipe[1]);

        *in = in_pipe[1];
        *out = out_pipe[0];
        *err = err_pipe[0];
    }
    else if (pid == 0)
    {
        char err_str[1024];
        size_t err_size = 0;
        int err_fd = err_pipe[1];

        if (close(in_pipe[1])
        ||  close(out_pipe[0])
        ||  close(err_pipe[0]))
        {
            err_size = snprintf(err_str, sizeof(err_str), "%s(child) close(): %s\n", __func__, strerror(errno));
            goto ERR_CHILD;
        }

        if ((dup2(in_pipe[0], STDIN_FILENO) == -1)
        ||  (dup2(out_pipe[1], STDOUT_FILENO) == -1)
        ||  (dup2(err_pipe[1], STDERR_FILENO) == -1))
        {
            err_size = snprintf(err_str, sizeof(err_str), "%s(child) dup2(): %s\n", __func__, strerror(errno));
            goto ERR_CHILD;
        }

        err_fd = STDERR_FILENO;

        if (close(in_pipe[0])
        ||  close(out_pipe[1])
        ||  close(err_pipe[1]))
        {
            err_size = snprintf(err_str, sizeof(err_str), "%s(child) close(): %s\n", __func__, strerror(errno));
            goto ERR_CHILD;
        }

        if (execvpe(cmd, argv, envp?envp:__environ))
        {
            err_size = snprintf(err_str, sizeof(err_str), "%s(child) execvpe(): %s\n", __func__, strerror(errno));
        }

ERR_CHILD:
        write(err_fd, err_str, err_size);
        _exit(1);
    }
    else
    {
        fprintf(stderr, "%s() fork(): %s", __func__, strerror(errno));
        goto ERR_FORK;
    }

    return pid;

ERR_FORK:
    close(err_pipe[0]);
    close(err_pipe[1]);

ERR_STDERR:
    close(out_pipe[0]);
    close(out_pipe[1]);

ERR_STDOUT:
    close(in_pipe[0]);
    close(in_pipe[1]);

ERR_STDIN:
    return -1;
}

所以我的问题是:child 进程在关闭所有三个标准文件描述符后是否应该成为僵尸进程? 当然总是假设它们在进程终止期间自动关闭(我认为假设没有人会费心手动关闭它们是安全的)

如果您想知道为什么我不使用 waitpid() 作为循环条件的一部分:那是因为该循环正在使用 epoll_wait() 等待文件描述符上的事件。因此,如果没有进一步的事件(因为描述符已关闭),我将不得不等待超时到期。

该进程在关闭所有文件描述符后可能需要一些时间,直到其状态更改为已终止,因此您不能期望它在所有管道上检测到 EOF 后立即终止。

只有当 epoll_waitSIGCHLD 中断时,您才能确定 waitpid 会立即 return 终止子进程的状态。

我认为无法保证 OS 更改已终止进程的状态所需的时间,尽管通常这应该是“短时间”。

此外,如果您终止该进程,它可能需要一些时间才能终止。

当您知道您的程序已成功创建子进程时,您实际上应该等待子进程结束。

  • 一种可能的实现方式是重复waitpid(..., WNOHANG) 直到它 return 成为预期的 PID(或直到发生超时)。

  • 另一种选择是为 SIGALRM 使用信号处理程序,并且 在调用之前调用 alarmsetitimer 定义超时 waitpid 没有 WNOHANG.

如果您的父进程将在子进程结束后终止,您甚至可以省略 waitpid 并让 init 进程(或负责的收割者)执行此操作。

顺便说一句:

来自 epoll_wait 的 return 代码 -1 并不一定意味着它被信号中断了。即使使用 errno==EINTR,您也不能确定它是 SIGCHLD,因此您应该实施更多检查,也许与设置标志的信号处理程序结合使用。

epoll_wait 超时并不一定意味着您的子进程已终止,它可能只是出于某种原因变慢,因此您可能必须在等待它之前终止子进程。

编辑:

我不建议无条件发送 SIGKILL,因为这可能会阻止子进程进行清理。 (您知道关闭文件描述符是子进程必须执行的最后一步,或者没有什么可清理的吗?)如果您不能干净地终止进程,SIGKILL 应该只用作最后一个选项。