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

被折叠的 条评论
为什么被折叠?



