当前位置: 首页 > java >正文

从竞态到原子:pread/pwrite 如何重塑高效文件 I/O?

在日常的文件 I/O 编程中,我们最熟悉的莫过于 read()write() 系统调用。它们是处理文件操作的基石。然而,在多线程或需要精确控制文件偏移量的场景下,这两个基础调用可能会显得笨拙甚至导致问题。这就是 Linux 和 Unix 系统提供 pread()pwrite() 的原因所在。

本文将深入探讨这两个强大的系统调用,帮助你提升 I/O 操作的效率和正确性。

1. 传统方式的痛点:read/write + lseek

在介绍新朋友之前,我们先回顾一下老朋友的工作方式。传统的文件读取流程通常是这样的:

  1. 使用 lseek() 系统调用将文件偏移量移动到目标位置。
  2. 调用 read() 从当前偏移量开始读取数据,read() 会自动推进偏移量。

写入流程也是类似的 lseek() + write()

这种方法在单线程环境下工作良好,但在多线程环境下有一个致命的缺陷:竞争条件(Race Condition)。

竞争条件示例

想象以下场景,两个线程共享同一个文件描述符(fd):

  1. 线程 A 希望读取文件 100 字节处的数据。
  2. 线程 B 希望读取文件 200 字节处的数据。
  3. 线程 A 成功调用 lseek(fd, 100, SEEK_SET),将偏移量设置为 100。
  4. 就在此时,操作系统调度器暂停了线程 A,并唤醒了线程 B。
  5. 线程 B 执行 lseek(fd, 200, SEEK_SET),成功将偏移量修改为 200。
  6. 线程 B 调用 read(fd, buffer, size),从偏移量 200 处开始读取。
  7. 线程 B 完成操作,操作系统再次调度线程 A。
  8. 线程 A 从它停止的地方继续,调用 read(fd, buffer, size)
  9. 问题来了! 文件偏移量现在是由线程 B 设置的 200,而不是线程 A 期望的 100。线程 A 读到了错误的数据。
时间线     线程A操作                   线程B操作                   文件偏移量
----------------------------------------------------------------------------t0                                                             0t1     lseek(fd, 100, SEEK_SET)                               100t2                             (线程切换)                      100t3                              lseek(fd, 200, SEEK_SET)       200t4                              read(fd, buffer, size)         200 + sizet5     (线程切换)                                               200 + sizet6     read(fd, buffer, size)                                  200 + size + size

图示:两个线程交替操作共享的文件偏移量,导致数据错乱。线程A本想读取100处的数据,却读到了200+size处的数据。

这是因为文件偏移量是内核中与文件描述符关联的一个属性,是全局共享的状态。传统的 read/write 隐式地使用和修改这个共享状态,从而导致了并发问题。

解决这个问题通常需要引入互斥锁(Mutex) 来保护 lseek + read/write 这个操作序列,使其成为原子操作。但这会增加代码的复杂性和锁的开销。

2. 更优雅的解决方案:pread 和 pwrite

pread() (Positional Read) 和 pwrite() (Positional Write) 就是为了解决上述问题而设计的。它们将"定位(Seek)"和"读写(Read/Write)"两个操作合并为一个单一的、原子的系统调用。

函数原型

#include <unistd.h>ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • fd:文件描述符。
  • buf:用于存储读取数据或待写入数据的缓冲区。
  • count:要读取或写入的字节数。
  • offset关键的参数! 指定从文件的哪个偏移量开始进行读写操作。

核心优势:原子性

最重要的特点是:preadpwrite 在执行时,不会改变文件描述符关联的文件偏移量。

pread(fd, buf, count, offset) 调用严格等价于以下代码序列的原子执行

off_t old_offset = lseek(fd, 0, SEEK_CUR); // 保存原偏移量
lseek(fd, offset, SEEK_SET);
read(fd, buf, count);
lseek(fd, old_offset, SEEK_SET); // 恢复原偏移量

但它是在内核层面一气呵成的,不会被其他线程中断。

这带来了三个巨大好处:

  1. 线程安全:因为它们不依赖也不修改全局的文件偏移量,多个线程可以同时使用同一个文件描述符调用 preadpwrite 而无需任何锁。它们彼此之间不会产生干扰。
  2. 避免副作用:由于文件偏移量保持不变,你可以在调用 pread/pwrite 前后,放心地使用传统的 read/write,而不用担心偏移量被意外修改。
  3. 代码更简洁:无需再手动调用 lseek,代码意图更加清晰——“请直接从 offset 位置读取 count 字节的数据”。

3. 代码示例对比

让我们通过一个简单的例子来感受两者的区别。

任务:从文件的开头和第 100 字节处分别读取 50 字节的数据。

使用传统方式(lseek + read)
// ... 打开文件获得 fd ...
int fd = open("file.txt", O_RDONLY);
char buf1[50], buf2[50];// 读取开头50字节
lseek(fd, 0, SEEK_SET);   // 手动定位
read(fd, buf1, 50);       // 偏移量变为50// 读取偏移量100处50字节
lseek(fd, 100, SEEK_SET); // 再次定位(易被其他线程干扰)
read(fd, buf2, 50);       // 偏移量变为150// 如果你想再回到开头读,又得重新lseek
使用 pread(推荐)
// ... 打开文件获得 fd ...
int fd = open("file.txt", O_RDONLY);
char buf1[50], buf2[50];pread(fd, buf1, 50, 0);   // 直接从偏移量0读取,不修改全局偏移
pread(fd, buf2, 50, 100); // 直接从偏移量100读取,全局偏移仍为初始值// 文件偏移量从头到尾都没有被改变过!
// 可以随意混合使用 pread 和传统 read,互不干扰

可以看到,使用 pread 的代码更简洁,意图更明确,并且天生就是线程安全的。

4. 重要注意事项

  1. 偏移量类型offset 参数是 off_t 类型,通常在 32 位系统上是 32 位,在 64 位系统上是 64 位。这意味着它可以支持大于 4GB 的大文件。
  2. 并发写入pwrite 的原子性保证的是"定位+写入"这个动作的原子性,而不是文件内容的原子性。如果你的数据块大小超过了一个磁盘扇区(通常是512字节),一次 pwrite 操作可能最终被分解为多次磁盘写入。如果需要更严格的原子性(如所有数据全部成功或全部失败),需要考虑事务或日志文件系统等其他机制。
  3. 适用文件类型preadpwrite 主要适用于常规文件。对于管道、套接字或某些特殊设备文件,它们可能无法使用(ESPIPE 错误),因为这些对象不支持"寻址"的概念。

5. 总结与适用场景

特性read/writepread/pwrite
文件偏移量使用并修改全局偏移量忽略不修改全局偏移量
线程安全不安全,需加锁天生安全
操作原子性非原子(lseek+IO原子操作
代码简洁性需配合 lseek更简洁,意图更明确

强烈建议在以下场景中使用 preadpwrite

  • 多线程编程:当多个线程操作同一个文件描述符时,这是首选方案。
  • 随机 I/O:需要在文件的不同位置进行读取或写入,特别是多次、跳跃式的访问(例如数据库操作)。
  • 保持偏移量:当你希望在执行一些特定位置的 I/O 后,文件偏移量仍然保持在原来的位置。

总之,preadpwrite 是文件 I/O 工具箱中两颗被低估的明珠。它们提供了更强的线程安全保证和更清晰的编程语义。下次当你需要从文件的特定位置读写数据时,请优先考虑它们,这会让你的代码更加健壮和高效。

http://www.xdnf.cn/news/19836.html

相关文章:

  • 如何使文件夹内的软件或者文件不受windows 安全中心的监视
  • Java8特性
  • 【HarmonyOS 6】仿AI唤起屏幕边缘流光特效
  • leetcode-每日一题-人员站位的方案数-C语言
  • Spring 循环依赖问题
  • 《LINUX系统编程》笔记p8
  • 大模型RAG项目实战:RAG技术原理及核心架构
  • SpringBoot 事务管理避坑指南
  • 机器学习:从技术原理到实践应用的深度解析
  • 机器人抓取中的力学相关概念解释
  • JVM中产生OOM(内存溢出)的8种典型情况及解决方案
  • 初识NOSQL
  • 方法决定效率
  • git: 取消文件跟踪
  • SRE团队是干嘛的
  • 关于IDE的相关知识之一【使用技巧】
  • Spring Security 如何使用@PreAuthorize注解
  • Nano Banana 新玩法超惊艳!附教程案例提示词!
  • AI 设计工具天花板
  • 【android bluetooth 协议分析 21】【ble 介绍 3】【ble acl Supervision Timeout 介绍】
  • 黑马头条面试重点业务
  • 构建下一代智能金融基础设施
  • SpringBoot--手写日期格式转换工具类
  • TiDB v8.5.3 单机集群部署指南
  • ASP.NET Core上传文件到minio
  • 【leetcode】236. 二叉树的最近公共祖先
  • 利用Base64传输二进制文件并执行的方法(适合没有ssh ftp等传输工具的嵌入式离线场景)
  • 研发文档版本混乱的根本原因是什么,怎么办
  • ELK 统一日志分析系统部署与实践指南(上)
  • 撤销修改 情况⼀:对于⼯作区的代码,还没有 add