为什么在使用LLVM时,std::ifstream的缓冲"break" std::getline?

Why does the buffering of std::ifstream "break" std::getline when using LLVM?

本文关键字:std 缓冲 break getline ifstream LLVM 为什么      更新时间:2023-10-16

我有一个简单的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 版本 10.0.1 (clang-1001.0.46.3)"。 使用 Homebrew 安装真正的 g++ 后,示例程序可以工作,就像在 Linux 上一样。

由于各种原因,我正在构建一个基于命名管道构建的简单 IPC 库,因此在这一点上,这对我来说几乎是一个要求。

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

这是一个错误吗?

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

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

更新:

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

  • 为什么libc++ getline在从管道读取时会阻塞,而libstdc++ getline不会?
  • https://bugs.llvm.org/show_bug.cgi?id=23078

不过,问题仍然存在。

我通过将 POSIXgetline()包装在一个简单的 C API 中并简单地从C++调用它来解决这个问题。 代码是这样的:

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] = '';
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++。

如前所述,boost::asio解决方案是最好的,但您的问题专门针对getline如何阻塞,所以我将对此进行讨论。

这里的问题是std::ifstream并不是真正为FIFO文件类型制作的。 在getline()的情况下,它试图进行缓冲读取,因此(在初始情况下)它决定缓冲区没有足够的数据到达分隔符('n'),调用底层streambufunderflow(),并对缓冲区长度的数据量进行简单读取。 这适用于文件,因为文件在某个时间点的长度是可知的长度,因此如果没有足够的数据来填充缓冲区,它可以返回 EOF,如果有足够的数据,它只需返回填充的缓冲区。 但是,对于FIFO,数据用完并不一定意味着EOF,因此在写入它的进程关闭之前它不会返回(这是将其打开的无限sleep命令)。

执行此操作的更典型方法是让编写器在读取和写入文件时打开和关闭文件。 当像poll()/epoll()这样功能更强大的东西可用时,这显然是浪费精力,但我正在回答你问的问题。