【问题标题】:Can popen() make bidirectional pipes like pipe() + fork()?popen() 可以制作像 pipe() + fork() 这样的双向管道吗?
【发布时间】:2011-04-22 11:55:33
【问题描述】:

我正在用 C++(主要是 C)在模拟文件系统上实现管道。它需要在主机外壳中运行命令,但在模拟文件系统上执行管道本身。

我可以使用pipe()fork()system() 系统调用来实现这一点,但我更喜欢使用popen()(它处理创建管道、分叉进程以及将命令传递给贝壳)。这可能是不可能的,因为(我认为)我需要能够从管道的父进程写入,在子进程端读取,从子进程写回输出,最后从父进程读取该输出。我系统上popen() 的手册页说双向管道是可能的,但我的代码需要在旧版本仅支持单向管道的系统上运行。

通过上面的单独调用,我可以打开/关闭管道来实现这一点。 popen() 可以吗?

举个简单的例子,要运行ls -l | grep .txt | grep cmds,我需要:

  • 打开管道和进程在主机上运行ls -l;回读其输出
  • ls -l 的输出传送回我的模拟器
  • ls -l 的管道输出上打开一个管道和进程以在主机上运行grep .txt
  • 通过管道将其输出回模拟器(卡在此处)
  • grep .txt 的管道输出上打开一个管道和进程以在主机上运行grep cmds
  • 通过管道将其输出回模拟器并打印出来

人爆表

来自 Mac OS X:

popen() 函数“打开”一个 通过创建一个双向的过程 管道、分叉和​​调用 shell。 以前的popen() 打开的任何流 父进程中的调用被关闭 在新的子进程中。 历史上,popen() 已实现 带单向管;因此, 仅popen() 的许多实现 允许模式参数指定 阅读或写作,而不是两者兼而有之。因为 popen() 现在使用 双向管道,模式参数 可以请求双向数据流。 mode 参数是一个指向 必须为空终止的字符串 'r' 用于阅读,'w' 用于写作,或 'r+' 用于读写。

【问题讨论】:

  • 忽略这本糟糕的手册,这使得管道的单向性看起来像是一种遗留行为。您的实现行为是非标准的,在其他环境中可能永远不会得到支持。只需自己设置管道、fork 和 exec,一切都会很开心。
  • 这整个线程都是由win组成的,所有答案和问题都值得+1。
  • 我刚刚在 Ubuntu 13.04 上做了一个 man popen,它指出:Since a pipe is by definition unidirectional, the type argument may specify only reading or writing, not both; the resulting stream is correspondingly read-only or write-only. 我很惊讶这是“旧”的 popen...

标签: c fork pipe popen dup2


【解决方案1】:

POSIX 规定 popen() 调用并非旨在提供双向通信:

popen() 的模式参数是一个指定 I/O 模式的字符串:

  1. 如果mode为r,则子进程启动时,其文件描述符STDOUT_FILENO应为管道的可写端,调用进程中的文件描述符fileno(stream),其中stream为popen返回的流指针(),应该是管道的可读端。
  2. 如果mode是w,当子进程启动时,它的文件描述符STDIN_FILENO应该是管道的可读端,调用进程中的文件描述符fileno(stream),其中stream是popen返回的流指针( ),应该是管道的可写端。
  3. 如果 mode 是任何其他值,则结果未指定。

除此之外,任何可移植代码都不会做任何假设。 BSD popen() 与您的问题描述的类似。

此外,管道与套接字不同,每个管道文件描述符都是单向的。您必须创建两个管道,为每个方向配置一个。

【讨论】:

    【解决方案2】:

    您似乎已经回答了自己的问题。如果您的代码需要在不支持popen 打开双向管道的旧系统上运行,那么您将无法使用popen(至少不是提供的那个)。

    真正的问题是有关旧系统的确切功能。特别是,他们的pipe 是否支持创建双向管道?如果他们有一个可以创建双向管道的pipe,但popen 不能,那么我将编写代码的主要流以将popen 与双向管道一起使用,并提供@ 的实现987654327@ 可以使用双向管道,在需要的地方编译。

    如果您需要支持足够老的系统以至于pipe 只支持单向管道,那么您几乎只能自己使用pipeforkdup2 等。我可能仍会将其封装在一个几乎类似于现代版popen 的函数中,但不是返回一个文件句柄,而是填充一个带有两个文件句柄的小结构,一个用于孩子的stdin,另一个为孩子的stdout

    【讨论】:

    • 我认为您已经帮助我意识到我的问题实际上是“双向功能是否‘常见’?”,但似乎并非如此。感谢您的好评。
    • +1 表示一个方向的 popen + pipe() 表示另一个方向。使我免于重写大多数 cout 语句,而只需要更改 cin。
    【解决方案3】:

    我建议您编写自己的函数来为您进行管道/分叉/系统化。您可以让该函数产生一个进程并返回读/写文件描述符,如...

    typedef void pfunc_t (int rfd, int wfd);
    
    pid_t pcreate(int fds[2], pfunc_t pfunc) {
        /* Spawn a process from pfunc, returning it's pid. The fds array passed will
         * be filled with two descriptors: fds[0] will read from the child process,
         * and fds[1] will write to it.
         * Similarly, the child process will receive a reading/writing fd set (in
         * that same order) as arguments.
        */
        pid_t pid;
        int pipes[4];
    
        /* Warning: I'm not handling possible errors in pipe/fork */
    
        pipe(&pipes[0]); /* Parent read/child write pipe */
        pipe(&pipes[2]); /* Child read/parent write pipe */
    
        if ((pid = fork()) > 0) {
            /* Parent process */
            fds[0] = pipes[0];
            fds[1] = pipes[3];
    
            close(pipes[1]);
            close(pipes[2]);
    
            return pid;
    
        } else {
            close(pipes[0]);
            close(pipes[3]);
    
            pfunc(pipes[2], pipes[1]);
    
            exit(0);
        }
    
        return -1; /* ? */
    }
    

    您可以在其中添加所需的任何功能。

    【讨论】:

      【解决方案4】:

      无需在每个进程中创建两个管道并浪费一个文件描述符。只需使用套接字即可。 https://stackoverflow.com/a/25177958/894520

      【讨论】:

      • 您能否添加更多详细信息,为什么每个进程的文件描述符会是“浪费”?我的意思是它的成本是多少?域套接字哪个更便宜?
      • 我也不相信在这种情况下需要套接字。但如果它确实比管道更好,我想知道如何以及为什么。
      • "在 UNIX 系统中,套接字描述符被实现为文件描述符。" - 那我猜最后还是一样
      • 您还需要什么详细信息?您需要两个管道来支持双向通信,每个管道使用 2 个文件描述符。您只需要一个套接字来支持双向通信,因此它总共只使用了 2 个文件描述符。如果你正在编写一个可以处理大量客户端连接的服务器,那么描述符的成本就会增加。
      【解决方案5】:

      netresolve 后端之一中,我正在与脚本交谈,因此我需要写入它的stdin 并从它的stdout 中读取。以下函数使用重定向到管道的 stdin 和 stdout 执行命令。您可以使用它并根据自己的喜好对其进行调整。

      static bool
      start_subprocess(char *const command[], int *pid, int *infd, int *outfd)
      {
          int p1[2], p2[2];
      
          if (!pid || !infd || !outfd)
              return false;
      
          if (pipe(p1) == -1)
              goto err_pipe1;
          if (pipe(p2) == -1)
              goto err_pipe2;
          if ((*pid = fork()) == -1)
              goto err_fork;
      
          if (*pid) {
              /* Parent process. */
              *infd = p1[1];
              *outfd = p2[0];
              close(p1[0]);
              close(p2[1]);
              return true;
          } else {
              /* Child process. */
              dup2(p1[0], 0);
              dup2(p2[1], 1);
              close(p1[0]);
              close(p1[1]);
              close(p2[0]);
              close(p2[1]);
              execvp(*command, command);
              /* Error occured. */
              fprintf(stderr, "error running %s: %s", *command, strerror(errno));
              abort();
          }
      
      err_fork:
          close(p2[1]);
          close(p2[0]);
      err_pipe2:
          close(p1[1]);
          close(p1[0]);
      err_pipe1:
          return false;
      }
      

      https://github.com/crossdistro/netresolve/blob/master/backends/exec.c#L46

      (我在popen simultaneous read and write中使用了相同的代码)

      【讨论】:

        【解决方案6】:

        这是代码(C++,但可以很容易地转换为 C):

        #include <unistd.h>
        
        #include <cstdlib>
        #include <cstdio>
        #include <cstring>
        #include <utility>
        
        // Like popen(), but returns two FILE*: child's stdin and stdout, respectively.
        std::pair<FILE *, FILE *> popen2(const char *__command)
        {
            // pipes[0]: parent writes, child reads (child's stdin)
            // pipes[1]: child writes, parent reads (child's stdout)
            int pipes[2][2];
        
            pipe(pipes[0]);
            pipe(pipes[1]);
        
            if (fork() > 0)
            {
                // parent
                close(pipes[0][0]);
                close(pipes[1][1]);
        
                return {fdopen(pipes[0][1], "w"), fdopen(pipes[1][0], "r")};
            }
            else
            {
                // child
                close(pipes[0][1]);
                close(pipes[1][0]);
        
                dup2(pipes[0][0], STDIN_FILENO);
                dup2(pipes[1][1], STDOUT_FILENO);
        
                execl("/bin/sh", "/bin/sh", "-c", __command, NULL);
        
                exit(1);
            }
        }
        

        用法:

        int main()
        {
            auto [p_stdin, p_stdout] = popen2("cat -n");
        
            if (p_stdin == NULL || p_stdout == NULL)
            {
                printf("popen2() failed\n");
                return 1;
            }
        
            const char msg[] = "Hello there!";
            char buf[32];
        
            printf("I say \"%s\"\n", msg);
        
            fwrite(msg, 1, sizeof(msg), p_stdin);
            fclose(p_stdin);
        
            fread(buf, 1, sizeof(buf), p_stdout);
            fclose(p_stdout);
        
            printf("child says \"%s\"\n", buf);
        
            return 0;
        }
        
        

        可能的输出:

        I say "Hello there!"
        child says "     1      Hello there!"
        

        【讨论】:

          猜你喜欢
          • 2014-05-31
          • 2012-04-25
          • 1970-01-01
          • 1970-01-01
          • 2020-05-07
          • 2013-01-15
          • 1970-01-01
          • 1970-01-01
          • 2010-12-04
          相关资源
          最近更新 更多