c - 管道保证在孩子退出后关闭
问题描述
在下面的代码中,依靠read() 失败来检测孩子的终止是否安全?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
int pipefd[2];
pipefd[0] = 0;
pipefd[1] = 0;
pipe(pipefd);
pid_t pid = fork();
if (pid == 0)
{
// child
close(pipefd[0]); // close unused read end
while ((dup2(pipefd[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {} // send stdout to the pipe
while ((dup2(pipefd[1], STDERR_FILENO) == -1) && (errno == EINTR)) {} // send stderr to the pipe
close(pipefd[1]); // close unused write end
char *argv[3];
argv[0] = "worker-app";
argv[1] = NULL;
argv[2] = NULL;
execvp("./worker-app", argv);
printf("failed to execvp, errno %d\n", errno);
exit(EXIT_FAILURE);
}
else if (pid == -1)
{
}
else
{
// parent
close(pipefd[1]); // close the write end of the pipe in the parent
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
while (1) // <= here is it safe to rely on read below to break from this loop ?
{
ssize_t count = read(pipefd[0], buffer, sizeof(buffer)-1);
printf("pipe read return %d\n", (int)count);
if (count > 0)
{
printf("child: %s\n", buffer);
}
else if (count == 0)
{
printf("end read child pipe\n", buffer);
break;
}
else if (count == -1)
{
if (errno == EINTR)
{ continue;
}
printf("error read child pipe\n", buffer);
break;
}
}
close(pipefd[0]); // close read end, prevent descriptor leak
int waitStatus = 0;
waitpid(pid, &waitStatus, 0);
}
fprintf(stdout, "All work completed :-)\n");
return EXIT_SUCCESS;
}
我应该在 while(1) 循环中添加一些东西来检测子终止吗?可能会发生什么特定情况并破坏此应用程序?
下面是一些改进的想法。但是我会浪费 CPU 周期吗?
使用带有特殊参数 0 的 kill 不会终止进程,而只是检查它是否响应:
if (kill(pid, 0)) { break; /* child exited */ };
/* 如果 sig 为 0,则不发送信号,但仍会执行错误检查;这可用于检查进程 ID 或进程组 ID 是否存在。https://linux.die.net/man/2/kill */在 while(1) 循环中使用 waitpid 非阻塞来检查孩子是否已经退出。
使用 select() 检查管道可读性以防止 read() 可能挂起?
谢谢!
解决方案
关于你的想法:
- 如果孩子产生了自己的孩子,
read()
则不会返回 0,直到其所有后代都死亡或关闭 stdout 和 stderr。如果不是这样,或者如果孩子总是比它的所有后代活得更久,那么等待read()
返回 0 就足够了,并且永远不会引起问题。 - 如果孩子死了但父母还没有接受
wait(2)
它,那么kill(pid, 0)
就会成功,就好像孩子还活着一样(至少在 Linux 上),所以这不是从你的父程序中进行的有效检查。 - 非阻塞
waitpid()
本身似乎可以解决孩子拥有自己的孩子的问题,但实际上会引入微妙的竞争条件。如果孩子在 之后waitpid()
但在 之前退出read()
,则read()
将阻塞,直到其余后代退出。 - 就其本身而言,如果您
select()
以阻塞方式使用,它并不比仅调用read()
. 如果您select()
以非阻塞方式使用,您最终只会在循环中消耗 CPU 时间。
我会做什么:
- 为 SIGCHLD 添加一个无操作信号处理函数,以便它在发生时导致 EINTR。
- 在开始循环之前阻止父级中的 SIGCHLD。
- 使用 non-blocking
read
s,并使用pselect(2)
to block 来避免永远旋转 CPU。 - 在 期间
pselect
,传入一个sigset_t
没有阻塞 SIGCHLD 的参数,这样当它最终被发送时,它保证会为它产生一个 EINTR。 - 在循环中的某个地方,做一个非阻塞
waitpid(2)
,并适当地处理它的返回。(确保在阻止 SIGCHLD 之后但在第一次调用之前至少执行一次select
,否则您将遇到竞争条件。)
推荐阅读
- python - 无法使用 selenium python 在谷歌驱动器预览模式下定位元素
- swift - 在 SwiftUI 中使用 Tab Bar 弹出到根视图
- tfs - 当管理员强制解锁特定文件时,本地更改会发生什么?
- html - 修复导航栏上的位置,使其进入网络中间
- c++ - 为什么在 c/c++ 中,开关的优化方式与链式 if else 不同?
- c# - 将 appSettings 保存到外部文件
- azure - 循环遍历租户中的几个 Azure 订阅(不是全部),使用每个循环 Powershell 来获取 RG 数据
- forms - Symfony 3.2 集合类型
- haskell - Haskell - 新类型上的 iso
- r - 当循环有增量时避免 for 循环