使用O_DIRECT读取持续写入文件时read()返回EINVAL的问题
O_DIRECT读取并发写入文件时返回EINVAL的问题解答
问题背景
我编写了一个使用O_DIRECT标志读取文件的程序,该文件正被另一个进程持续写入。读取循环在到达写入操作的位置前运行正常,但此时read()会因EINVAL(无效参数)失败;若不使用O_DIRECT则无此问题,且read()会返回有效值。已确认块大小为4096。
代码示例
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <malloc.h> #include <iostream> #define BUFFER_SIZE 524288 // 512 KB int main() { int fd; char *buffer; ssize_t bytesRead; // Open file with O_DIRECT fd = open("/data/file.lz4", O_RDONLY | O_DIRECT); if (fd == -1) { perror("Error opening file"); return EXIT_FAILURE; } // Allocate aligned memory for O_DIRECT buffer = (char *) aligned_alloc(4096, BUFFER_SIZE); // Read in a loop while (1) { bytesRead = read(fd, buffer, BUFFER_SIZE); if (bytesRead == -1) { std::cerr << "Error reading file: " << strerror(errno) << std::endl; std::cerr << "fd: " << fd << " buffer: " << (void*)buffer << " BUFFER_SIZE: " << BUFFER_SIZE << std::endl; break; } std::cout << bytesRead << std::endl; } // Free the aligned buffer free(buffer); // Close file close(fd); return 0; }
观测到的行为
程序在文件有数据时读取正常;当到达写入位置时,输出如下:
524288 524288 524288 524288 524288 172020 0 0 Error reading file: Invalid argument fd: 3 buffer: 0x7f4140e88000 BUFFER_SIZE: 524288
这说明read()在尝试读取正在写入的数据时失败。
已检查内容
- 使用
aligned_alloc(4096, BUFFER_SIZE)正确对齐了缓冲区 - BUFFER_SIZE是4096的倍数,满足O_DIRECT对齐要求
- 错误发生前fd有效
- 文件存在且正被另一个进程持续更新
环境细节
- 文件系统:ext4(本地SSD)
- CPU型号:Intel(R) Core(TM) i9-14900KS
- Linux版本:5.15.77-1-lts
疑问解答
1. 为什么到达写入点时read()会返回EINVAL?
O_DIRECT要求读取的偏移量、缓冲区地址、读取长度都必须严格对齐到文件系统的块大小(这里是4096)。当读取到文件末尾的未完全写入的块时,问题就出现了:
- 并发写入时,文件长度可能处于“中间状态”——写入进程正在写入一个块,文件长度已更新,但该块尚未完全写入磁盘。
- 此时
read()请求跨越了已写入块的边界,尝试读取未完全初始化的块。O_DIRECT绕过页缓存,无法像普通读取那样用0填充未写入区域,内核会直接返回EINVAL拒绝这种“部分块”读取。 - 普通读取(无O_DIRECT)依赖页缓存,会自动处理这种情况:要么等待写入完成,要么返回已写入部分并填充剩余为0,不会返回错误。
2. 当读取到未写入部分时,能否等待更多数据而非直接失败?
默认情况下不行,O_DIRECT的设计目标是直接与存储设备交互,不经过页缓存,因此不提供“等待数据”的语义。但可以通过以下方式实现类似效果:
- 检测EINVAL并重试:当
read()返回EINVAL时,短暂休眠(比如用usleep())后重新尝试读取,直到写入进程完成该块写入,读取即可成功。 - 结合O_NONBLOCK轮询:打开文件时加上
O_NONBLOCK,但O_DIRECT与O_NONBLOCK的组合行为需测试,部分场景仍可能返回EINVAL,最终还是需要重试逻辑。 - 放弃O_DIRECT:如果业务允许,去掉O_DIRECT标志,依赖页缓存处理并发写入的同步问题,这是最简单的解决方案。
3. O_DIRECT与并发写入的交互机制
O_DIRECT绕过Linux页缓存,直接在用户缓冲区和存储设备间传输数据,与并发写入的交互有以下核心特点:
- 无缓存一致性保证:页缓存会自动处理读写一致性,而O_DIRECT直接从磁盘取数据,无法感知写入进程在页缓存中的未刷新数据——只有写入进程调用
fsync()或fdatasync()将数据刷到磁盘后,O_DIRECT才能读取到最新内容。 - 严格对齐约束:任何不符合块对齐的读取请求(包括读取未完全写入的块)都会被内核拒绝,返回EINVAL。
- 无自动等待/重试:内核不会为O_DIRECT请求等待写入操作完成,因为不涉及页缓存的同步逻辑,一旦检测到不符合要求的读取条件,直接返回错误。
- 数据可见性延迟:写入的数据必须刷到磁盘后才能被O_DIRECT读取,否则读取的可能是旧数据。
内容的提问来源于stack exchange,提问作者rishi jain
相关产品推荐
相关产品推荐

