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_wait
被 SIGCHLD
中断时,您才能确定 waitpid
会立即 return 终止子进程的状态。
我认为无法保证 OS 更改已终止进程的状态所需的时间,尽管通常这应该是“短时间”。
此外,如果您终止该进程,它可能需要一些时间才能终止。
当您知道您的程序已成功创建子进程时,您实际上应该等待子进程结束。
一种可能的实现方式是重复waitpid(..., WNOHANG)
直到它 return 成为预期的 PID(或直到发生超时)。
另一种选择是为 SIGALRM
使用信号处理程序,并且
在调用之前调用 alarm
或 setitimer
定义超时
waitpid
没有 WNOHANG
.
如果您的父进程将在子进程结束后终止,您甚至可以省略 waitpid
并让 init
进程(或负责的收割者)执行此操作。
顺便说一句:
来自 epoll_wait
的 return 代码 -1
并不一定意味着它被信号中断了。即使使用 errno==EINTR
,您也不能确定它是 SIGCHLD
,因此您应该实施更多检查,也许与设置标志的信号处理程序结合使用。
epoll_wait
超时并不一定意味着您的子进程已终止,它可能只是出于某种原因变慢,因此您可能必须在等待它之前终止子进程。
编辑:
我不建议无条件发送 SIGKILL
,因为这可能会阻止子进程进行清理。 (您知道关闭文件描述符是子进程必须执行的最后一步,或者没有什么可清理的吗?)如果您不能干净地终止进程,SIGKILL
应该只用作最后一个选项。
我有一个函数,它使用 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_wait
被 SIGCHLD
中断时,您才能确定 waitpid
会立即 return 终止子进程的状态。
我认为无法保证 OS 更改已终止进程的状态所需的时间,尽管通常这应该是“短时间”。
此外,如果您终止该进程,它可能需要一些时间才能终止。
当您知道您的程序已成功创建子进程时,您实际上应该等待子进程结束。
一种可能的实现方式是重复
waitpid(..., WNOHANG)
直到它 return 成为预期的 PID(或直到发生超时)。另一种选择是为
SIGALRM
使用信号处理程序,并且 在调用之前调用alarm
或setitimer
定义超时waitpid
没有WNOHANG
.
如果您的父进程将在子进程结束后终止,您甚至可以省略 waitpid
并让 init
进程(或负责的收割者)执行此操作。
顺便说一句:
来自 epoll_wait
的 return 代码 -1
并不一定意味着它被信号中断了。即使使用 errno==EINTR
,您也不能确定它是 SIGCHLD
,因此您应该实施更多检查,也许与设置标志的信号处理程序结合使用。
epoll_wait
超时并不一定意味着您的子进程已终止,它可能只是出于某种原因变慢,因此您可能必须在等待它之前终止子进程。
编辑:
我不建议无条件发送 SIGKILL
,因为这可能会阻止子进程进行清理。 (您知道关闭文件描述符是子进程必须执行的最后一步,或者没有什么可清理的吗?)如果您不能干净地终止进程,SIGKILL
应该只用作最后一个选项。