14

考虑这个小程序被编译为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是如何做到的?

4

5 回答 5

11

您已经被这样一个事实所困扰,即在 C 程序中自动打开的流的缓冲会随着所连接设备的类型而变化。

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

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

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

标准输入的开放组手册页在缓冲方面相当模糊,以便为实现提供足够的余地以提高效率,但它确实说:

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

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

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

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

于 2016-09-08T12:06:07.920 回答
8

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

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

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

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

于 2016-09-08T12:24:48.260 回答
2

我在原始帖子中的问题已经在其他答案中得到很好的解释。
控制台应用程序使用一个名为的函数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;
}
于 2016-09-16T10:04:36.573 回答
0

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

于 2016-09-08T10:55:33.560 回答
-1

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

于 2016-09-08T11:21:57.830 回答