【问题标题】:How to convert from popen() to fork() and not duplicate process memory?如何从 popen() 转换为 fork() 而不是复制进程内存?
【发布时间】:2015-03-28 02:01:32
【问题描述】:

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

我认为可以通过使用fork() 来简化管道代码,但想知道如何删除所有父级的堆栈/内存,这在 fork 的子级中不需要?这是旧工作代码的 sn-p:

#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 计算签名、增量或合并文件。

【问题讨论】:

  • 内存空间便宜。复制它很便宜。 fork 很便宜。 (也许您将内存空间与内存混淆了?)您认为popen 如何创建新进程?你的另一个选择是某种spawn,但很难说,因为你还没有真正解释孩子要做什么。它会运行其他程序吗?
  • 如果我的进程分配了 500MB 的堆空间,我调用了 fork,子进程是否得到了父进程堆空间的副本?
  • 您不能复制空间。孩子从与父母相同的地址映射开始。映射的页面是共享的,直到任一进程修改它们为止。
  • 因此,如果父级在子级运行时接触了大多数页面,那么您将不必要地得到每个此类页面的两个副本,原始的(现在在子级中未使用)和在父级中的修改后的副本。如果可能的话,我想避免这种潜在的开销,因为孩子需要很少的父母的记忆。
  • 如果您可以在调用fork 之后立即调用exec(或相关函数),那么您可以避免这种内存污染。您甚至可以exec自己(即您当前正在运行的同一可执行文件),使用命令行参数告诉自己以某种特定模式运行。

标签: c++ pipe exec fork popen


【解决方案1】:

首先,popenfork() 之上的一个非常薄的包装器,然后是exec() [还有一些调用pipedup 等等来管理管道的末端]。

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

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

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

【讨论】:

    猜你喜欢
    • 2020-08-17
    • 1970-01-01
    • 1970-01-01
    • 2014-11-30
    • 1970-01-01
    • 2013-03-30
    • 2019-03-17
    • 2019-06-10
    • 1970-01-01
    相关资源
    最近更新 更多