在 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);
- 功能:释放文件描述符,关闭对应文件;
- 参数:
fd为open返回的文件描述符; - 返回值:成功返回 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
关键要点
- 使用
4096字节缓冲区:平衡内存占用和系统调用次数,提升拷贝效率; - 错误处理:每个系统调用后检查返回值,避免文件描述符泄漏;
- 处理
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;
}
六、文件描述符的核心特性
- 上限限制 :系统对每个进程的文件描述符数量有上限(可通过
ulimit -n查看 / 修改); - 最小未使用原则:新创建的文件描述符总是选择当前最小的、未被使用的非负整数;
- 默认打开:进程启动时默认打开 3 个文件描述符:0(标准输入)、1(标准输出)、2(标准错误)。
总结
本文系统讲解了 Linux 文件 IO 的核心知识点,关键总结如下:
- 文件 IO 与标准 IO 的核心差异:文件 IO 是无缓存的系统调用,适合设备 / 通信文件;标准 IO 是有缓存的库函数,适合普通文件;
- 文件 IO 核心流程 :
open(打开 / 创建文件)→read/write(读写数据)→lseek(调整偏移)→close(关闭文件); - 实战关键:二进制文件拷贝需使用固定缓冲区循环读写,且要严格处理系统调用的返回值,避免数据丢失或文件描述符泄漏。
掌握文件 IO 是 Linux 系统编程的基础,后续无论是网络编程、设备驱动开发还是进程间通信,都离不开这些核心接口的应用。建议结合本文的示例代码多做实战,加深对接口的理解和使用熟练度。
