从popen()读取的输出是否在pclose()之前完成

Is output read from popen()ed FILE* complete before pclose()?

本文关键字:pclose 输出 popen 读取 是否      更新时间:2023-10-16

pclose()的手册页上写着:

pclose()函数等待相关进程终止,并返回wait4(2)返回的命令退出状态。

我觉得这意味着,如果popen()创建的关联FILE*是用类型"r"打开的,以便读取command的输出,那么直到调用pclose()之后,您才真正确定输出是否完成。但是在pclose()之后,闭合的FILE*肯定是无效的,那么你怎么能确定你已经读取了command的整个输出呢?

为了举例说明我的问题,请考虑以下代码:

// main.cpp
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
int main( int argc, char* argv[] )
{
FILE* fp = popen( "someExecutableThatTakesALongTime", "r" );
if ( ! fp )
{
std::cout << "popen failed: " << errno << " " << strerror( errno )
<< std::endl;
return 1;
}
char buf[512] = { 0 };
fread( buf, sizeof buf, 1, fp );
std::cout << buf << std::endl;
// If we're only certain the output-producing process has terminated after the
// following pclose(), how do we know the content retrieved above with fread()
// is complete?
int r = pclose( fp );
// But if we wait until after the above pclose(), fp is invalid, so
// there's nowhere from which we could retrieve the command's output anymore,
// right?
std::cout << "exit status: " << WEXITSTATUS( r ) << std::endl;
return 0;
}

我的问题,如上所述:如果我们只确定生成输出的子进程在pclose()之后已经终止,我们如何知道用fread()检索的内容是完整的?但是,如果我们等到pclose()之后,fp就无效了,那么我们就再也无法检索命令的输出了,对吧?

这感觉像是一个鸡和蛋的问题,但我已经看到了与上面类似的代码,所以我可能误解了一些东西。我很感激你对此的解释。

TL;DR执行摘要:我们如何知道用fread()检索到的内容是完整的--我们有EOF。

当子进程关闭其管道末端时,会得到一个EOF。当它显式调用close退出时,可能会发生这种情况。在那之后,没有什么能从你的管道末端出来。在得到EOF之后,你不知道进程是否已经终止,但你可以肯定的是,它永远不会向管道写入任何内容。

通过调用pclose,您可以关闭管道的末端等待子进程的终止。当pclose返回时,您知道子级已终止。

如果你在没有得到EOF的情况下调用pclose,而孩子试图把东西写到它的管道末端,它就会失败(事实上,它会得到SIGPIPE,很可能会死)。

这里绝对没有鸡和蛋的地方。

在进一步研究这个问题时,我学到了一些东西,我认为这回答了我的问题:

本质上:是的,从popenpclose之前返回的FILE*fread是安全的。假设给fread的缓冲区足够大,则不会"错过"给popencommand生成的输出。

回过头来仔细考虑fread的作用:它有效地阻止,直到(size*nmemb)字节被读取或遇到文件结尾(或错误)。

多亏了不使用popen的C-pipe,我更好地理解了popen在后台的作用:它执行dup2,将其stdout重定向到它使用的管道的写端。重要的是:它执行某种形式的exec,以在分叉进程中执行指定的command,并且在该子进程终止后,其打开的文件描述符,包括1(stdout)将关闭。即,终止指定的command是子进程‘stdout’关闭的条件。

接下来,我回过头来仔细思考EOF在这种情况下到底是什么。起初,我有一种松散而错误的印象,认为">fread试图以最快的速度从FILE*中读取,并在读取最后一个字节后返回/取消阻止"。这并不完全正确:如上所述:fread将读取/阻止,直到读取其目标字节数或遇到EOF或错误。popen返回的FILE*来自popen使用的管道读取端的fdopen,因此当子进程'stdout(与管道写入端一起使用dup2)关闭时,其EOF发生。

因此,最终我们得到的是:popen创建一个管道,其写端获得运行指定command的子进程的输出,如果fdopen指向传递给freadFILE*,则其读端。(假设fread的缓冲区足够大),fread将阻塞,直到出现EOF,这对应于由于执行command的终止而导致的popen管道的写入端的关闭。也就是说,由于fread在遇到EOF之前一直处于阻塞状态,而EOF发生在command(在popen的子进程中运行)终止之后,因此可以安全地使用fread(具有足够大的缓冲区)来捕获commandpopen的完整输出。

如果有人能证实我的推论和结论,我将不胜感激。

仔细阅读popen的文档:

pclose()函数应关闭由popen()打开的流,等待命令终止,并返回运行命令语言解释器的进程的终止状态。

它阻塞并等待。

popen()只是fork、dup2、execv、fdopen等系列的快捷方式。它将使我们能够轻松地通过文件流操作访问子STDOUT、STDIN。

popen()之后,父进程和子进程都独立执行。pclose()不是一个"kill"函数,它只是等待子进程终止。由于它是一个阻塞函数,在执行pclose()期间生成的输出数据可能会丢失。

为了避免这些数据丢失,只有当我们知道子进程已经终止时,我们才会调用pclose():fgets()调用将从阻塞返回NULL或fread(),共享流到达末尾,EOF()将返回true。

下面是一个将popen()与fread()结合使用的示例。如果执行过程失败,此函数返回-1,如果正常,则返回0。子输出数据在szResult中返回。

int exec_command( const char * szCmd, std::string & szResult ){
printf("Execute commande : [%s]n", szCmd );
FILE * pFile = popen( szCmd, "r");
if(!pFile){
printf("Execute commande : [%s] FAILED !n", szCmd );
return -1;
}
char buf[256];
//check if the output stream is ended.
while( !feof(pFile) ){
//try to read 255 bytes from the stream, this operation is BLOCKING ...
int nRead = fread(buf, 1, 255, pFile);
//there are something or nothing to read because the stream is closed or the program catch an error signal
if( nRead > 0 ){
buf[nRead] = '';
szResult += buf;
}
}
//the child process is already terminated. Clean it up or we have an other zoombie in the process table.
pclose(pFile); 
printf("Exec command [%s] return : n[%s]n",  szCmd, szResult.c_str() );
return 0;
}

注意,返回流上的所有文件操作都在BLOCKING模式下进行,流是打开的,没有O_NONBLOCK标志。当子进程挂起并且nerver终止时,fread()可能会被永远阻止,所以只能对受信任的程序使用popen()。

为了对子进程进行更多的控制,避免文件阻塞操作,我们应该自己使用fork/vfork/exelv等,用O_NONBLOCK标志修改管道打开的属性,不时使用poll()或select()来确定是否有一些数据,然后使用read()函数从管道中读取。

定期将waitpid()与WNOHANG一起使用,以查看子进程是否已终止。