深入理解 Linux 文件 IO:从基础接口到实战应用

在 Linux 系统编程中,文件 IO 是核心基础能力之一。相比于我们日常开发中接触的标准 IO,文件 IO 作为内核提供的系统调用接口,在处理设备文件、通信文件等场景下具备不可替代的优势。本文将从文件 IO 与标准 IO 的核心区别入手,系统讲解文件 IO、目录 IO、时间相关的核心接口,并通过实战案例帮助你掌握文件 IO 的实际应用。

一、文件 IO vs 标准 IO:核心区别解析

在开始学习文件 IO 之前,我们首先要明确它与标准 IO(stdio)的核心差异,这能帮助我们在实际开发中做出正确的技术选型。

表格

特性 标准 IO (stdio) 文件 IO (系统调用)
缓存机制 有用户态缓存(全缓冲 / 行缓冲) 无缓存(直接操作内核缓冲区)
实现层面 库函数(对系统调用的封装) 系统调用(内核函数接口)
适用场景 普通文件(文本 / 二进制文件) 设备文件、通信文件(管道 / 套接字)
接口示例 fopen/fclose/fread/fwrite open/close/read/write

简单来说:

  • 标准 IO 为了提升普通文件的读写效率,在用户层增加了缓存机制,是对文件 IO 的封装;
  • 文件 IO 直接与内核交互,无额外缓存,更适合对实时性要求高的设备 / 通信场景。

二、文件 IO 核心接口详解

文件 IO 的核心接口包括open/close/read/write/lseek,这些接口是操作文件的基础,我们逐一拆解学习。

1. open:打开 / 创建文件

open是文件 IO 的入口,用于打开或创建文件,并返回一个文件描述符(非负整数,内核用于标识打开的文件)。

函数原型

c

运行

复制代码
// 打开已存在的文件
int open(const char *pathname, int flags);
// 创建并打开新文件(文件不存在时)
int open(const char *pathname, int flags, mode_t mode);
关键参数说明
  • pathname:文件路径(绝对 / 相对路径);
  • flags :打开方式(必选 + 可选组合):
    • 必选(三选一):O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写);
    • 可选:O_CREAT(创建文件)、O_TRUNC(清空文件)、O_EXCL(文件存在则报错)、O_APPEND(追加写入);
  • mode :文件权限(仅O_CREAT时有效),如0664(rw-rw-r--)、0777(rwxrwxrwx)(最终权限会受 umask 掩码影响)。
常用 flags 组合(对应标准 IO 的 r/r+/w/w+/a/a+)

表格

标准 IO 模式 文件 IO flags 组合 说明
r O_RDONLY 只读打开已存在文件
r+ O_RDWR 读写打开已存在文件
w O_WRONLY | O_CREAT | O_TRUNC, 0664 只写,不存在则创建,存在则清空
w+ O_RDWR | O_CREAT | O_TRUNC, 0664 读写,不存在则创建,存在则清空
a O_WRONLY | O_CREAT | O_APPEND, 0664 追加写入,不存在则创建
a+ O_RDWR | O_CREAT | O_APPEND, 0664 追加 + 读取,不存在则创建
返回值
  • 成功:返回新的文件描述符(Linux 默认已打开 0/1/2:标准输入 / 输出 / 错误);
  • 失败:返回 - 1(需检查 errno)。

2. close:关闭文件描述符

c

运行

复制代码
int close(int fd);
  • 功能:释放文件描述符,关闭对应文件;
  • 参数:fdopen返回的文件描述符;
  • 返回值:成功返回 0,失败返回 - 1。

3. write:写入文件

c

运行

复制代码
ssize_t write(int fd, const void *buf, size_t count);
  • 功能:将buf指向的内存中count个字节写入fd对应的文件;
  • 参数:
    • fd:文件描述符;
    • buf:待写入数据的内存首地址;
    • count:期望写入的字节数;
  • 返回值:
    • 成功:实际写入的字节数(可能小于 count,如磁盘满);
    • 失败:返回 - 1。

4. read:读取文件

c

运行

复制代码
ssize_t read(int fd, void *buf, size_t count);
  • 功能:从fd对应的文件中读取最多count个字节到buf指向的内存;
  • 参数:
    • fd:文件描述符;
    • buf:存放读取数据的内存首地址;
    • count:最大读取字节数;
  • 返回值:
    • 成功:实际读取的字节数;
    • 读到文件末尾:返回 0;
    • 失败:返回 - 1。

5. lseek:调整文件偏移量

文件 IO 通过 "偏移量" 记录当前读写位置,lseek用于调整该偏移量:

运行

复制代码
off_t lseek(int fd, off_t offset, int whence);
  • 参数:
    • offset:偏移量(可正可负);
    • whence:偏移基准:
      • SEEK_SET:文件开头;
      • SEEK_CUR:当前位置;
      • SEEK_END:文件末尾;
  • 返回值:成功返回调整后的偏移量(相对于文件开头),失败返回 - 1。
常用场景

c

运行

复制代码
// 移动到文件开头
lseek(fd, 0, SEEK_SET);
// 移动到文件末尾(可获取文件大小)
off_t file_size = lseek(fd, 0, SEEK_END);
// 从当前位置向后偏移100字节
lseek(fd, 100, SEEK_CUR);

三、实战:文件 IO 实现图片拷贝

掌握了核心接口后,我们通过 "图片拷贝" 实战巩固知识点。该案例通过open打开源文件和目标文件,循环read源文件数据并write到目标文件,实现二进制文件的完整拷贝。

完整代码

运行:

编译与运行

bash

运行

复制代码
# 编译
gcc copy_image.c -o copy_image
# 运行(替换为实际图片路径)
./copy_image ./source.jpg ./target.jpg

关键要点

  1. 使用4096字节缓冲区:平衡内存占用和系统调用次数,提升拷贝效率;
  2. 错误处理:每个系统调用后检查返回值,避免文件描述符泄漏;
  3. 处理write返回值:确保写入字节数与读取字节数一致,避免数据丢失。

四、目录 IO:操作目录的核心接口

除了普通文件,Linux 中目录也是一种文件类型,目录 IO 提供了专门的接口来操作目录:

1. opendir/closedir:打开 / 关闭目录

c

运行

复制代码
#include <dirent.h>
// 打开目录,返回目录流指针
DIR *opendir(const char *name);
// 关闭目录流
int closedir(DIR *dirp);

2. readdir:读取目录项

复制代码
struct dirent *readdir(DIR *dirp);
  • 功能:读取目录中的下一个目录项(文件 / 子目录);

  • 返回值:成功返回目录项结构体指针,读到末尾 / 失败返回 NULL;

  • 核心结构体struct dirent

    c

    运行

    复制代码
    struct dirent {
        ino_t d_ino;          // 索引节点号
        off_t d_off;          // 目录流偏移量
        unsigned short d_reclen; // 结构体长度
        unsigned char d_type; // 文件类型(如DT_REG普通文件、DT_DIR目录)
        char d_name[256];     // 文件名(核心字段)
    };

3. 目录操作辅助接口

表格

接口 原型 功能
mkdir int mkdir(const char *path, mode_t mode); 创建目录(权限通常 0777)
rmdir int rmdir(const char *path); 删除空目录
chdir int chdir(const char *path); 切换当前工作目录
getcwd char *getcwd(char *buf, size_t size); 获取当前工作目录绝对路径

五、时间相关接口:文件操作的时间辅助

在文件 IO 中,我们常需要记录文件操作时间(如创建 / 修改时间),Linux 提供了一套时间转换接口:

1. time:获取秒级时间戳

c

运行

复制代码
#include <time.h>
time_t time(time_t *tloc);
  • 功能:获取从 1970-01-01 00:00:00 UTC 到现在的秒数(时间戳);
  • 返回值:成功返回时间戳,失败返回 - 1。

2. localtime:时间戳转本地日历时间

c

运行

复制代码
struct tm *localtime(const time_t *timep);
  • 功能:将时间戳转换为包含时区的日历时间结构体;

  • 核心结构体struct tm

    c

    运行

    复制代码
    struct tm {
        int tm_sec;   // 秒(0-60)
        int tm_min;   // 分(0-59)
        int tm_hour;  // 时(0-23)
        int tm_mday;  // 日(1-31)
        int tm_mon;   // 月(0-11,需+1)
        int tm_year;  // 年(需+1900)
        int tm_wday;  // 星期(0-6,周日为0)
        int tm_yday;  // 年中天数(0-365)
        int tm_isdst; // 夏令时标识
    };

3. mktime:日历时间转时间戳

c

运行

复制代码
time_t mktime(struct tm *tm);
  • 功能:将struct tm转换回时间戳,常用于时间计算。

示例:获取当前时间并格式化输出

c

运行

复制代码
#include <stdio.h>
#include <time.h>

int main() {
    // 1. 获取时间戳
    time_t now = time(NULL);
    // 2. 转换为本地时间
    struct tm *tm_now = localtime(&now);
    // 3. 格式化输出(注意tm_year和tm_mon的偏移)
    printf("当前时间:%d年%d月%d日 %d:%d:%d\n",
           tm_now->tm_year + 1900,
           tm_now->tm_mon + 1,
           tm_now->tm_mday,
           tm_now->tm_hour,
           tm_now->tm_min,
           tm_now->tm_sec);
    return 0;
}

六、文件描述符的核心特性

  1. 上限限制 :系统对每个进程的文件描述符数量有上限(可通过ulimit -n查看 / 修改);
  2. 最小未使用原则:新创建的文件描述符总是选择当前最小的、未被使用的非负整数;
  3. 默认打开:进程启动时默认打开 3 个文件描述符:0(标准输入)、1(标准输出)、2(标准错误)。

总结

本文系统讲解了 Linux 文件 IO 的核心知识点,关键总结如下:

  1. 文件 IO 与标准 IO 的核心差异:文件 IO 是无缓存的系统调用,适合设备 / 通信文件;标准 IO 是有缓存的库函数,适合普通文件;
  2. 文件 IO 核心流程open(打开 / 创建文件)→ read/write(读写数据)→ lseek(调整偏移)→ close(关闭文件);
  3. 实战关键:二进制文件拷贝需使用固定缓冲区循环读写,且要严格处理系统调用的返回值,避免数据丢失或文件描述符泄漏。

掌握文件 IO 是 Linux 系统编程的基础,后续无论是网络编程、设备驱动开发还是进程间通信,都离不开这些核心接口的应用。建议结合本文的示例代码多做实战,加深对接口的理解和使用熟练度。

相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell9 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
少云清9 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木9 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076609 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声9 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易9 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
偷吃的耗子10 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn