【问题标题】:Why is data corrupt when reading back from a file as it's being written with O_DIRECT为什么在使用 O_DIRECT 写入文件时从文件中读取数据时数据损坏
【发布时间】:2019-02-19 14:58:50
【问题描述】:

我有一个 C++ 程序,它使用 POSIX API 编写一个用O_DIRECT 打开的文件。同时,另一个线程正在通过不同的文件描述符从同一个文件中读回。我注意到有时从文件读回的数据包含全零,而不是我写的实际数据。这是为什么呢?

这是 C++17 中的 MCVE。使用g++ -std=c++17 -Wall -otest test.cpp 或等效项进行编译。抱歉,我似乎无法缩短它。它所做的只是在一个线程中将 100 MiB 的常量字节 (0x5A) 写入文件并在另一个线程中读回它们,如果任何读回的字节不等于 0x5A,则打印一条消息。

警告,此 MCVE 将删除并重写当前工作目录中名为 foo 的任何文件。

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

constexpr size_t CHUNK_SIZE = 1024 * 1024;
constexpr size_t TOTAL_SIZE = 100 * CHUNK_SIZE;

int main(int argc, char *argv[])
{
    ::unlink("foo");

    std::thread write_thread([]()
    {
        int fd = ::open("foo", O_WRONLY | O_CREAT | O_DIRECT, 0777);
        if (fd < 0) std::exit(-1);

        uint8_t *buffer = static_cast<uint8_t *>(
            std::aligned_alloc(4096, CHUNK_SIZE));

        std::fill(buffer, buffer + CHUNK_SIZE, 0x5A);

        size_t written = 0;
        while (written < TOTAL_SIZE)
        {
            ssize_t rv = ::write(fd, buffer,
                std::min(TOTAL_SIZE - written, CHUNK_SIZE));
            if (rv < 0) { std::cerr << "write error" << std::endl; std::exit(-1); }
            written += rv;
        }
    });

    std::thread read_thread([]()
    {
        int fd = ::open("foo", O_RDONLY, 0);
        if (fd < 0) std::exit(-1);

        uint8_t *buffer = new uint8_t[CHUNK_SIZE];

        size_t checked = 0;
        while (checked < TOTAL_SIZE)
        {
            ssize_t rv = ::read(fd, buffer, CHUNK_SIZE);
            if (rv < 0) { std::cerr << "write error" << std::endl; std::exit(-1); }

            for (ssize_t i = 0; i < rv; ++i)
                if (buffer[i] != 0x5A)
                    std::cerr << "readback mismatch at offset " << checked + i << std::endl;

            checked += rv;
        }
    });

    write_thread.join();
    read_thread.join();
}

(为了 MCVE,这里省略了正确的错误检查和资源管理等细节。这不是我的实际程序,但它显示了相同的行为。)

我正在使用 SSD 在 Linux 4.15.0 上进行测试。大约有 1/3 的时间运行程序,会打印“回读不匹配”消息。有时它不会。在所有情况下,如果我在发现 foo 确实包含正确的数据之后检查它。

如果您从写入线程中的::open() 标志中删除O_DIRECT,问题就会消失,并且永远不会打印“回读不匹配”消息。

我可以理解为什么我的::read() 可能会返回 0 或其他东西,表明我已经读取了所有已刷新到磁盘的内容。但我不明白为什么它会执行看似成功的读取,但使用的数据不是我写的。显然我遗漏了一些东西,但它是什么?

【问题讨论】:

  • 正如the open manual page 中提到的,您确实需要使用O_SYNC 来同步写入文件。您所体验的可能是异步效果以及写入和读取线程之间的数据竞争。
  • O_SYNC | O_DIRECT 的行为与仅O_DIRECT 的行为相同。

标签: c++ linux io posix


【解决方案1】:

所以,O_DIRECT has some additional constraints 这可能不是您想要的:

应用程序应避免将O_DIRECT 和普通 I/O 混为一谈 文件,尤其是同一文件中的重叠字节区域。 即使文件系统正确处理了 在这种情况下,整体 I/O 吞吐量可能会低于 单独使用任一模式。

相反,我认为O_SYNC 可能会更好,因为它确实提供了预期的保证:

O_SYNC 提供同步 I/O 文件完整性完成,意思是 写操作会将数据和所有关联的元数据刷新到 底层硬件。 O_DSYNC 提供同步的 I/O 数据 完整性完成,这意味着写入操作会将数据刷新到 底层硬件,但只会刷新元数据更新 需要允许后续读取操作完成 成功地。数据完整性完成可以减少 不需要的应用程序所需的磁盘操作 文件完整性完成的保证。

【讨论】:

  • O_DIRECT 添加到回读文件描述符的标志似乎可以解决与此信息一致的问题。 Linus 的“精神错乱的猴子”名言让我对这有多挑剔有了一个不错的感觉。谢谢。
猜你喜欢
  • 2011-12-16
  • 1970-01-01
  • 1970-01-01
  • 2017-07-16
  • 2014-07-09
  • 2013-02-28
  • 2015-09-22
  • 2011-01-22
  • 2017-09-14
相关资源
最近更新 更多