【发布时间】: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";
}
}
在我的应用程序中以这种方式创建了两种具有不同任务的子进程:
- 通过 SSH 连接到另一台计算机并调用一个服务器,该服务器将与作为客户端的父级进行通信。
- 使用 rsync 计算签名、增量或合并文件。
【问题讨论】:
-
内存空间便宜。复制它很便宜。
fork很便宜。 (也许您将内存空间与内存混淆了?)您认为popen如何创建新进程?你的另一个选择是某种spawn,但很难说,因为你还没有真正解释孩子要做什么。它会运行其他程序吗? -
如果我的进程分配了 500MB 的堆空间,我调用了 fork,子进程是否得到了父进程堆空间的副本?
-
您不能复制空间。孩子从与父母相同的地址映射开始。映射的页面是共享的,直到任一进程修改它们为止。
-
因此,如果父级在子级运行时接触了大多数页面,那么您将不必要地得到每个此类页面的两个副本,原始的(现在在子级中未使用)和在父级中的修改后的副本。如果可能的话,我想避免这种潜在的开销,因为孩子需要很少的父母的记忆。
-
如果您可以在调用
fork之后立即调用exec(或相关函数),那么您可以避免这种内存污染。您甚至可以exec自己(即您当前正在运行的同一可执行文件),使用命令行参数告诉自己以某种特定模式运行。