Linux文件编程——read函数与lseek函数

发布于:2025-05-13 ⋅ 阅读:(8) ⋅ 点赞:(0)

一、read函数

在 Linux 文件编程中,read 函数是一个系统调用,用于从文件描述符(File Descriptor)指向的文件或设备中读取数据到缓冲区。它是 Unix/Linux 系统编程中实现底层 I/O 操作的核心函数之一。以下是 read 函数的详细使用方法和注意事项。


1. 函数原型

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • 参数
    • fd:文件描述符(通过 open 或 creat 等函数获得)。
    • buf:指向存储读取数据的缓冲区的指针。
    • count:要读取的最大字节数。
  • 返回值
    • 成功时返回实际读取的字节数(可能小于 count,例如到达文件末尾时返回 0)。
    • 失败时返回 -1,并设置 errno 表示错误原因。

2. 使用方法

基本示例
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main() {
    int fd;
    char buf[256];
    ssize_t bytes_read;

    // 打开文件(只读模式)
    fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // 读取数据
    bytes_read = read(fd, buf, sizeof(buf) - 1); // 保留一个字节给 '\0'
    if (bytes_read == -1) {
        perror("read failed");
        close(fd);
        return 1;
    }

    // 添加字符串终止符并打印
    buf[bytes_read] = '\0'; // 确保是字符串
    printf("Read %zd bytes: %s\n", bytes_read, buf);

    // 关闭文件
    close(fd);
    return 0;
}
关键步骤
  1. 打开文件
    • 使用 open 函数获取文件描述符,指定读取模式(如 O_RDONLY)。
  2. 读取数据
    • 调用 read(fd, buf, count),将数据从文件读取到 buf 中。
    • 检查返回值,确保读取成功。
  3. 处理数据
    • 如果读取的是文本数据,通常需要手动添加字符串终止符 \0
  4. 关闭文件
    • 使用 close(fd) 释放文件描述符。

3. 注意事项

(1) 返回值处理
  • 正常情况
    • 返回实际读取的字节数(可能小于 count,例如文件末尾或缓冲区不足)。
    • 返回 0 表示已到达文件末尾(EOF)。
  • 错误情况
    • 返回 -1,需检查 errno(如 EBADFEINTREIO 等)。
  • 示例代码(循环读取)
ssize_t total_read = 0;
while (total_read < count) {
    ssize_t bytes_read = read(fd, buf + total_read, count - total_read);
    if (bytes_read == -1) {
        if (errno == EINTR) continue; // 被信号中断,重试
        perror("read failed");
        break;
    } else if (bytes_read == 0) {
        break; // EOF
    }
    total_read += bytes_read;
}
(2) 错误处理
  • 常见错误包括:
    • EBADF:无效的文件描述符。
    • EINTR:被信号中断(需重试)。
    • EIO:底层 I/O 错误。
    • EAGAIN/EWOULDBLOCK:非阻塞模式下无数据可读(需结合 select/poll 使用)。
  • 使用 perror 或 strerror(errno) 打印错误信息。
(3) 缓冲与非阻塞 I/O
  • read 是无缓冲的(直接调用系统调用),但文件可能因缓冲设置(如 O_NONBLOCK)或设备特性而阻塞。
  • 对于非阻塞文件描述符(如套接字),read 可能返回 EAGAIN 或 EWOULDBLOCK,需结合 select/poll 使用。
(4) 性能优化
  • 批量读取(减少系统调用次数)。
  • 使用 mmap 映射文件到内存(适合大文件随机访问)。
  • 对于大文件,考虑 pread 进行分散/聚集读取(Scatter-Gather I/O)。
(5) 安全性
  • 确保缓冲区 buf 的大小足够,避免缓冲区溢出。
  • 验证文件描述符 fd 的合法性(如通过 fstat 检查)。
  • 避免读取未初始化的数据或越界访问。
(6) 信号中断
  • 如果 read 被信号中断(EINTR),通常需要重新尝试读取。
(7) 二进制 vs 文本数据
  • read 是字节级操作,不关心数据格式(文本或二进制)。
  • 读取文本时需手动处理换行符或编码问题。

4. 替代函数

  • pread:在指定偏移量处读取(无需 lseek)。
  • readv:聚集读取(将数据分散到多个缓冲区)。
  • recv:网络编程中用于套接字的读取(支持标志位)。

5. 常见问题

(1) 为什么 read 返回的字节数小于 count
  • 文件末尾(EOF)。
  • 缓冲区不足。
  • 信号中断(EINTR)。
  • 非阻塞 I/O 且无数据可读(EAGAIN/EWOULDBLOCK)。
(2) 如何确保读取完整数据?
  • 循环读取,直到满足条件(如读取到特定分隔符或固定长度)。
  • 示例(读取固定长度数据):
#define FIXED_SIZE 1024
char buf[FIXED_SIZE];
ssize_t total = 0;
while (total < FIXED_SIZE) {
    ssize_t ret = read(fd, buf + total, FIXED_SIZE - total);
    if (ret <= 0) break; // 错误或 EOF
    total += ret;
}
(3) 如何处理大文件?
  • 使用 mmap 映射文件到内存,避免频繁 read
  • 分块读取并处理。

6. 总结

read 是 Linux 文件编程的基础函数,使用时需注意:

  1. 正确处理返回值:区分 EOF、错误和部分读取。
  2. 错误处理:检查 errno 并处理信号中断。
  3. 性能优化:批量读取、避免频繁系统调用。
  4. 安全性:防止缓冲区溢出和非法访问。
  5. 非阻塞 I/O:结合 select/poll 使用。

通过合理使用 read,可以高效、可靠地完成文件 I/O 操作。

二、lseek函数的使用方法

1.函数原型

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • 参数
    • fd:文件描述符。
    • offset:偏移量(字节数),可正可负。
    • whence:基准点,取值如下:
      • SEEK_SET:从文件开头偏移。
      • SEEK_CUR:从当前位置偏移。
      • SEEK_END:从文件末尾偏移。
  • 返回值:成功时返回新的文件偏移量,失败时返回-1并设置errno

2.典型用途

随机访问文件:通过定位到文件的任意位置进行读写。例如,读取文件第1024字节处的512字节数据:

int fd = open("data.dat", O_RDONLY);
lseek(fd, 1024, SEEK_SET); // 定位到文件开头后1024字节处
char buf[512];
read(fd, buf, sizeof(buf));
close(fd);

获取文件大小:通过定位到文件末尾,再获取当前偏移量:

int fd = open("file.txt", O_RDONLY);
off_t size = lseek(fd, 0, SEEK_END); // 偏移量为文件大小
printf("File size: %lld bytes\n", (long long)size);
close(fd);

创建空洞文件:通过将偏移量移动到超过当前文件大小的位置,再写入数据,中间的区域会被视为空洞(不占用磁盘空间):

int fd = open("large_file.dat", O_CREAT | O_WRONLY, 0644);
lseek(fd, 1024 * 1024 * 1024 - 1, SEEK_SET); // 定位到1GB偏移处(最后一个字节)
write(fd, "", 1); // 写入一个字节,文件大小变为1GB
close(fd);

三、lseek函数的注意事项

  1. 返回值检查
    • 成功时返回新的文件偏移量,失败时返回-1,不能用<0判断,因为偏移量可能是负的(如SEEK_CURSEEK_END为基准且offset为负时)。
    • 必须检查返回值以确保操作成功。
  2. 文件类型限制
    • lseek仅适用于支持随机访问的文件(如普通文件),对管道、套接字等流式文件无效。例如,在管道或设备文件上调用lseek会失败,返回-1errno=ESPIPE
  3. 偏移量溢出风险
    • 在32位系统中,off_t为4字节,偏移量超过2GB可能导致溢出。64位系统中,off_t为8字节,可处理更大的文件。
  4. 并发与原子性
    • lseek是原子操作,多线程环境下无需额外同步,但读写操作仍需考虑线程安全。
  5. 文件打开模式的影响
    • 如果文件以追加模式(O_APPEND)打开,lseek设置的偏移量对write操作无效,write仍会追加到文件末尾。

四、lseekread函数的关系

  • 协同工作

lseek用于调整文件的读写位置,read用于从当前位置读取数据。例如:

int fd = open("file.txt", O_RDONLY);
lseek(fd, 100, SEEK_SET); // 定位到第100字节
char buf[10];
read(fd, buf, sizeof(buf)); // 从第100字节开始读取10字节
close(fd);
  • 文件偏移量的更新

read函数会更新文件的当前偏移量,而lseek可以显式地修改偏移量。两者共同决定了下一次读写操作的位置。

  • 性能优化

在需要随机访问文件时,lseek可以避免不必要的顺序读取,提高效率。例如,在数据库文件中,通过lseek直接定位到指定记录的起始位置进行读写操作。

  • 错误处理的关联性

如果lseek调用失败,后续的read操作可能会读取到错误的数据或返回错误。因此,必须在使用lseek后检查其返回值。


网站公告

今日签到

点亮在社区的每一天
去签到