我相信代码正在尝试,但有时会失败,以解决以下情况:
- 父进程还关闭了部分或全部三个标准流(标准输入、标准输出和标准错误),因此部分或全部文件描述符
0、1 和2 已关闭。
- 父进程打开了三个文件描述符,作为子进程的标准输入、标准输出和标准错误。
有几个子场景:
-
child_stdout 被分配给 0 (STDIN_FILENO)。可以想象,child_stderr 被分配到了1 (STDOUT_FILENO)。
-
child_stderr 被分配到 0 或 1 — 大概意思是 child_stdout 没有被分配到其中任何一个。
-
child_stdout 和 child_stderr 均未分配给 0、1 或 2。
代码分叉;父代码立即返回——这一切都很干净。它在失败时返回 -1,在成功时返回 PID。
对于子场景 1,第一个条件运行 dup(),它将 child_stdout 的分配更改为 0 之后可用的第一个未打开文件描述符。这可能是1 或2 或更大的数字。
它会丢弃有关 child_stdout 最初是什么的信息。
然后循环尝试确保child_stderr 既不是0 也不是1,再次丢弃有关它最初是什么的信息。
接下来的三个调用确保当前在child_stdin 中的文件描述符被复制到STDIN_FILENO,child_stdout 被复制到STDOUT_FILENO,child_stderr 被复制到STDERR_FILENO。由于dup2() 不会关闭与要复制的文件描述符相同的原始文件描述符(否则会关闭),因此最终会产生合理的连接。
但是,在正常情况下,输入参数是(例如)3、5 和 7,代码不能确保这些参数已关闭。
这是一个错误。
然后代码继续使用execvp() 执行命令。它不处理失败的情况;它只是从函数的末尾掉线,导致未定义的行为,因为函数应该返回一个值。如果exec*() 系列函数中的任何函数返回,则它已失败。代码可能应该报告错误消息并退出,可能使用exit() 或者可能使用“快速退出”(_exit()、_Exit() 或类似的东西)。
如何解决?
我认为代码应该保留子文件描述符的原始值,并准备在dup2() 序列之后关闭它们,如果它们超出范围0,1,@ 987654367@。除此之外,该代码可能会处理大多数情况。
请注意,在重定向标准错误后报告错误是令人担忧的。下面的代码只是写入当前的标准错误,这是它可以做的最好的事情,除非你使用syslog 或类似的系统。
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define RC_CHECK(test) assert((test) != 0)
int run_child(char *progname, char *argv[], int child_stdin, int child_stdout, int child_stderr);
int run_child(char *progname, char *argv[], int child_stdin, int child_stdout, int child_stderr)
{
int child;
if ((child = fork()))
{
return child;
}
int fd[3] = { child_stdin, child_stdout, child_stderr };
if (child_stdout == STDIN_FILENO)
{
child_stdout = dup(child_stdout);
RC_CHECK(child_stdout >= 0);
}
while (child_stderr == STDIN_FILENO || child_stderr == STDOUT_FILENO)
{
child_stderr = dup(child_stderr);
RC_CHECK(child_stderr >= 0);
}
child_stdin = dup2(child_stdin, STDIN_FILENO);
RC_CHECK(child_stdin == STDIN_FILENO);
child_stdout = dup2(child_stdout, STDOUT_FILENO);
RC_CHECK(child_stdout == STDOUT_FILENO);
child_stderr = dup2(child_stderr, STDERR_FILENO);
RC_CHECK(child_stderr == STDERR_FILENO);
for (int i = 0; i < 3; i++)
{
if (fd[i] != STDIN_FILENO && fd[i] != STDOUT_FILENO && fd[i] != STDERR_FILENO)
close(fd[i]);
}
execvp(progname, argv);
/* Or: fprintf(stderr, "Failed to execute program %s\n", progname); */
char *msg[] = { "Failed to execute program ", progname, "\n" };
enum { NUM_MSG = sizeof(msg) / sizeof(msg[0]) };
for (int i = 0; i < NUM_MSG; i++)
write(2, msg[i], strlen(msg[i]));
exit(1); /* Or an alternative status such as 126 or 127 based on errno */
}
这不是简单的代码;你的思绪会被这些可能性所震撼。 (我已经删除了各种特殊情况,经过进一步检查,在编写我的分析时证明并不特别。)我不相信用dup() 修复的预检查是值得的; run_child() 的编写者可以简单地规定所有三个文件描述符 0、1 和 2 都是打开的,因此所有子文件描述符参数都大于 2 并简单地继续 I/ O 重定向。当然,仍然需要关闭传递给函数的文件描述符。