【问题标题】:Force flush or read unflushed output强制刷新或读取未刷新的输出
【发布时间】:2020-01-04 01:18:13
【问题描述】:

这是工具dc的前端;这个想法是键入一个中缀表达式 (2 + 3),将其映射到相应的后缀符号 (2 3 +) 并将其发送到dc。这就是bc 所做的。

我正在使用管道执行此操作,但前端挂起等待输出。
这是代码;我将在下面继续评论它。我的dc 中的“h”命令未实现,因此我正在寻找 dc 的输出结尾。

TL;DR:进程挂起是因为dc 中的stdout 没有被刷新,或者这就是我认为已经找到的。每次写入后如何读取或强制刷新?

我的发现在下面评论。

#define DC_EOF_RCV "dc: 'h' (0150) unimplemented"
#define DC_EOF_SND "h\n"

static FILE *sndfp, *rcvfp;

int dcinvoke() {
    int pfdout[2], pfdin[2];
    pid_t pid;

    pipe(pfdout);
    pipe(pfdin);
    switch (pid = fork()) {
        case -1: exit(1);
        case 0:
            dup2(pfdout[0], STDIN_FILENO);
            dup2(pfdin[1], STDOUT_FILENO);
            close(pfdout[0]);
            close(pfdout[1]);
            close(pfdin[0]);
            close(pfdin[1]);
            execlp("dc", "dc", "-", NULL);
    }
    close(pfdout[0]);
    close(pfdin[1]);
    sndfp = fdopen(pfdout[1], "w");
    rcvfp = fdopen(pfdin[0], "r");
    return 1;
}

void dcsnd(const char *s) {
    fputs(s, sndfp);
    fflush(sndfp);
}

void dcrcv(char *buf, size_t max) {
    fgets(buf, max, rcvfp); // <<<<< HANGS HERE
}

int turnaround() {
    dcsnd(DC_EOF_SND); fflush(sndfp);
}

int rcvall() {
    char buf[256];
    turnaround();
    for (;;) {
        dcrcv(buf, sizeof(buf));
        if (! strcmp(buf, DC_EOF_RCV)) {
            break;
        }
        printf("%s", buf);
    }
    return 1;
}

int prompt(const char *msg, char *res, size_t resmax, int *eofp) {
    char *p;
    printf("\n%s ", msg);
    if (!fgets(res, resmax, stdin)) {
        *eofp = 1;
    } else {
        if (p = strrchr(res, '\n')) {
            *p = 0;
        }
        *eofp = 0;
    }
    return 1;
}

int main() {
    char buf[128], line[128];
    int eof;

    dcinvoke();
    dcsnd("2 3 +\n");
    dcsnd(DC_EOF_SND);
    rcvall();
    for (;;) {
        prompt("Expression", buf, sizeof(buf), &eof);
        if (eof) {
            break;
        }
        snprintf(line, sizeof(line), "%s\n", buf);
        dcsnd(line);
        rcvall();
    }
    dcsnd("q\n");
    return 0;
}

为了简单起见,我删除了错误检查。

前端挂在行:

fgets(buf, max, rcvfp);

因为我真的不知道如何找到问题,所以我编写了自己的dc,它什么都不做,只是正确响应“h”命令并将它获取的所有内容输出到文件中(这样我就可以调试它;我我不知道任何其他方式)。有用的话我可以加在这里。

我发现在(我的)dc 中对fflush(stdout) 的调用会恢复前端,因为对fgets 的调用最终会返回。

我无法更改dc 本身。怎么能挂不上fgets

我的一些想法:

  • 使用另一个线程刷新rcvfpstdout of dc

  • 使用伪终端编写

我正在寻找建议或避免这两种情况的简单方法。

我尝试过:

  • 使用read()fileno(rcvfp)

  • 阅读thisthis类似的帖子

  • 使用setlinebuf() (man)

【问题讨论】:

  • 可能的原因:因为 dc 没有 tty,它的 stdout 处于完全缓冲模式。您需要给它一个 pty 以使输出行缓冲,或者在每次操作后使其退出以强制刷新其标准输出。
  • 旁白:由于关于输出刷新的规则不同,在printf("\n%s ", msg); 之后,考虑fflush(stdout); 以确保输出发生在fgets() 之前。
  • 您应该在execlp() 之后有错误代码(即使它只是exit(EXIT_FAILURE);)——它可能会失败,然后退出而不是尝试继续很重要。您不需要测试来自execlp() 的返回值;如果成功则不会返回;如果返回,则失败。你也可以在退出前向stderr 写一个错误。
  • bc 的现代版本通常不使用dc。 POSIX 没有标准化 dc 的使用(dc 不是 POSIX 的一部分,但 bc 是;基本原理提到 dc 及其在 bc 的某些实现中的使用)。
  • @JonathanLeffler 每个调用都包含错误检查宏,甚至是那个 exec,但正如我在问题中所说的,I have removed the error checks for simplicity of this question.。没有必要使用 dc 除了作为使用管道的练习......我认为它会更容易,发现它比最初想象的更有趣

标签: c linux stdout buffered


【解决方案1】:

dc 将错误消息发送到 stderr,而不是 stdout,因此您永远不会看到它们——它们直接发送到终端(或您环境中连接的任何 stderr)。为了能够在您的程序中读取它们,您还需要重定向 stderr。添加

dup2(pfdin[1], STDERR_FILENO);

在子 fork 代码中。


为了使您的代码真正起作用,我还必须在 DC_EOF_RCV 字符串的末尾添加一个换行符(以匹配 dc 发送的内容),并从 main 中删除额外的 dcsnd(DC_EOF_SND); 调用,因为您从不接收在任何地方匹配它。即使这样,您仍然可以在写入 stdout 和 stderr 之间获得 dc 内的竞争条件,这可能会导致输出线混合,从而导致响应与您的 DC_EOF_RCV 字符串不匹配。您可以通过在 turnaround 函数中添加一个小睡眠来避免这些。


另一种选择是在主循环中使用 poll 同时从标准输入和直流读取。有了这个,你不需要奇怪的“发送 h 命令来触发错误”机制来确定 dc 何时完成——你只需循环。你的主循环最终看起来像:

struct pollfd polling[2] = { { STDIN_FILENO, POLLIN, 0 }, { fileno(rcvfp), POLLIN, 0 } };
printf("Expression ");
for(;;) {
    if (poll(polling, 2, -1) < 0) {
        perror("poll");
        exit(1); }
    if (polling[0].revents) {
        /* stdin is ready */
        if (!fgets(buf, sizeof(buf), stdin))
            break;
        dcsnd(buf); }
    if (polling[1].revents) {
        /* dc has something for us */
        printf("\r          \r");   /* erase the previsouly printed prompt */
        dcrcv(buf, sizeof(buf));
        printf("%s\n", buf); }
    printf("Expression ");  /* new prompt */
}

您可以在此处管理错误条件的更多细节(revents 可能是 POLLERRPOLLHUP 用于错误或挂断,而不是 POLLIN 用于正常输入 - 所有这些都非零值,因此仅针对非零进行测试是合理的)。

【讨论】:

  • 这是需要牢记的,但这并不能解决这里的问题。尽管如此,我还是尝试过,它仍然卡在等待来自管道的数据
  • @bicup: 但直到它从 dc 读取并输出两条错误消息。请注意,由于换行符,您在 rcval 中与 DC_EOF_RCV 的比较将永远不会匹配,因此该循环将永远不会退出。如果你解决了这个问题,进入主循环调用提示符。
  • 使用strstr(buf, DC_EOF_RCV) 没有帮助
  • 通过您的编辑,我从main 中删除了该行,并将strstrDC_EOF_RCV "'h' (0150) unimplemented" 一起使用,加上sleep(1),但对我来说它不起作用。对你来说不是死锁吗?
  • 没有像您描述的 strstr 的死锁。尝试使用strace -f 运行,看看 dc 在内部做什么......
【解决方案2】:

使用 macOS 10.14.6 Mojave 提供的dc 命令,向程序发送2 3 + 可以正常工作;它只是什么也没说,因为你没有要求它。

$ dc
2 3 +
p
5
q
$

p(打印)命令触发响应5q 命令退出。

发送h 得到的反应不一:

$ dc
h
dc: 'h' (0150) unimplemented
q
$

响应参差不齐,因为dc:(和空格)进入标准错误,消息进入标准输出:

$ dc >/dev/null
h
dc:
q
$

$ dc 2>/dev/null
h
'h' (0150) unimplemented
q
$

因此,我认为您的主要问题是您正在等待dc 不会提供的响应。如果要确保获得打印值,则需要添加p 命令。这可以与 2 3 + 表达式在同一行或不同行。

$ dc
2 3 + p
5
2
3
+
p
5
q
$

【讨论】:

  • 问题是这应该只是一个前端;在用户输入p 之前,不应假定命令中存在隐含的p - 至少这是我的看法。事实上dc 可以使用堆栈和push/pop 表达式结果......那么它是pn 还是其他命令?
  • 那么你需要设计一个“超时读取”机制,这样如果有答案,你就会得到它,如果没有,你就没有。你不能因为dc 等待你的程序输入而你的程序等待dc 的输出而陷入僵局,但这就是正在发生的事情。超时应该很短——几十到几百毫秒。或者您可以添加p 以确保有响应;这是一种非破坏性操作。如果用户在类型化表达式中包含多个p 操作会发生什么? 2 3 + p 4 5 * p - p 在 3 行上生成 5 20 -15
  • 交替使用selectpoll“同时”从 dc 或用户读取数据并处理您获得的任何一个。
  • 当然,对于bcbc 程序正在生成由dc 运行的程序(表达式),并控制发送的所有内容,并且知道从它发送的表达式中会返回什么。
猜你喜欢
  • 1970-01-01
  • 2017-01-16
  • 1970-01-01
  • 2012-09-19
  • 2010-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多