我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pread 和 pwrite 函数,它们是 read 和 write 系统调用的增强版本,允许在单次调用中指定文件偏移量,而不会改变文件的当前读写位置指针。
1. 函数介绍
pread (Positioned Read) 和 pwrite (Positioned Write) 是 Linux 系统调用,它们结合了 read/write 的数据传输功能和 lseek 的定位功能。
pread: 从文件描述符 fd 关联的文件中,从指定的偏移量 offset 处开始读取 count 个字节的数据,并将其存储到缓冲区 buf 中。关键点:此操作不会修改文件的当前读写位置指针(即调用 lseek(fd, 0, SEEK_CUR) 返回的值保持不变)。
pwrite: 将 count 个字节的数据从缓冲区 buf 写入到文件描述符 fd 关联的文件中,从指定的偏移量 offset 处开始写入。关键点:此操作也不会修改文件的当前读写位置指针。
你可以把它们想象成 lseek + read 或 lseek + write 的原子性组合,但又不影响文件的“书签”(当前文件偏移量)。
2. 函数原型
1 | #include <unistd.h> // 必需 |
3. 功能
pread(fd, buf, count, offset):
将文件 fd 的读取位置临时设置到 offset。
从该位置读取最多 count 个字节到 buf。
读取完成后,文件的全局读写位置指针保持不变。
pwrite(fd, buf, count, offset):
将文件 fd 的写入位置临时设置到 offset。
从 buf 写入 count 个字节到该位置。
写入完成后,文件的全局读写位置指针保持不变。
这种“原子性定位并操作”的特性在多线程环境中特别有用,可以避免多个线程同时操作同一个文件描述符的当前偏移量而导致的竞争条件(race condition)。
4. 参数
这两个函数的参数非常相似:
int fd: 有效的文件描述符。
void *buf (pread) / const void *buf (pwrite): 指向数据缓冲区的指针。
size_t count: 要读取/写入的字节数。
off_t offset: 在文件中进行读取/写入操作的绝对偏移量(从文件开头算起的字节数)。
5. 返回值
成功时:
返回实际读取/写入的字节数。这个数可能小于请求的 count(例如,在读取时接近文件末尾,或在写入时遇到磁盘空间不足)。
对于 pread,如果返回 0,通常表示偏移量已在文件末尾或没有更多数据。
失败时:
- 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF fd 无效,EINVAL offset 无效,EIO I/O 错误等)。
6. 相似函数,或关联函数
read, write: 基础的读写函数,它们的操作基于并会修改文件的当前偏移量。
lseek: 用于显式地移动文件的当前读写位置指针。pread/pwrite 内部可能使用了类似 lseek 的机制,但对用户是透明的,且不影响全局偏移量。
mmap: 另一种访问文件内容的方式,通过内存映射将文件内容映射到进程地址空间。
7. 示例代码
示例 1:基本 pread 和 pwrite 使用
这个例子演示了如何使用 pread 从文件的不同位置读取数据,以及使用 pwrite 向文件的不同位置写入数据,同时文件的当前偏移量保持不变。
1 | #include <unistd.h> // pread, pwrite, open, close, lseek |
代码解释:
data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">创建一个文件并写入一些初始数据。
使用 lseek(fd, 0, SEEK_CUR) 获取并打印当前文件偏移量(应该在文件末尾)。
pwrite 操作:
调用 pwrite 两次,分别在偏移量 5 和 30 处写入数据。
每次调用后,再次使用 lseek 检查文件偏移量,确认它没有改变。
pread 操作:
调用 pread 三次,分别从偏移量 0、15 和 50 处读取数据。
打印读取到的内容。
最后再次检查文件偏移量,确认在整个过程中它始终保持不变。
关闭文件。
示例 2:多线程环境中的 pread/pwrite
这个例子(概念性地)说明了 pread/pwrite 在多线程场景下的优势。虽然完整的多线程代码比较复杂,但我们可以通过伪代码和解释来理解。
1 | // 假想的多线程程序片段 |
代码解释 (概念性):
- 假设有多个线程共享同一个文件描述符 shared_file_fd。2. 线程 1 (thread_read_header): 需要读取文件头部。它使用 pread(fd, buf, size, 0),明确指定从偏移量 0 开始读取。这不会影响文件的全局偏移量,因此其他线程可以同时进行其他操作。3. 线程 2 (thread_read_footer): 需要读取文件尾部。它使用 pread(fd, buf, size, file_size - size),明确指定从文件末尾开始读取。同样,不影响全局偏移量。4. 线程 3 (thread_write_log): 需要在文件中间写入日志。它使用 pwrite(fd, buf, size, offset),明确指定写入位置。不影响全局偏移量。5. 如果使用传统的 lseek + read/write,线程在 lseek 和 read/write 之间可能会被切换,导致线程间相互干扰文件偏移量,产生不可预测的结果。pread/pwrite 的原子性定位和操作避免了这个问题。
总结:
pread 和 pwrite 是非常实用的系统调用,特别是在需要随机访问文件或在多线程环境中操作文件时。它们通过将定位和 I/O 操作合并为一个原子步骤,并且不修改文件的全局状态,简化了编程并提高了安全性。理解它们的关键在于掌握它们与传统 read/write + lseek 组合的区别。