pread系统调用及示例

我们继续学习 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. 函数原型

#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);

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. 相似函数,或关联函数

  • readwrite: 基础的读写函数,它们的操作基于并会修改文件的当前偏移量。
  • lseek: 用于显式地移动文件的当前读写位置指针。pread/pwrite 内部可能使用了类似 lseek 的机制,但对用户是透明的,且不影响全局偏移量。
  • mmap: 另一种访问文件内容的方式,通过内存映射将文件内容映射到进程地址空间。

7. 示例代码

示例 1:基本 pread 和 pwrite 使用

这个例子演示了如何使用 pread 从文件的不同位置读取数据,以及使用 pwrite 向文件的不同位置写入数据,同时文件的当前偏移量保持不变。

#include <unistd.h>  // pread, pwrite, open, close, lseek
#include <fcntl.h>   // O_RDWR, O_CREAT
#include <stdio.h>   // perror, printf
#include <stdlib.h>  // exit
#include <string.h>  // strlen

int main() {
    int fd;
    const char *filename = "pread_pwrite_example.txt";
    const char *initial_data = "This is the initial content of the file.\nIt spans multiple lines.\n";
    const char *write_data1 = "[OVERWRITTEN_PART_1]";
    const char *write_data2 = "[OVERWRITTEN_PART_2]";
    char read_buffer[100];
    ssize_t bytes_rw;
    off_t current_offset;

    // 1. 创建并写入初始数据
    fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open for creation");
        exit(EXIT_FAILURE);
    }

    if (write(fd, initial_data, strlen(initial_data)) == -1) {
        perror("write initial data");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("Created file '%s' and wrote initial data.\n", filename);

    // 2. 获取并打印当前文件偏移量 (应在文件末尾)
    current_offset = lseek(fd, 0, SEEK_CUR);
    if (current_offset == -1) {
        perror("lseek to get current offset");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("Current file offset after initial write: %ld\n", (long)current_offset);

    // --- 使用 pwrite 进行写入 ---
    printf("\n--- Using pwrite ---\n");
    // 在偏移量 5 处写入数据
    bytes_rw = pwrite(fd, write_data1, strlen(write_data1), 5);
    if (bytes_rw == -1) {
        perror("pwrite 1");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("pwrite 1: Wrote %zd bytes at offset 5.\n", bytes_rw);

    // 在偏移量 30 处写入另一部分数据
    bytes_rw = pwrite(fd, write_data2, strlen(write_data2), 30);
    if (bytes_rw == -1) {
        perror("pwrite 2");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("pwrite 2: Wrote %zd bytes at offset 30.\n", bytes_rw);

    // 3. 再次检查当前文件偏移量 (应该没有改变)
    off_t offset_after_pwrite = lseek(fd, 0, SEEK_CUR);
    if (offset_after_pwrite == -1) {
        perror("lseek after pwrite");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("Current file offset after pwrite calls: %ld (Should be same as before)\n",
           (long)offset_after_pwrite);

    // --- 使用 pread 进行读取 ---
    printf("\n--- Using pread ---\n");
    // 从偏移量 0 开始读取 20 个字节
    bytes_rw = pread(fd, read_buffer, 20, 0);
    if (bytes_rw == -1) {
        perror("pread 1");
        close(fd);
        exit(EXIT_FAILURE);
    }
    read_buffer[bytes_rw] = '\0'; // 确保字符串结束
    printf("pread 1: Read %zd bytes from offset 0: '%s'\n", bytes_rw, read_buffer);

    // 从偏移量 15 开始读取 25 个字节
    bytes_rw = pread(fd, read_buffer, 25, 15);
    if (bytes_rw == -1) {
        perror("pread 2");
        close(fd);
        exit(EXIT_FAILURE);
    }
    read_buffer[bytes_rw] = '\0';
    printf("pread 2: Read %zd bytes from offset 15: '%s'\n", bytes_rw, read_buffer);

    // 从偏移量 50 开始读取 10 个字节 (可能读到文件末尾)
    bytes_rw = pread(fd, read_buffer, 10, 50);
    if (bytes_rw == -1) {
        perror("pread 3");
        close(fd);
        exit(EXIT_FAILURE);
    } else if (bytes_rw == 0) {
        printf("pread 3: Read %zd bytes from offset 50 (likely EOF).\n", bytes_rw);
    } else {
        read_buffer[bytes_rw] = '\0';
        printf("pread 3: Read %zd bytes from offset 50: '%s'\n", bytes_rw, read_buffer);
    }

    // 4. 最后再次确认文件偏移量未变
    off_t final_offset = lseek(fd, 0, SEEK_CUR);
    if (final_offset == -1) {
        perror("lseek to get final offset");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("\nFinal file offset: %ld (Should still be the same)\n", (long)final_offset);

    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("File operations completed. Check the file content.\n");
    return 0;
}

代码解释:

  1. 创建一个文件并写入一些初始数据。
  2. 使用 lseek(fd, 0, SEEK_CUR) 获取并打印当前文件偏移量(应该在文件末尾)。
  3. pwrite 操作:
    • 调用 pwrite 两次,分别在偏移量 5 和 30 处写入数据。
    • 每次调用后,再次使用 lseek 检查文件偏移量,确认它没有改变。
  4. pread 操作:
    • 调用 pread 三次,分别从偏移量 0、15 和 50 处读取数据。
    • 打印读取到的内容。
  5. 最后再次检查文件偏移量,确认在整个过程中它始终保持不变。
  6. 关闭文件。

示例 2:多线程环境中的 pread/pwrite

这个例子(概念性地)说明了 pread/pwrite 在多线程场景下的优势。虽然完整的多线程代码比较复杂,但我们可以通过伪代码和解释来理解。

// 假想的多线程程序片段

#include <pthread.h> // POSIX 线程
#include <unistd.h>  // pread, pwrite
// ... 其他包含 ...

int shared_file_fd; // 所有线程共享的文件描述符

// 线程函数 1: 读取文件头部
void* thread_read_header(void *arg) {
    char header_buf[HEADER_SIZE];
    ssize_t bytes_read;

    // 线程 1 总是从偏移量 0 读取头部
    // 使用 pread 确保不影响其他线程的文件位置
    bytes_read = pread(shared_file_fd, header_buf, HEADER_SIZE, 0);
    if (bytes_read > 0) {
        // 处理头部数据...
        process_header(header_buf, bytes_read);
    }
    return NULL;
}

// 线程函数 2: 读取文件尾部
void* thread_read_footer(void *arg) {
    char footer_buf[FOOTER_SIZE];
    ssize_t bytes_read;
    off_t file_size;

    // 获取文件大小 (可能需要预先获取或用 fstat)
    file_size = get_file_size_somehow(shared_file_fd);

    // 线程 2 总是从文件末尾倒数的位置读取尾部
    // 使用 pread 确保不影响其他线程的文件位置
    bytes_read = pread(shared_file_fd, footer_buf, FOOTER_SIZE, file_size - FOOTER_SIZE);
    if (bytes_read > 0) {
        // 处理尾部数据...
        process_footer(footer_buf, bytes_read);
    }
    return NULL;
}

// 线程函数 3: 在文件中间某处写入日志
void* thread_write_log(void *arg) {
    const char *log_msg = "Log entry from thread 3\n";
    off_t write_offset = (off_t)arg; // 假设偏移量通过 arg 传入

    // 线程 3 在指定位置写入日志
    // 使用 pwrite 确保不影响其他线程的文件位置
    ssize_t bytes_written = pwrite(shared_file_fd, log_msg, strlen(log_msg), write_offset);
    if (bytes_written == -1) {
        perror("pwrite in thread 3");
    } else {
        printf("Thread 3 wrote %zd bytes at offset %ld\n", bytes_written, (long)write_offset);
    }
    return NULL;
}

// 主函数 (概念性)
int main() {
    // ... 打开文件 shared_file_fd ...

    pthread_t t1, t2, t3;

    // 创建线程
    pthread_create(&t1, NULL, thread_read_header, NULL);
    pthread_create(&t2, NULL, thread_read_footer, NULL);
    pthread_create(&t3, NULL, thread_write_log, (void*)MIDDLE_OFFSET); // 传递写入偏移量

    // 等待线程完成
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    // ... 关闭文件 ...
    return 0;
}

代码解释 (概念性):

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 组合的区别。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注