文件IO:读写

文件IO:读写

Linux 系统编程:文件 I/O 操作详解 (read/write/open)

本笔记基于视频内容整理,并结合 Linux 标准文档(man pages)进行了技术细节的校正与补充。内容涵盖 readwriteopen 函数的核心用法、参数详解、返回值处理、文件权限标志以及命令行参数传递的最佳实践。

1. read 函数详解

read 函数用于从文件描述符对应的文件中读取数据。

1.1 函数原型与头文件

根据 man 2 read 手册,函数定义如下:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • 头文件#include <unistd.h>(与 close 函数相同,无需重复添加)。
  • 返回值类型ssize_t(有符号整型,通常在 Linux 中被定义为 intlong)。

1.2 参数说明

1. int fd

  • 文件描述符(File Descriptor)。
  • 来源:必须是之前通过 open 函数成功打开文件后返回的有效描述符。

2. void *buf

  • 缓冲区指针,用于存储读取到的数据。
  • 类型兼容性:虽然原型是 void *,实际使用时通常传递 char 型数组的首地址。C 语言允许隐式强制转换,因此传入 char 数组是合法的。
  • 注意:必须确保该内存空间已分配且足够大,防止缓冲区溢出。

3. size_t count

  • 期望读取的最大字节数。
  • 限制:不能超过 buf 指向的缓冲区实际大小。如果尝试读取超过缓冲区容量的数据,会导致数据丢失或程序异常(Segmentation Fault)。

1.3 返回值处理

read 的返回值至关重要,用于判断操作状态:

  • > 0:成功读取的实际字节数。
  • 0:读到文件末尾(End Of File, EOF)。表示没有更多内容可读,但并非错误。
  • -1:出错。此时全局变量 errno 会被设置以指示具体错误原因。
    • 视频中提到 EOF 宏定义通常为 -1,判断出错时可以直接比较 -1 或使用 EOF

1.4 读取逻辑与循环

由于文件内容可能超过单次 read 的缓冲区大小,通常需要使用循环读取直到文件结束。

推荐模式(while 循环):

ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
    // 处理读取到的数据,例如打印
    write(STDOUT_FILENO, buf, n);
}
if (n == -1) {
    // 处理错误
    perror("read error");
}
  • 逻辑解释
    • n > 0 时,继续循环读取。
    • n == 0 时,循环终止,表示文件读完。
    • n < 0 时,循环终止,表示发生错误。
  • 视频中的优化点
    • 即使文件为空(第一次 read 就返回 0),循环也不会进入,但程序逻辑应能区分“未执行读取”和“读取到 0 字节”。
    • 建议增加计数器变量,累计读取的总字节数,以便调试和验证。

1.5 示例代码:安全读取文件

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open error");
        return -1;
    }

    char buf[20];
    ssize_t n;
    int total = 0;

    // 循环读取直到 EOF 或 error
    while ((n = read(fd, buf, sizeof(buf))) > 0) {
        write(STDOUT_FILENO, buf, n); // 输出到屏幕
        total += n;
    }

    if (n == -1) {
        perror("read error");
    } else {
        printf("\nRead finished. Total bytes: %d\n", total);
    }

    close(fd);
    return 0;
}

2. write 函数详解

write 函数用于向文件描述符对应的文件中写入数据。

2.1 函数原型与头文件

根据 man 2 write 手册:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

2.2 参数说明

1. int fd

  • 目标文件的描述符,必须通过 open 以写权限(O_WRONLYO_RDWR)打开。

2. const void *buf

  • 要写入的数据源地址。
  • 可以是字符串常量(如 "Hello World")、字符数组首地址等。
  • 由于是 const,函数内部不会修改缓冲区内容。

3. size_t count

  • 期望写入的字节数。
  • 计算方式
    • 固定长度:直接使用数字(如 20)。
    • 动态长度:使用 strlen() 函数计算字符串长度(需包含 <string.h>)。
    • 注意strlen 不包含结尾的 \0,而 sizeof 包含。写入文本文件时通常使用 strlen 或手动加 1。

2.3 返回值处理

  • > 0:成功写入的实际字节数。
    • 注意:返回值可能小于请求的 count(例如磁盘空间不足),健壮的程序应检查是否写完。
  • -1:写入失败,设置 errno

2.4 写入逻辑

视频中演示了使用 do-whilewhile 循环确保数据完全写入的逻辑,但在简单示例中,单次写入通常能完成。

健壮写入模式:

size_t total_written = 0;
while (total_written < count) {
    ssize_t n = write(fd, buf + total_written, count - total_written);
    if (n == -1) {
        perror("write error");
        break;
    }
    total_written += n;
}

2.5 示例代码:写入字符串

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }

    // 使用 O_WRONLY | O_CREAT | O_TRUNC 确保可写且创建文件
    int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open error");
        return -1;
    }

    char *msg = "Hello World\n";
    ssize_t n = write(fd, msg, strlen(msg));

    if (n == -1) {
        perror("write error");
    } else {
        printf("Successfully wrote %zd bytes\n", n);
    }

    close(fd);
    return 0;
}

3. open 函数标志位与权限控制

文件打开方式直接决定了后续 readwrite 操作的合法性。

3.1 常见标志位 (Flags)

  • O_RDONLY:只读模式。
    • 允许:read
    • 禁止:write(若尝试写入,write 返回 -1,程序可能陷入错误循环)。
  • O_WRONLY:只写模式。
    • 允许:write
    • 禁止:read(若尝试读取,read 返回 -1)。
  • O_RDWR:读写模式。
    • 允许:readwrite
  • 组合标志
    • O_CREAT:如果文件不存在则创建。
    • O_TRUNC:如果文件存在且可写,将长度截断为 0。
    • O_APPEND:每次写入前将文件偏移量移动到文件末尾。

3.2 权限验证实验结论

视频中通过修改 open 的标志位进行了测试:

  1. 只读 (O_RDONLY)
    • 执行 read:成功。
    • 执行 write:失败(返回 -1),程序若未检查返回值可能陷入死循环或逻辑错误。
  2. 只写 (O_WRONLY)
    • 执行 read:失败。
    • 执行 write:成功。
  3. 读写 (O_RDWR)
    • 执行 readwrite:均成功。

最佳实践:在 open 时必须根据后续操作需求选择合适的标志位,并在代码中检查 read/write 的返回值以处理权限不足的情况。

4. 命令行参数处理 (argc & argv)

为了使程序更灵活,视频演示了通过命令行传递文件名,而不是硬编码在源代码中。

4.1 参数结构

int main(int argc, char *argv[])
  • argc (Argument Count):参数个数。
    • 至少为 1(程序本身名称 argv[0])。
  • argv (Argument Vector):参数字符串数组。
    • argv[0]:程序执行名称(如 ./a.out)。
    • argv[1]:第一个用户传入参数(如文件名 test.txt)。

4.2 安全性检查

在访问 argv[1] 之前,必须检查 argc 的值。

  • 错误做法:直接使用 open(argv[1], ...)。如果用户未传参,argv[1]NULL,会导致段错误(Segmentation Fault)。
  • 正确做法
if (argc < 2) {
    printf("Usage: %s <filename>\n", argv[0]);
    return -1; // 提前退出,避免非法内存访问
}

4.3 编译与运行

  • 编译gcc main.c -o main
  • 运行./main test.txt
  • 注意:视频中出现 ./a.out 是默认生成的可执行文件名。

5. 综合示例:读写完整流程

结合视频内容,以下是一个完整的、经过优化的文件读写程序示例,包含了错误处理、参数检查和权限管理。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // 1. 参数检查
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return EXIT_FAILURE;
    }

    // 2. 打开文件 (使用 O_RDWR 以便同时测试读写)
    // 若文件不存在则创建,权限设为 0644
    int fd = open(argv[1], O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open failed");
        return EXIT_FAILURE;
    }

    // 3. 写操作
    const char *write_buf = "Hello Linux IO\n";
    ssize_t write_n = write(fd, write_buf, strlen(write_buf));
    if (write_n == -1) {
        perror("write failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Written %zd bytes\n", write_n);

    // 4. 重置文件偏移量 (重要:写完后指针在末尾,读之前需移回开头)
    lseek(fd, 0, SEEK_SET);

    // 5. 读操作
    char read_buf[1024];
    ssize_t read_n;
    printf("Reading content:\n");
    while ((read_n = read(fd, read_buf, sizeof(read_buf))) > 0) {
        write(STDOUT_FILENO, read_buf, read_n);
    }
  
    if (read_n == -1) {
        perror("read failed");
    }
    printf("\nRead operation finished.\n");

    // 6. 关闭文件
    close(fd);
    return EXIT_SUCCESS;
}

6. 常见问题与调试建议 (基于视频讨论)

1. 读取返回 0 的问题

  • 现象:程序运行后提示“读了 0 个字符”。
  • 原因:文件本身为空,或者文件偏移量已经在文件末尾。
  • 解决:检查文件内容;如果是多次读写,注意使用 lseek 重置偏移量。

2. 死循环风险

  • 现象:在使用 while 循环写入时,如果权限不足(如只读打开),write 返回 -1,若循环条件判断不当(如 while(n >= 0)),可能导致死循环。
  • 解决:严格判断返回值 if (n == -1) break;

3. 缓冲区初始化

  • 建议:定义字符数组后,最好进行初始化(如 char buf[20] = {0};),避免打印时出现乱码(尽管 read 返回了长度,但调试打印字符串时 \0 很重要)。

4. 头文件依赖

  • read/write/close<unistd.h>
  • open<fcntl.h>
  • strlen<string.h>
  • printf<stdio.h>

7. 总结

  • 核心流程open -> read/write (循环处理) -> close
  • 返回值检查:必须检查系统调用的返回值,区分成功、EOF 和错误。
  • 权限匹配open 的标志位必须与后续操作(读/写)相匹配。
  • 健壮性:通过 argc 检查命令行参数,通过循环处理不定长数据,通过错误处理应对异常情况。
文件IO:文件描述符、打开与关闭 2026-02-25
文件IO:文件定位、权限修改 2026-02-25

评论区