Почему буферизация std :: ifstream "ломает" std :: getline при использовании LLVM?

У меня есть простое приложение 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.

  • Я компилирую и запускаю код C++, используя g++ -std=C++11 test.cpp &&./a.out.

  • Я передаю данные 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 ("эй") и продолжает делать это, всегда отставая от сообщения и отображая сообщение предыдущего echo вместо один исполнен. Последнее сообщение отображается только после закрытия канала.

Я обнаружил, что в macOS g++ в основном clang++, так как g++ --version выдает: "Apple LLVM версия 10.0.1 (clang-1001.0.46.3)". После установки настоящего g++ с использованием Homebrew, пример программы работает так же, как и в Linux.

Я строю простую библиотеку IPC, построенную на именованных каналах по разным причинам, поэтому правильная работа с ней в значительной степени является требованием для меня на данный момент.

Что вызывает это странное поведение при использовании LLVM? (обновление: это вызвано lib C++)

Это ошибка?

Каким-то образом гарантируется, как это работает на g++ стандартом C++?

Как я могу заставить этот фрагмент кода работать должным образом, используя clang++?

Обновить:

По-видимому, это вызвано реализацией lib C++ функции getline(). Ссылки по теме:

Вопросы все еще стоят, хотя.

Ответы

Ответ 1

Как обсуждалось отдельно, решение boost::asio было бы лучше, но ваш вопрос конкретно о том, как getline блокирует, так что я поговорю об этом.

Проблема здесь в том, что std::ifstream самом деле не создан для типа файла FIFO. В случае getline() он пытается выполнить буферизованное чтение, поэтому (в начальном случае) он решает, что в буфере недостаточно данных для достижения разделителя ('\n'), вызывает метод underflow() для лежащий в основе streambuf, и это делает простое чтение для объема данных длины буфера. Это прекрасно работает для файлов, потому что длина файла в определенный момент времени является ощутимой, поэтому он может возвращать EOF, если данных недостаточно для заполнения буфера, и если данных достаточно, он просто возвращается с заполненным буфером. Однако в случае FIFO нехватка данных не обязательно означает EOF, поэтому он не возвращается до тех пор, пока не завершится процесс записи в него (это ваша бесконечная команда sleep которая удерживает ее открытой).

Более типичный способ сделать это - открыть и закрыть файл, когда он читает и пишет. Очевидно, что это пустая трата усилий, когда доступно что-то более функциональное, например poll()/epoll(), но я отвечаю на вопрос, который вы задаете.

Ответ 2

Я обошел эту проблему, обернув POSIX getline() в простой 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] = '\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;
    }
}

Это хорошо работает против lib C++ или libstd C++.