【问题标题】:How can I read process output that has not been flushed?如何读取尚未刷新的进程输出?
【发布时间】:2017-01-16 06:33:15
【问题描述】:

考虑将这个小程序编译为application.exe

#include <stdio.h>

int main()
{
    char str[100];
    printf ("Hello, please type something\n");
    scanf("%[^\n]s", &str);
    printf("you typed: %s\n", str);
    return 0;
}

现在我使用这段代码启动application.exe 并获取它的输出。

#include <stdio.h>
#include <iostream>
#include <stdexcept>

int main()
{
    char buffer[128];
    FILE* pipe = popen("application.exe", "r");
    while (!feof(pipe)) {
        if (fgets(buffer, 128, pipe) != NULL)
            printf(buffer);
    }
    pclose(pipe);
    return 0;
}

我的问题是在我输入之前没有输出。然后获取两条输出线。 我可以通过在第一个 printf 语句之后添加这一行来解决这个问题。

fflush(stdout);

然后在我按预期输入之前获取第一行。

但是如何获取我无法修改且不“实时”使用fflush() 的应用程序的输出(即在它们退出之前)? . 而windows cmd是怎么做到的呢?

【问题讨论】:

  • 你有理由不使用更简单的while(fgets(...)) fputs(...);吗?您可能还想在阅读程序中刷新标准输出。
  • 请定义“实时”的含义。
  • while (!feof(pipe)) 错误。
  • 这显然是 C++,而不是 C。(好吧,它可以写成 C,但你显然编译成 C++。如果你想这是 C 代码,编译成 C!它们是不同的语言。)
  • @AlgirdasPreidžius 它对我有用,即使在 Ubuntu 上也是如此。

标签: c++ winapi process stdout unbuffered


【解决方案1】:

我原来的帖子中的问题已经很好解释了 在其他答案中。
控制台应用程序使用名为 isatty() 的函数来检测 如果他们的stdout 处理程序连接到管道或真正的控制台。如果是管道 除非您直接调用fflush(),否则所有输出都会以块的形式缓冲和刷新。 在真正的控制台的情况下,输出是无缓冲的并直接打印到 控制台输出。
在 Linux 中,您可以使用 openpty() 创建一个伪终端并在其中创建您的进程。作为一个 结果该进程会认为它在真正的终端中运行并使用无缓冲的输出。
Windows 似乎没有 这样的选择。

经过大量挖掘 winapi 文档后,我发现这不是真的。其实你可以创建 您自己的控制台屏幕缓冲区,并将其用于您的进程的stdout,然后将被取消缓冲。
遗憾的是,这不是一个非常舒适的解决方案,因为没有事件处理程序,我们需要轮询新数据。 同样,目前我不确定如何在此屏幕缓冲区已满时处理滚动。
但即使仍有一些问题 离开了,我想我已经为那些想要获取无缓冲(和未刷新)的人创建了一个非常有用(和有趣)的起点 windows 控制台进程输出。

#include <windows.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    char cmdline[] = "application.exe"; // process command
    HANDLE scrBuff;                     // our virtual screen buffer
    CONSOLE_SCREEN_BUFFER_INFO scrBuffInfo; // state of the screen buffer
                                            // like actual cursor position
    COORD scrBuffSize = {80, 25};       // size in chars of our screen buffer
    SECURITY_ATTRIBUTES sa;             // security attributes
    PROCESS_INFORMATION procInfo;       // process information
    STARTUPINFO startInfo;              // process start parameters
    DWORD procExitCode;                 // state of process (still alive)
    DWORD NumberOfCharsWritten;         // output of fill screen buffer func
    COORD pos = {0, 0};                 // scr buff pos of data we have consumed
    bool quit = false;                  // flag for reading loop

    // 1) Create a screen buffer, set size and clear

    sa.nLength = sizeof(sa);
    scrBuff = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE,
                                         FILE_SHARE_READ | FILE_SHARE_WRITE,
                                         &sa, CONSOLE_TEXTMODE_BUFFER, NULL);
    SetConsoleScreenBufferSize(scrBuff, scrBuffSize);
    // clear the screen buffer
    FillConsoleOutputCharacter(scrBuff, '\0', scrBuffSize.X * scrBuffSize.Y,
                               pos, &NumberOfCharsWritten);

    // 2) Create and start a process
    //      [using our screen buffer as stdout]

    ZeroMemory(&procInfo, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&startInfo, sizeof(STARTUPINFO));
    startInfo.cb = sizeof(STARTUPINFO);
    startInfo.hStdOutput = scrBuff;
    startInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
    startInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    startInfo.dwFlags |= STARTF_USESTDHANDLES;
    CreateProcess(NULL, cmdline, NULL, NULL, FALSE,
                  0, NULL, NULL, &startInfo, &procInfo);    
    CloseHandle(procInfo.hThread);

    // 3) Read from our screen buffer while process is alive

    while(!quit)
    {
        // check if process is still alive or we could quit reading
        GetExitCodeProcess(procInfo.hProcess, &procExitCode);
        if(procExitCode != STILL_ACTIVE) quit = true;

        // get actual state of screen buffer
        GetConsoleScreenBufferInfo(scrBuff, &scrBuffInfo);

        // check if screen buffer cursor moved since
        // last time means new output was written
        if (pos.X != scrBuffInfo.dwCursorPosition.X ||
            pos.Y != scrBuffInfo.dwCursorPosition.Y)            
        {
            // Get new content of screen buffer
            //  [ calc len from pos to cursor pos: 
            //    (curY - posY) * lineWidth + (curX - posX) ]
            DWORD len =  (scrBuffInfo.dwCursorPosition.Y - pos.Y)
                        * scrBuffInfo.dwSize.X 
                        +(scrBuffInfo.dwCursorPosition.X - pos.X);
            char buffer[len];
            ReadConsoleOutputCharacter(scrBuff, buffer, len, pos, &len);

            // Print new content
            // [ there is no newline, unused space is filled with '\0'
            //   so we read char by char and if it is '\0' we do 
            //   new line and forward to next real char ]
            for(int i = 0; i < len; i++)
            {
                if(buffer[i] != '\0') printf("%c",buffer[i]);
                else
                {
                    printf("\n");
                    while((i + 1) < len && buffer[i + 1] == '\0')i++;
                }
            }

            // Save new position of already consumed data
            pos = scrBuffInfo.dwCursorPosition;
        }
        // no new output so sleep a bit before next check
        else Sleep(100);
    }

    // 4) Cleanup and end

    CloseHandle(scrBuff);   
    CloseHandle(procInfo.hProcess);
    return 0;
}

【讨论】:

    【解决方案2】:

    你不能。 因为尚未刷新的数据归程序本身所有。

    【讨论】:

    • 但是 windows cmd 和 linux ttys 似乎可以这样做。我在问为什么。
    • 即使是 windows cmd 也无法做到这一点 AFAIK。而且我从不使用 linux ttys。
    • 你有例子吗?
    • 我的例子在我的问题中。第一段代码适用于 windows cmd。但在我使用flush之前,我的输出提取器代码不会。
    • 不,问题的关键是:为什么printfstdout是控制台时刷新每一行,而在stdout是文件时缓冲数据 .
    【解决方案3】:

    C 程序中自动打开的流的缓冲会随着所连接设备的类型而变化,这一事实让您感到困扰。

    这有点奇怪——让 *nixes 很好玩的一件事(并且反映在 C 标准库中)是进程不太关心他们从哪里获取数据以及从哪里写入数据.您只需在闲暇时进行管道和重定向,它通常是即插即用的,而且速度非常快。

    此规则打破的一个明显地方是交互;你举了一个很好的例子。如果程序的输出是块缓冲的,您可能在累积 4k 数据之前看不到它,或者进程退出。

    程序可以检测它是否通过isatty() 写入终端(也可能通过其他方式)。终端在概念上包括用户,建议交互式节目。打开 stdin 和 stdout 的库代码会检查并将它们的缓冲策略更改为行缓冲:当遇到换行符时,将刷新流。这对于交互式、面向行的应用程序来说是完美的。 (对于行编辑来说,它并不完美,因为 bash 会完全禁用缓冲。)

    open group man page for stdin 在缓冲方面相当模糊,以便为实现提供足够的余地以提高效率,但它确实说:

    标准输入和标准输出流被完全缓冲当且仅当流可以被确定不引用交互式设备。

    这就是您的程序发生的情况:标准库看到它正在“非交互式”运行(写入管道),并尝试变得聪明高效并打开块缓冲。写入换行符不再刷新输出。通常这是一件好事:想象一下写入二进制数据,平均每 256 个字节写入磁盘!太可怕了。

    值得注意的是,在您和磁盘之间可能存在整个级联的缓冲区;在 C 标准库之后是操作系统的缓冲区,然后是磁盘。

    现在你的问题是:用于存储要写入的字符的标准库缓冲区位于程序的内存空间中。尽管看起来,数据还没有离开你的程序,因此其他程序不能(官方)访问。我觉得你运气不好。您并不孤单:大多数交互式控制台程序在尝试通过管道操作时都会表现不佳。

    【讨论】:

    • 感谢这个解释得很好的答案。我现在清楚多了。
    【解决方案4】:

    恕我直言,这是 IO 缓冲中逻辑性较低的部分之一:当定向到终端或文件或管道时,它的行为不同。如果 IO 被定向到文件或管道,它通常是缓冲的,这意味着只有在缓冲区已满或发生显式刷新时才会实际写入输出 => 这就是你在什么时候看到的你通过popen执行一个程序。

    但是当 IO 被定向到终端时,会发生一种特殊情况:在从同一终端读取之前,所有挂起的输出都会自动刷新。这种特殊情况对于允许交互式程序在阅读前显示提示是必要的。

    不好的是,如果您尝试通过管道驱动交互式应用程序,您就会松懈:提示只能在应用程序结束或输出足够的文本以填充缓冲区时读取。这就是 Unix 开发人员发明所谓的 pseudo ttys (pty) 的原因。它们被实现为终端驱动程序,因此应用程序使用交互式缓冲,但 IO 实际上是由拥有 pty 主控部分的另一个程序操作的。

    不幸的是,当您编写application.exe 时,我假设您使用的是 Windows,并且我不知道 Windows API 中的等效机制。被调用者必须使用无缓冲 IO(stderr 默认为无缓冲)以允许调用者在发送应答之前读取提示。

    【讨论】:

    • 感谢您的回答。实际上我正在寻找一个独立于操作系统的解决方案。因此,正如您所解释的那样,使用 linux 和 this question shows 似乎是可能的,但对于 windows 是不可能的。
    【解决方案5】:

    我认为您可以将数据刷新到stderr 或封装fgetcfungetc 的函数以不破坏流或使用system("application.ext &gt;&gt;log") 然后mmap 登录到内存来做你想做的事情。

    【讨论】:

    • 你认为application.exe的标准输出会因为你重定向它而变得无缓冲吗? (我不是在讽刺。我不完全确定。但我几乎可以肯定:stdout 的缓冲是在 putchar 宏中编码的,它是系统上 c 标准库的一部分;它不受不同的控制进程(如 shell 或在 OP 中建立管道的程序。)
    • 回答我自己的问题:是的,它确实改变了;标准库根据它们是否连接到终端来更改标准输入/输出的缓冲。至于您的建议:日志文件将不包含程序的输出,无论是否映射;-)。
    猜你喜欢
    • 2020-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多