首页 > 解决方案 > 为什么在使用 O_DIRECT 写入文件时从文件中读取数据时数据损坏

问题描述

我有一个 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事后检查,我发现它确实包含正确的数据。

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

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

标签: c++linuxioposix

解决方案


因此,O_DIRECT 有一些额外的限制可能无法满足您的需求:

应用程序应避免对同一文件进行混合O_DIRECT和正常 I/O,尤其是同一文件中的重叠字节区域。即使文件系统在这种情况下正确处理了一致性问题,总体 I/O 吞吐量也可能比单独使用任何一种模式都要慢。

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

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


推荐阅读