24

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

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

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

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

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

男人弹出

从 Mac OS X:

popen()函数通过创建双向管道、分叉和​​调用 shell 来“打开”一个进程。父进程中先前popen() 调用打开的任何流都在新的子进程中关闭。从历史上看,popen()是用单向管道实现的;因此,许多实现popen()只允许 mode 参数指定读取或写入,而不是两者。因为 popen()现在使用双向管道实现,所以 mode 参数可能会请求双向数据流。mode 参数是一个指向以 null 结尾的字符串的指针,该字符串必须是 'r' 用于读取,'w' 用于写入,或 'r+' 用于读取和写入。

4

6 回答 6

20

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

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; /* ? */
}

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

于 2010-10-07T17:55:42.900 回答
10

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

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

如果您需要支持pipe仅支持单向管道的足够老的系统,那么您几乎只能自己使用pipe, fork,dup2等。我可能仍然会将它包装在一个几乎现代版本的.popenstdinstdout

于 2010-10-07T17:32:18.113 回答
9

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

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

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

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

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

于 2010-10-07T17:29:20.383 回答
4

在一个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 同时读写中使用了相同的代码)

于 2017-02-23T17:19:38.417 回答
2

这是代码(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!"
于 2020-10-14T18:49:24.387 回答
1

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

于 2014-08-07T08:51:58.930 回答