【问题标题】:Linux: Executing child process with piped stdin/stdoutLinux:使用管道标准输入/标准输出执行子进程
【发布时间】:2012-03-13 10:35:31
【问题描述】:

使用 Linux 和 C++,我想要一个执行以下操作的函数:

string f(string s)
{
    string r = system("foo < s");
    return r;
}

显然上述方法不起作用,但你明白了。我有一个字符串s,我想将它作为应用程序“foo”的子进程执行的标准输入传递,然后我想将其标准输出记录到字符串r,然后返回。

我应该使用哪种 Linux 系统调用或 POSIX 函数组合? 我使用的是 Linux 3.0,不需要该解决方案来处理旧系统。

【问题讨论】:

  • @Joe:Linux 是一个操作系统。 3.0 是一个版本号。我指出的是仅适用于 Linux 3.0.0 和更新版本的平台特定答案。
  • 那我建议你查一下系统调用pipeforkdup2exec。或许可以查看this one之类的教程。
  • 如果不出意外,您应该执行与system 等效的操作,但信号处理要更加明智......并且可能还可以省略外壳。
  • (在 5 年多后发表评论,因为这恰好出现在头版。)Linux 是一个内核。 Linux 内核 3.0 版于 2011 年 7 月 21 日发布。有大量使用 Linux 内核的操作系统(这些系统通常被称为“Linux”)。此类操作系统的版本号往往不相关;如果不查看它们的发布历史,您将无法判断 Fedora X 是比 Debian Y 新还是旧。我的猜测是 OP 正在使用某些未指定发行版的 3.0 版。我们不知道是哪一个,但这可能并不重要。

标签: c++ c linux posix glibc


【解决方案1】:

由 erpini 提供的代码不能像写的那样工作。请注意,例如,在父级中关闭的管道末端将在以后使用。看看

close(wpipefd[1]); 

以及随后对该关闭描述符的写入。这只是换位,但它表明这个代码从未被使用过。下面是我测试过的版本。不幸的是,我改变了代码风格,所以这没有被接受为对 erpini 代码的编辑。

唯一的结构变化是我只重定向子路径中的 I/O(注意 dup2 调用仅在子路径中。)这非常重要,否则父 I/O 会搞砸。感谢 erpini 提供的最初答案,我在开发这个答案时使用了它。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define PIPE_READ 0
#define PIPE_WRITE 1

int createChild(const char* szCommand, char* const aArguments[], char* const aEnvironment[], const char* szMessage) {
  int aStdinPipe[2];
  int aStdoutPipe[2];
  int nChild;
  char nChar;
  int nResult;

  if (pipe(aStdinPipe) < 0) {
    perror("allocating pipe for child input redirect");
    return -1;
  }
  if (pipe(aStdoutPipe) < 0) {
    close(aStdinPipe[PIPE_READ]);
    close(aStdinPipe[PIPE_WRITE]);
    perror("allocating pipe for child output redirect");
    return -1;
  }

  nChild = fork();
  if (0 == nChild) {
    // child continues here

    // redirect stdin
    if (dup2(aStdinPipe[PIPE_READ], STDIN_FILENO) == -1) {
      exit(errno);
    }

    // redirect stdout
    if (dup2(aStdoutPipe[PIPE_WRITE], STDOUT_FILENO) == -1) {
      exit(errno);
    }

    // redirect stderr
    if (dup2(aStdoutPipe[PIPE_WRITE], STDERR_FILENO) == -1) {
      exit(errno);
    }

    // all these are for use by parent only
    close(aStdinPipe[PIPE_READ]);
    close(aStdinPipe[PIPE_WRITE]);
    close(aStdoutPipe[PIPE_READ]);
    close(aStdoutPipe[PIPE_WRITE]); 

    // run child process image
    // replace this with any exec* function find easier to use ("man exec")
    nResult = execve(szCommand, aArguments, aEnvironment);

    // if we get here at all, an error occurred, but we are in the child
    // process, so just exit
    exit(nResult);
  } else if (nChild > 0) {
    // parent continues here

    // close unused file descriptors, these are for child only
    close(aStdinPipe[PIPE_READ]);
    close(aStdoutPipe[PIPE_WRITE]); 

    // Include error check here
    if (NULL != szMessage) {
      write(aStdinPipe[PIPE_WRITE], szMessage, strlen(szMessage));
    }

    // Just a char by char read here, you can change it accordingly
    while (read(aStdoutPipe[PIPE_READ], &nChar, 1) == 1) {
      write(STDOUT_FILENO, &nChar, 1);
    }

    // done with these in this example program, you would normally keep these
    // open of course as long as you want to talk to the child
    close(aStdinPipe[PIPE_WRITE]);
    close(aStdoutPipe[PIPE_READ]);
  } else {
    // failed to create child
    close(aStdinPipe[PIPE_READ]);
    close(aStdinPipe[PIPE_WRITE]);
    close(aStdoutPipe[PIPE_READ]);
    close(aStdoutPipe[PIPE_WRITE]);
  }
  return nChild;
}

【讨论】:

  • 我正在尝试做类似的事情,但对我来说,我相信这段代码会导致以下流程出现死锁: parent: write, then read;孩子:标准输入,然后是标准输出。如果我删除任何一个读取,问题就会消失。代码正是 Ammo 编写的。 (父母先读后子写不会卡住)
  • 哦,我现在明白你的评论了。示例程序在父端执行阻塞读取(...),因此如果子节点停止发送任何内容并且不关闭标准输出,它实际上将永远阻塞。这超出了这个示例程序的范围,这只是为了让分叉和重定向正确。您将需要编写自己的应用程序逻辑来在父子节点之间进行通信。
  • 这里是一个完全可怕的应用程序协议(在 MacOS 上运行,应该在 Linux 上运行)的要点,它只发送一个 hello world 消息并假设没有任何问题。你当然会为生产代码编写一个真正的应用程序协议,但至少你可以看到它在这里运行。 gist.github.com/derammo/e2802f9e4a713633901c7c5390388b78
【解决方案2】:

由于您希望对进程进行双向访问,因此您必须执行 popen 在幕后使用管道显式执行的操作。我不确定这是否会在 C++ 中发生任何变化,但这是一个纯 C 示例:

void piped(char *str){
    int wpipefd[2];
    int rpipefd[2];
    int defout, defin;
    defout = dup(stdout);
    defin = dup (stdin);
    if(pipe(wpipefd) < 0){
            perror("Pipe");
            exit(EXIT_FAILURE);
    }
    if(pipe(rpipefd) < 0){
            perror("Pipe");
            exit(EXIT_FAILURE);
    }
    if(dup2(wpipefd[0], 0) == -1){
            perror("dup2");
            exit(EXIT_FAILURE);
    }
    if(dup2(rpipefd[1], 1) == -1){
            perror("dup2");
            exit(EXIT_FAILURE);
    }
    if(fork() == 0){
            close(defout);
            close(defin);
            close(wpipefd[0]);
            close(wpipefd[1]);
            close(rpipefd[0]);
            close(rpipefd[1]);
            //Call exec here. Use the exec* family of functions according to your need
    }
    else{
            if(dup2(defin, 0) == -1){
                    perror("dup2");
                    exit(EXIT_FAILURE);
            }
            if(dup2(defout, 1) == -1){
                    perror("dup2");
                    exit(EXIT_FAILURE);
            }
            close(defout);
            close(defin);
            close(wpipefd[1]);
            close(rpipefd[0]);
            //Include error check here
            write(wpipefd[1], str, strlen(str));
            //Just a char by char read here, you can change it accordingly
            while(read(rpipefd[0], &ch, 1) != -1){
                    write(stdout, &ch, 1);
            }
    }

}

实际上你这样做了:

  1. 创建管道并将 stdout 和 stdin 重定向到两个管道的末端(请注意,在 linux 中,pipe() 创建单向管道,因此您需要使用两个管道来实现您的目的)。
  2. Exec 现在将启动一个新进程,该进程具有标准输入和标准输出管道的末端。
  3. 关闭未使用的描述符,将字符串写入管道,然后开始读取进程可能转储到另一个管道的任何内容。

dup() 用于在文件描述符表中创建重复条目。虽然 dup2() 改变了描述符指向的内容。

注意:正如 Ammo@ 在他的解决方案中提到的,我上面提供的或多或少是一个模板,如果您只是尝试执行代码,它将不会运行,因为显然缺少 exec*(函数系列) ,所以孩子将在 fork() 之后几乎立即终止。

【讨论】:

  • 当他们随后使用 dup2 重定向时,为什么要使用 dup 保留对 stdin/stdout 的引用? dup2 为你隐式关闭它的第二个参数。
  • 你说得对,那是不必要的。上面Ammo的回答是正确的做法,请参考。
  • 显然,您希望将dup2() 调用放在if(fork()==0) 部分的close() 行之前,而不是放在else 部分中。 if() 部分中需要它们。
【解决方案3】:

Ammo 的代码有一些错误处理错误。子进程在 dup 失败后返回而不是退出。也许可以将子副本替换为:

    if (dup2(aStdinPipe[PIPE_READ], STDIN_FILENO) == -1 ||
        dup2(aStdoutPipe[PIPE_WRITE], STDOUT_FILENO) == -1 ||
        dup2(aStdoutPipe[PIPE_WRITE], STDERR_FILENO) == -1
        ) 
    {
        exit(errno); 
    }

    // all these are for use by parent only
    close(aStdinPipe[PIPE_READ]);
    close(aStdinPipe[PIPE_WRITE]);
    close(aStdoutPipe[PIPE_READ]);
    close(aStdoutPipe[PIPE_WRITE]);

【讨论】:

  • 你是对的。子进程不应从该堆栈帧返回,也不应尝试打印错误。可能没有人注意到,因为这从未发生过。
  • return 替换为exit() 是正确的。我认为“不要尝试打印错误”是正确的。没有其他进程可以报错;标准错误用于报告错误。很少有程序会静默失败。
最近更新 更多