Implementing a Bidirectional Communication popen() Using Pipes

这篇博客介绍了一个C++实现的popen2函数,用于创建子进程并进行双向通信。通过管道实现了标准输入输出的重定向,允许父进程与子进程同时读写。文中还提供了一个测试客户端,展示了如何使用这个函数与`cat`命令进行数据交换。此外,讨论了在C++中使用智能指针改进popen2函数的可能性。

Though this is much easier to do if we use UNIX sockets : )
Let’s cut to the chase and go straight to the code (like we always do :^)

Header file popen2.h:

#ifndef POPEN2_H
#define POPEN2_H

#include <stdio.h>
#include <sys/types.h>

// We might also want to use these in C, or provide an interface
// for high-level languages like python (though pybind may seem
// to be a better option, or is it? :).
#ifdef __cplusplus
extern "C" {
#endif

// kinda miss default values
struct subprocess_t {
    pid_t cpid;
    int exit_code;
    FILE* p_stdin;  // child process's stdin, writable
    FILE* p_stdout; // child process's stdout, readable
};

// though I think calling it `popen1` will be more appropriate :)
/*
 * This version enables the calling process to both read from and
 * write to the program's stdout and stdin, respectively.
 */
struct subprocess_t popen2(const char* program);

int pclose2(subprocess_t* p);

#ifdef __cplusplus
}
#endif

#endif // POPEN2_H

Implementation file popen2.cc:

#include "popen2.h"
#include <list>
#include <algorithm>
#include <cerrno>
#include <sys/wait.h>
#include <unistd.h>

void init_subprocess(subprocess_t* p)
{
    p->cpid = -1;
    p->exit_code = 0;
    p->p_stdin = nullptr;
    p->p_stdout = nullptr;
}

// thread unsafe :)
static std::list<subprocess_t> opened_processes;

subprocess_t popen2(const char* program)
{
    subprocess_t subprocess;
    // although already initialized (in C++), but maybe not what we want
    init_subprocess(&subprocess);
    // We will need 2 pipes in order to communicate between parent and child.
    // If only using 1 pipe, both child and parent will hold two ends of the
    // pipe, and now if the parent is going to write to the pipe, data may
    // get read from parent immediately, without going to the child.
    int pipe_pc[2];  // write: P -> C  /  read: C <- P
    int pipe_cp[2];  // write: C -> P  /  read: P <- C
    if (pipe(pipe_pc) < 0) {
        return subprocess;
    }
    if (pipe(pipe_cp) < 0) {
        close(pipe_pc[0]);
        close(pipe_pc[1]);
        return subprocess;
    }

    pid_t pid = fork();
    if (pid == -1) { // failed
        close(pipe_pc[0]);
        close(pipe_pc[1]);
        close(pipe_cp[0]);
        close(pipe_cp[1]);
        return subprocess;
    }
    else if (pid == 0) { // child
        // close unrelated files inherited from parent
        for (const auto& p : opened_processes) {
            close(fileno(p.p_stdin));
            close(fileno(p.p_stdout));
        }

        close(pipe_cp[0]); // close unused read  end in pipe C -> P
        close(pipe_pc[1]); // close unused write end in pipe P -> C

        // redirect stdout to write end of the pipe
        if (pipe_cp[1] != STDOUT_FILENO) {
            dup2(pipe_cp[1], STDOUT_FILENO);
            close(pipe_cp[1]);
        }
        // redirect stdin to read end of the pipe
        if (pipe_pc[0] != STDIN_FILENO) {
            dup2(pipe_pc[0], STDIN_FILENO);
            close(pipe_pc[0]);
        }

        // launch the shell to run the program
        const char* argp[] = {"sh", "-c", program, nullptr};
        execve("/bin/sh", (char**) argp, environ);
        // shouldn't be reached here
        _exit(127);
    }
    close(pipe_pc[0]); // close unused read  end in pipe P -> C
    close(pipe_cp[1]); // close unused write end in pipe C -> P
    // parent (client) reads from the subprocess's stdout and
    //                 writes to the subprocess's stdin
    subprocess.p_stdout = fdopen(pipe_cp[0], "r");
    subprocess.p_stdin  = fdopen(pipe_pc[1], "w");
    subprocess.cpid = pid;
    opened_processes.push_back(subprocess);
    return subprocess;
}

int pclose2(subprocess_t* p)
{
    auto it = std::find_if(opened_processes.begin(), opened_processes.end(),
                           [&](const subprocess_t& process) {
                               return process.cpid == p->cpid;
                           });
    if (it == opened_processes.end()) {
        return -1;
    }

    fclose(p->p_stdin);
    fclose(p->p_stdout);

    pid_t pid = p->cpid;
    int pstat;
    do {
        pid = waitpid(pid, &pstat, 0);
    } while (pid == -1 && errno == EINTR);

    // obtain the subprocess's exit code
    if (WIFEXITED(pstat)) {
        p->exit_code = WEXITSTATUS(pstat);
    }
    else if (WIFSIGNALED(pstat)) {
        p->exit_code = WTERMSIG(pstat);
    }

    opened_processes.erase(it);
    return (pid == -1 ? -1 : pstat);
}

Test client:

#include "popen2.h"
#include <cassert>

void write_data(subprocess_t& p, const char* data)
{
    // write data to p's stdin
    fprintf(p.p_stdin, "%s", data);
    fflush(p.p_stdin); // IMPORTANT, to make sure child receives the data
}

void read_data(const subprocess_t& p, char* data)
{
    // read data from p's stdout
    fscanf(p.p_stdout, "%s", data);
    //fflush(p.p_stdout);
}

int main()
{
    char buf[5][10]{};
    subprocess_t subprocess = popen2("cat");
    assert(subprocess.cpid != -1);

    // communicate with the subprocess
    // for the test subprocess `cat`, we need a '\n' in each write
    write_data(subprocess, "one\n");
    read_data(subprocess, buf[0]);

    write_data(subprocess, "two\n");
    write_data(subprocess, "three\n");
    read_data(subprocess, buf[1]);
    read_data(subprocess, buf[2]);

    write_data(subprocess, "four\n");
    read_data(subprocess, buf[3]);

    write_data(subprocess, "five\n");
    read_data(subprocess, buf[4]);

    pclose2(&subprocess);
    for (char * s : buf)
        fprintf(stdout, "%s\n", s);
    return 0;
}

Final thought:
In popen2.cc, we can incorperate pclose2() to the destructor of
subprocess_t in C++ (given we need to copy the subprocess returned
from popen2(), we may need to add a shared pointer).

class PopenedSubprocess {
    shared_ptr<subprocess_t> p {}; // shared pointer to the process
public:
    PopenedSubprocess(const char* program) {
        p = make_shared<subprocess_t>(popen2(program));
    }

    ~PopenedSubprocess() {
        if (p.use_count() == 1) // the last one
            pclose2(p.get());
    }
};

I just made it a repo so that you can run the tests directly. : )

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值