【问题标题】:Interaction between pipes, execvpe, and shell scripts管道、execvpe 和 shell 脚本之间的交互
【发布时间】:2021-01-15 07:26:09
【问题描述】:

我有以下代码分叉和执行一个 shell 脚本,并将其 STDERR 和 STDOUT 重定向到父进程。

#define _GNU_SOURCE

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define BUFLEN 65536

int main () {
  int pipes[2];
  pipe(pipes);
  pid_t pid = fork();
  if (pid == -1) {return 1;}
  else if (pid > 0) {
    close(pipes[1]);
    char buf[BUFLEN];
    size_t n = read(pipes[0], buf, sizeof(buf));
    int stat = 0;
    waitpid(pid, &stat, 0);
    printf("%.*s", n, buf);
  } else {
    dup2(pipes[1], STDOUT_FILENO);
    dup2(pipes[1], STDERR_FILENO);
    close(pipes[0]);
    char *const argv[] = {"sh", "test", NULL};
    execvpe(argv[0], argv, environ);
  }

  return 0;
}

作为一个最小的工作示例"test" 是:

#!/bin/bash
cat file.txt
echo "Hello!"
echo "Goodbye!"

C 程序的输出是file.txt 的内容,然后回显的输出会丢失。如果是三个 echo 语句,那么它们都会被看到。

我最好的猜测是 echo 是一个内置的 shell,shell 将为cat 分叉,我的管道将丢失。在我的项目中,脚本中调用的第一个命令似乎都丢失了。

如果我的假设是正确的,我如何从 execvpe 产生的任何和所有子级收集所有输出?

【问题讨论】:

  • 您需要循环读取直到read 返回0(这意味着管道的另一端已关闭)。或者直到出现错误,当然(当read 返回-1 时)。

标签: c linux shell posix


【解决方案1】:

我认为问题只是时间的组合,而不是在停止从管道读取之前检查 EOF。如果将read() 调用包装到一个循环中以读取所有内容,则会读取所有数据。当cat 完成时,read() 返回所有可用的内容。 echo 命令的输出随后被添加到管道中,但根本不读取。

我相信这段代码演示了。请注意,execvpe() 不是 POSIX 标准(在 macOS 上不可用),所以我使用自己的代理标头 #include "execvpe.h" 和实现 execvpe.c 来获得它的实现。另外,POSIX 没有定义声明environ 的标头,所以我也声明了它。您可能正在使用 Linux,并且那里的系统头文件修复了 POSIX 留下的一些空白。

这是工作代码和数据。

pipe17.c

/* SO 6412-3757 */
#define _GNU_SOURCE

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdio.h>

#define BUFLEN 65536

#include "execvpe.h"    /* execvpe() is not in POSIX */
extern char **environ;  /* No POSIX header declares environ */

int main(void)
{
    int pipes[2];
    pipe(pipes);
    pid_t pid = fork();
    if (pid == -1)
    {
        return 1;
    }
    else if (pid > 0)
    {
        close(pipes[1]);
        char buffer[BUFLEN];
        char *b_str = buffer;
        size_t b_len = sizeof(buffer);
        size_t t_len = 0;
        ssize_t n_len;
        while (b_len > 0 && (n_len = read(pipes[0], b_str, b_len)) > 0)
        {
            b_str += n_len;
            b_len -= n_len;
            t_len += n_len;
        }
        close(pipes[0]);
        int status = 0;
        int corpse = waitpid(pid, &status, 0);
        printf("%d 0x%.4X: [[%.*s]]\n", corpse, status, (int)t_len, buffer);
    }
    else
    {
        dup2(pipes[1], STDOUT_FILENO);
        dup2(pipes[1], STDERR_FILENO);
        close(pipes[0]);
        close(pipes[1]);
        char *argv[] = {"sh", "test", NULL};
        execvpe(argv[0], argv, environ);
        fprintf(stderr, "failed to execute '%s'\n", argv[0]);
        exit(1);
    }

    return 0;
}

test

#!/bin/bash
cat file.txt
echo "Hello!"
echo "Goodbye!"
echo "Errors go to standard error" >&2

file.txt

Line 1 of file1.txt
The very last line of file1.txt

样本输出

14171 0x0000: [[Line 1 of file1.txt
The very last line of file1.txt
Hello!
Goodbye!
Errors go to standard error
]]

请注意,代码在调用execvpe() 之前关闭了管道的两端。这在这里并不重要,但这样做通常很重要。您的原始代码将size_tn 传递给printf(),以供* 以格式使用。在 sizeof(int) == sizeof(size_t) 的 64 位机器上,你可能会侥幸逃脱,但它会在 sizeof(int) &lt; sizeof(size_t) 的 64 位机器上产生编译警告。


经验法则:如果您 dup2() 管道的一端到标准输入或标准输出,关闭两者 返回的原始文件描述符 pipe() 尽早。 特别是,您应该在使用任何 exec*() 函数族。

如果您使用以下任一方式复制描述符,该规则也适用 dup() 要么 fcntl() F_DUPFDF_DUPFD_CLOEXEC

【讨论】:

  • 这是一个非常彻底的答案,循环是我更复杂的项目代码中的问题(它确实将一个 int 传递给 printf phew)。你说得对,我正在开发的软件是专门为 Linux 开发的,谢谢!