popen()函数读取的输出在pclose()函数执行前是否完整?

3
pclose()的手册页面如下所示:

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

我认为这意味着,如果使用popen()打开具有类型"r"以读取command的输出的相关FILE*,那么在调用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会无效,因此我们不能再从中检索命令的输出了,对吗?

这感觉像一个鸡生蛋的问题,但我在很多地方都看到类似上面的代码,所以我可能理解错了什么。我很感谢您对此进行解释。


读取直到读取返回错误。如果您尝试关闭并且进程被阻止尝试向满管道发送输出,则会挂起。 - stark
4个回答

3
TL;DR 总结:当我们使用 fread() 检索到内容时,我们如何知道它是否是完整的?答案是 EOF。
当子进程关闭其管道端口时,您会得到 EOF。这可能发生在子进程显式调用 close 或退出时。此后,您的管道端口不会有任何输出。获取 EOF 后,您并不知道该进程是否已终止,但您确信它永远不会向管道写入任何内容。
通过调用 pclose,您关闭管道的端口并等待子进程终止。当 pclose 返回时,您就知道子进程已经终止了。
如果您在没有获得 EOF 的情况下调用 pclose,并且子进程尝试将内容写入管道,则会失败(实际上它会收到 SIGPIPE 并且可能会死亡)。
这里绝对没有任何鸡生蛋的情况。

1
我猜OP混淆了进程生命周期和“程序”生命周期,特别是涉及流的打开/关闭,这有点公平,因为如果你是C++程序员而不是Linux专家,它并不一定直观。(“但std::cout一直开放到我的程序结束!”)但确实这都是确定性和安全的,只是令人困惑。 - Lightness Races in Orbit
@LightnessRacesinOrbit - 我也曾对EOF的真正含义感到困惑。我在想:“子进程输出一些东西,然后可能会 sleep 一段时间;同时父进程中的 fread 读取 _some stuff_,因为这是目前可读的所有内容,所以它必须结束/解除阻塞状态; 过一段时间,子进程决定输出_other stuff_,此时fread将会错过。无论如何,这一切都已经澄清了。我学到了一些EOF相关的知识,学习了新的知识,受益匪浅 :) - StoneThrow
@StoneThrow 我的意思是,如果子进程在关闭其管道端之前没有一直读取到EOF,那么这种情况肯定是可能的。事实上,这是一个常见的编程错误! - Lightness Races in Orbit
@LightnessRacesinOrbit - 同意:基于我对该主题的新理解。为了简化这个问题的前提,我假设 fread 提供的缓冲区足够大,可以读取 popen 打开的命令的所有输出。但这也解释了为什么一些使用 popen 的示例使用循环中的 fgets 从打开的文件中读取。 - StoneThrow
@StoneThrow 没错。 - Lightness Races in Orbit

1

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

基本上:是的,在调用pclose之前从popen返回的FILE*中使用fread是安全的。假设给fread的缓冲区足够大,你不会“错过”由popen给出的command生成的输出。

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

感谢 C - pipe without using popen,我对 popen 在底层的工作原理有了更深入的了解:它使用 dup2 将其 stdout 重定向到所使用的管道的写端。重要的是:它执行某种形式的 exec 来在派生的进程中执行指定的 command并且在子进程终止后,包括 1stdout)在内的打开文件描述符会被关闭。也就是说,指定的 command 终止是关闭子进程的 stdout 的条件。

接下来,我回过头来仔细思考了一下在这种情况下EOF的真正含义。起初,我有一个松散而错误的印象,认为"fread尝试尽可能快地从FILE*中读取并在读取完最后一个字节后返回/解除阻塞"。但这并不完全正确:如上所述:fread将读取/阻塞直到达到其目标字节数或遇到EOF或错误。由popen返回的FILE*来自于使用popen的管道的读端进行fdopen,因此当子进程的stdout(它与管道的写端进行了dup2)关闭时,它的EOF就会发生。
所以,最终我们得到的是:popen创建了一个管道,其写端获取运行指定command的子进程的输出,而其读端则通过传递给freadFILE*进行fdopen。 (假设fread的缓冲区足够大),fread将阻塞直到出现EOF,这对应于关闭popen的管道的写端,该管道由执行command导致终止的结果。也就是说,因为fread会阻塞直到遇到EOF,而EOF发生在command(在popen的子进程中运行)终止之后,使用fread(具有足够大的缓冲区)来捕获给定给popencommand的完整输出是安全的。

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


0

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] = '\0';
            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;
}

请注意,返回流上的所有文件操作都在阻塞模式下进行,流是没有O_NONBLOCK标志打开的。当子进程挂起并永远不终止时,fread()可能会被永久阻塞,因此只能使用可信程序的popen()。
为了更好地控制子进程并避免文件阻塞操作,我们应该自己使用fork/vfork/execlv等方法,修改打开的管道属性以使用O_NONBLOCK标志,定期使用poll()或select()来确定是否有一些数据,然后使用read()函数从管道中读取。
使用带有WNOHANG选项的waitpid()定期查看子进程是否已终止。

0

更仔细地阅读 popen 的文档

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

它会阻塞并等待。


我知道 _pclose_ 会阻塞并等待。而 popen 的文档说:“在 popen() 之后,父进程和子进程都能够独立执行,直到其中一个终止。”也就是说,在阻塞的 pclose 之前,你不知道子进程是否已经终止。换句话说:无论在关闭 FILE* 之前从 fp 中读取多少次,你怎么知道子进程不会产生更多的输出?似乎只有在 pclose() 之后才能知道,但此时你不能再从 fp 中读取了。 - StoneThrow
1
popen() 函数族并不像其他一些函数那样功能强大,因此如果您需要更多的控制权,您需要使用更低级别的东西,例如您可以监视信号,如远程关闭。请参见此类示例 使用 pipe,其中低级文件描述符对于获取此类信息非常有用。FILE* 是一个包装器。 - tadman
那么,我说popen()函数族在上述用法中存在潜在的鸡生蛋问题是正确的吗?我理解你所说的需要更低级别的东西,可能使用文件描述符,因为为了回答我的问题,似乎你想知道子进程何时终止才能使包含子进程输出的对象失效 - 对吗?也就是说,你有点想分离由pclose()封装的功能,对吧? - StoneThrow
2
这不是一个鸡生蛋的问题,而是如果你正在使用popen(),那么你表达的是你不关心细节,只关心在完成时获取输出。FILE*结构是一个抽象,它不会给你子进程PID的详细信息。pipe/fork/exec方法可以做到,但需要更多的工作。 - tadman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接