为什么在使用LLVM时,std :: ifstream的缓冲会“破坏”使用std :: getline的功能?

15

我有一个简单的C++应用程序,它应该从一个POSIX命名管道中读取行:

#include<iostream>
#include<string>
#include<fstream>

int main() {
    std::ifstream pipe;
    pipe.open("in");

    std::string line;
    while (true) {
        std::getline(pipe, line);
        if (pipe.eof()) {
            break;
        }
        std::cout << line << std::endl;
    }
}

步骤:

  • 我创建了一个命名管道:mkfifo in

  • 我使用g++ -std=c++11 test.cpp && ./a.out编译并运行C++代码。

  • 我向in管道发送数据:

sleep infinity > in &  # keep pipe open, avoid EOF
echo hey > in
echo cats > in
echo foo > in
kill %1                # this closes the pipe, C++ app stops on EOF

在Linux下执行此操作时,应用程序成功显示每个echo命令后的输出(g++ 8.2.1)。

当尝试在macOS上完成整个过程时,只有在关闭管道后(即在kill %1之后)才会显示输出。我开始怀疑存在某种缓冲问题,因此我尝试禁用它,就像这样:

std::ifstream pipe;
pipe.rdbuf()->pubsetbuf(0, 0);
pipe.open("out");

通过这个更改,在第一个echo之后,应用程序不会输出任何内容,然后在第二个echo之后打印出第一条消息("hey"),并且始终落后于一条消息,并显示前一个echo的消息而不是已执行的消息。 只有在关闭管道后才显示最后一条消息。

我发现在macOS上,g++基本上是clang++,因为g ++ --version 得出:"Apple LLVM version 10.0.1(clang-1001.0.46.3)"。 在使用Homebrew安装真正的g++之后,示例程序可以正常工作,就像在Linux上一样。

我正在构建一个简单的IPC库,以命名管道为基础,这对我来说几乎是一个要求,因此正确工作非常重要。

当使用LLVM时,是什么导致了这种奇怪的行为?(更新:这是由libc++引起的)

这是一个错误吗?

这种方式是否以某种方式由C++标准保证在g ++上工作?

如何使用clang++使此代码段正常工作?

更新:

这似乎是由getline()的libc++实现引起的。 相关链接:

但问题仍然存在。


@DavisHerring 是的,你说得对,我可能应该更准确地表达我的意思。当然,我的意思是如果流中有可用的换行符,std::getlines() 不会阻塞。使用 DOS 换行符也会产生相同的结果。 - krispet krispet
3
有趣的是,我使用一款较旧版本的“Apple LLVM 7.0.2(clang-700.1.81)”和 _LIBCPP_VERSION 为1101时,能够看到期望的行为。 - Davis Herring
1
在相关的错误报告中,我找到了以下回复:“问题实际上出现在basic_filebuf<_CharT, _Traits>::underflow()函数中。大约在第595行,它调用fread函数来填充filebuf的缓冲区(默认为4096字节)。这个读取操作会一直等待,直到可以读取一个完整的缓冲区。当它读取文件时,如果文件没有那么多数据,就会出现短读取。当它读取管道时,它会一直等待直到有足够的数据可用。”这似乎表明这确实是一个bug。当我有时间时,我应该检查我们尝试过的两个版本中所引用的代码。 - krispet krispet
1
我很感激这个建议,因为它是一个好建议。事实上,我以前使用过boost的asyncio,并且对于一般的IPC来说,这将是一种优雅的解决方案。然而,问题不在于“如何构建可扩展和灵活的IPC解决方案”。像大多数项目一样,我有一些要求超出了我的能力范围。问题明确是关于libc++中std::getline()的奇怪行为,而不是我读取命名管道直到定界符的原因。 - krispet krispet
@darune,很奇怪这个错误报告自2015年以来一直没有被处理,我想知道C++标准是否有关于管道上fstream行为的规定。但是没关系,我在发布后几天就自己解决了这个问题,我会发布我的解决方案以帮助其他人。 - krispet krispet
显示剩余4条评论
2个回答

2

我通过将POSIX getline() 封装在一个简单的C API中并从C++中调用该API,解决了这个问题。

代码大致如下:

typedef struct pipe_reader {
    FILE* stream;
    char* line_buf;
    size_t buf_size;
} pipe_reader;

pipe_reader new_reader(const char* pipe_path) {
    pipe_reader preader;
    preader.stream = fopen(pipe_path, "r");
    preader.line_buf = NULL;
    preader.buf_size = 0;
    return preader;
}

bool check_reader(const pipe_reader* preader) {
    if (!preader || preader->stream == NULL) {
        return false;
    }
    return true;
}

const char* recv_msg(pipe_reader* preader) {
    if (!check_reader(preader)) {
        return NULL;
    }
    ssize_t read = getline(&preader->line_buf, &preader->buf_size, preader->stream);
    if (read > 0) {
        preader->line_buf[read - 1] = '\0';
        return preader->line_buf;
    }
    return NULL;
}

void close_reader(pipe_reader* preader) {
    if (!check_reader(preader)) {
        return;
    }
    fclose(preader->stream);
    preader->stream = NULL;
    if (preader->line_buf) {
        free(preader->line_buf);
        preader->line_buf = NULL;
    }
}

这对抗libc++或libstdc++很有效。


1
如另行讨论,使用boost::asio解决方案最佳,但您的问题特别是关于getline如何阻塞,因此我将谈论它。问题在于std::ifstream并不适用于FIFO文件类型。在getline()的情况下,它正在尝试进行缓冲读取,因此(在初始情况下)它决定缓冲区没有足够的数据来达到分隔符('\n'),在底层streambuf上调用underflow(),并且这会简单地读取一个缓冲区长度的数据量。这对于文件非常有效,因为文件在某个时间点的长度是可知的,因此如果没有足够的数据填充缓冲区,它可以返回EOF,如果有足够的数据,则只需返回填充的缓冲区。然而,对于FIFO,耗尽数据并不一定意味着EOF,因此它直到写入它的进程关闭(这是无限制的sleep命令,使其保持打开状态)才会返回。
一种更典型的方法是,写入程序在读写文件时打开和关闭文件。当有更实用的功能可用,如 poll()/epoll() 时,这显然是一种浪费,但我正在回答你提出的问题。

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