如何从popen()转换为fork()而不是重复进程内存?

时间:2015-01-28 23:52:37

标签: c++ pipe exec fork popen

我有一个多线程C ++ 03应用程序,目前使用popen()在新进程中再次调用自身(相同的二进制文件)和ssh(不同的二进制文件)并读取输出,但是,当移植到Android NDK这会带来一些问题,例如无权访问ssh,因此我将Dropbear ssh链接到我的应用程序以尝试避免该问题。此外,我目前的popen解决方案要求将stdout和stderr合并为一个FD,这有点凌乱,我想停止这样做。

我认为可以通过使用fork()来简化管道代码,但是想知道如何丢弃fork的子代中不需要的所有父代的堆栈/内存?以下是旧工作代码的片段:

#include <iostream>
#include <stdio.h>
#include <string>
#include <errno.h>

using std::endl;
using std::cerr;
using std::cout;
using std::string;

void
doPipe()
{
  // Redirect stderr to stdout with '2>&1' so that we see any error messages
  // in the pipe output.
  const string selfCmd = "/path/to/self/binary arg1 arg2 arg3 2>&1";
  FILE *fPtr = ::popen(selfCmd.c_str(), "r");
  const int bufSize = 4096;
  char buf[bufSize + 1];

  if (fPtr == NULL) {
    cerr << "Failed attempt to popen '" << selfCmd << "'." << endl;
  } else {
    cout << "Result of: '" << selfCmd << "':\n";

    while (true) {
      if (::fgets(buf, bufSize, fPtr) == NULL) {
        if (!::feof(fPtr)) {
          cerr << "Failed attempt to fgets '" << selfCmd << "'." << endl;
        }
        break;
      } else {
        cout << buf;
      }
    }

    if (pclose(fPtr) == -1) {
      if (errno != 10) {
        cerr << "Failed attempt to pclose '" << selfCmd << "'." << endl;
      }
    }

    cout << "\n";
  }
}

到目前为止,这是我为转换为fork()而做的松散,但fork不必要地重复整个父进程内存空间。此外,它不太有用,因为父对象从未在pipe()读取的outFD上看到EOF。我需要在哪里关闭FD以使其工作?如何在不提供二进制路径(在Android上不容易获得)的情况下执行execlp()之类的操作,而是重新使用相同的二进制文件和带有新参数的空白图像?

#include <iostream>
#include <stdio.h>
#include <string>
#include <errno.h>

using std::endl;
using std::cerr;
using std::cout;
using std::string;

int
selfAction(int argc, char *argv[], int &outFD, int &errFD)
{
  pid_t childPid; // Process id used for current process.

  // fd[0] is the read end of the pipe and fd[1] is the write end of the pipe.
  int fd[2];      // Pipe for normal communication between parent/child.
  int fdErr[2];   // Pipe for error  communication between parent/child.

  // Create a pipe for IPC between child and parent.
  const int pipeResult = pipe(fd);

  if (pipeResult) {
    cerr << "selfAction normal pipe failed: " << errno << ".\n";

    return -1;
  }

  const int errorPipeResult = pipe(fdErr);

  if (errorPipeResult) {
    cerr << "selfAction error pipe failed: " << errno << ".\n";

    return -1;
  }

  // Fork - error.
  if ((childPid = fork()) < 0) {
    cerr << "selfAction fork failed: " << errno << ".\n";

    return -1;
  } else if (childPid == 0) { // Fork -> child.
    // Close read end of pipe.
    ::close(fd[0]);
    ::close(fdErr[0]);

    // Close stdout and set fd[1] to it, this way any stdout of the child is
    // piped to the parent.
    ::dup2(fd[1],    STDOUT_FILENO);
    ::dup2(fdErr[1], STDERR_FILENO);

    // Close write end of pipe.
    ::close(fd[1]);
    ::close(fdErr[1]);

    // Exit child process.
    exit(main(argc, argv));
  } else { // Fork -> parent.
    // Close write end of pipe.
    ::close(fd[1]);
    ::close(fdErr[1]);

    // Provide fd's to our caller for stdout and stderr:
    outFD = fd[0];
    errFD = fdErr[0];

    return 0;
  }
}


void
doFork()
{
  int argc = 4;
  char *argv[4] = { "/path/to/self/binary", "arg1", "arg2", "arg3" };
  int outFD = -1;
  int errFD = -1;
  int result = selfAction(argc, argv, outFD, errFD);

  if (result) {
    cerr << "Failed to execute selfAction." << endl;

    return;
  }

  FILE *outFile = fdopen(outFD, "r");
  FILE *errFile = fdopen(errFD, "r");

  const int bufSize = 4096;
  char buf[bufSize + 1];

  if (outFile == NULL) {
    cerr << "Failed attempt to open fork file." << endl;

    return;
  } else {
    cout << "Result:\n";

    while (true) {
      if (::fgets(buf, bufSize, outFile) == NULL) {
        if (!::feof(outFile)) {
          cerr << "Failed attempt to fgets." << endl;
        }
        break;
      } else {
        cout << buf;
      }
    }

    if (::close(outFD) == -1) {
      if (errno != 10) {
        cerr << "Failed attempt to close." << endl;
      }
    }

    cout << "\n";
  }

  if (errFile == NULL) {
    cerr << "Failed attempt to open fork file err." << endl;

    return;
  } else {
    cerr << "Error result:\n";

    while (true) {
      if (::fgets(buf, bufSize, errFile) == NULL) {
        if (!::feof(errFile)) {
          cerr << "Failed attempt to fgets err." << endl;
        }
        break;
      } else {
        cerr << buf;
      }
    }

    if (::close(errFD) == -1) {
      if (errno != 10) {
        cerr << "Failed attempt to close err." << endl;
      }
    }

    cerr << "\n";
  }
}

以这种方式创建了两种子进程,在我的应用程序中使用不同的任务:

  1. SSH到另一台计算机并调用将与作为客户端的父服务器通信的服务器。
  2. 使用rsync计算签名,增量或合并文件。

1 个答案:

答案 0 :(得分:2)

首先,popen是一个非常薄的包装器,位于fork()之后,后跟exec() [还有一些调用pipedup等等在管理管道的末端]。

其次,内存只是以“写时复制”内存的形式复制 - 这意味着除非其中一个进程写入某个页面,否则实际的物理内存将在两个进程之间共享。

当然,它确实意味着操作系统必须创建一个每4KB 4-8字节的内存映射[在典型情况下](可能加上一些内部操作系统数据来跟踪该页面和内容的副本数量 - 但只要页面与父进程保持相同,子页面就会使用父进程内部数据。与创建新流程和将可执行文件加载到新流程中所涉及的其他所有内容相比,这只是一小部分时间。由于你几乎立即做exec,所以不会触及很多父进程的记忆,所以很少会发生。

我的建议是,如果popen有效,请继续使用popen。如果popen由于某种原因不能完全符合您的要求,请使用fork + exec - 但请确保您知道这样做的原因是什么。