一、文件 IO 的核心分类:系统调用与标准 IO
Linux 中的文件 IO 主要分为两类:系统调用级文件 IO 和 C 库封装的标准 IO,二者底层逻辑和适用场景差异显著,是掌握文件 IO 的首要前提。
1. 系统调用级文件 IO
系统调用是操作系统内核直接暴露的接口(如open/read/write/close),属于内核态操作,具备以下特点:
- 操作对象为
int类型的文件描述符(File Descriptor),每个打开的文件对应唯一的非负整数(0、1、2 默认分配给标准输入、标准输出、标准错误); - 无用户态缓存,直接与内核交互,适合串口、磁盘设备等对实时性要求高的场景;
- 函数原型由内核定义,调用时直接触发系统态切换。
2. C 库标准 IO
C 标准库(如stdio.h)对系统调用进行了封装,提供fopen/fread/fwrite/fclose等接口,属于用户态操作,特点如下:
- 操作对象为
FILE*类型的文件流指针,内部封装了文件描述符和缓存区; - 自带用户态缓存,减少系统调用次数,提升普通文件(如 txt、log)的读写效率;
- 跨平台性更好,符合 ANSI C 标准,无需关注不同系统的内核调用差异。
核心对比表
| 特性 | 系统调用级文件 IO | C 库标准 IO |
|---|---|---|
| 操作对象 | 文件描述符(int) | 文件流指针(FILE*) |
| 缓存机制 | 无用户态缓存 | 有用户态缓存 |
| 函数示例 | open/read/write/close | fopen/fread/fwrite/fclose |
| 依赖层级 | 直接依赖内核 | 依赖 C 库,底层调用系统调用 |
| 适用场景 | 设备文件、实时操作 | 普通文件、批量读写 |
二、系统调用级文件 IO:核心函数与示例
系统调用是文件 IO 的底层核心,其中open函数是操作的起点,掌握其用法是理解文件 IO 的关键。
1. open 函数:打开 / 创建文件
函数原型
int open(const char *pathname, int flags, mode_t mode);
参数说明
pathname:文件路径(绝对路径或相对路径,如./test.txt、/home/user/data.log);flags:打开文件的标志,核心取值如下(可通过|组合):O_RDONLY:只读打开;O_WRONLY:只写打开;O_RDWR:读写打开;O_CREAT:文件不存在时创建(必须配合mode参数);O_TRUNC:文件存在时清空内容;O_APPEND:以追加模式打开,写入内容追加到文件末尾;
mode:文件创建时的权限(仅O_CREAT生效),取值为八进制数(如0644表示用户可读可写,组和其他只读)。
返回值
- 成功:返回非负整数(文件描述符);
- 失败:返回 - 1,并设置
errno(可通过perror打印错误信息)。
2. read/write/close:读写与关闭文件
read 函数(读取文件)
ssize_t read(int fd, void *buf, size_t count);
fd:open 返回的文件描述符;buf:存储读取数据的缓冲区;count:期望读取的字节数;- 返回值:成功返回实际读取的字节数(0 表示文件末尾),失败返回 - 1。
write 函数(写入文件)
ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符;buf:待写入数据的缓冲区;count:期望写入的字节数;- 返回值:成功返回实际写入的字节数,失败返回 - 1。
close 函数(关闭文件)
int close(int fd);
fd:文件描述符;- 返回值:成功返回 0,失败返回 - 1;
- 注意:打开的文件必须关闭,否则会导致文件描述符泄露,耗尽系统资源。
3. 完整示例:基于 open 的文件读写
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
int main() {
// 1. 打开/创建文件:只写模式,不存在则创建,存在则清空,权限0644
int fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed"); // 打印错误信息(如文件权限不足、路径不存在)
return -1;
}
printf("文件打开成功,文件描述符:%d\n", fd);
// 2. 写入数据
const char *write_buf = "Hello Linux File IO!\n";
ssize_t write_len = write(fd, write_buf, strlen(write_buf));
if (write_len == -1) {
perror("write failed");
close(fd); // 失败时也要关闭文件描述符
return -1;
}
printf("成功写入 %zd 字节数据\n", write_len);
// 3. 关闭文件
if (close(fd) == -1) {
perror("close failed");
return -1;
}
// 4. 重新以只读模式打开文件,读取内容
fd = open("./test.txt", O_RDONLY);
if (fd == -1) {
perror("open for read failed");
return -1;
}
char read_buf[1024] = {0};
ssize_t read_len = read(fd, read_buf, sizeof(read_buf) - 1);
if (read_len == -1) {
perror("read failed");
close(fd);
return -1;
}
printf("读取到的数据:%s", read_buf);
// 5. 关闭文件
close(fd);
return 0;
}
编译运行说明
使用gcc编译:
gcc file_io_demo.c -o file_io_demo
./file_io_demo
运行结果:
文件打开成功,文件描述符:3
成功写入 21 字节数据
读取到的数据:Hello Linux File IO!
三、目录 IO 操作:遍历目录内容
除了普通文件,Linux 中目录也是一种特殊文件,需通过专门的函数操作,核心函数为opendir/readdir/closedir。
1. 核心函数说明
opendir(打开目录)
DIR *opendir(const char *name);
- 参数:目录路径(如
./); - 返回值:成功返回
DIR*类型的目录流指针,失败返回NULL。
readdir(读取目录项)
struct dirent *readdir(DIR *dirp);
- 参数:
opendir返回的目录流指针; - 返回值:成功返回
struct dirent结构体(包含文件名d_name、文件类型d_type等),读取到末尾或失败返回NULL。
closedir(关闭目录)
int closedir(DIR *dirp);
- 参数:目录流指针;
- 返回值:成功返回 0,失败返回 - 1。
2. 示例:遍历指定目录下的所有文件
#include <stdio.h>
#include <dirent.h>
#include <errno.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("用法:%s <目录路径>\n", argv[0]);
return -1;
}
// 1. 打开目录
DIR *dir = opendir(argv[1]);
if (dir == NULL) {
perror("opendir failed");
return -1;
}
// 2. 遍历目录项
struct dirent *entry = NULL;
printf("目录 %s 下的文件列表:\n", argv[1]);
while ((entry = readdir(dir)) != NULL) {
// 跳过.和..目录
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("文件名:%s\n", entry->d_name);
}
// 3. 关闭目录
closedir(dir);
return 0;
}
编译运行
gcc dir_io_demo.c -o dir_io_demo
./dir_io_demo ./
运行结果(示例):
目录 ./ 下的文件列表:
file_io_demo
file_io_demo.c
dir_io_demo
dir_io_demo.c
test.txt
四、工程化管理:用 Makefile 简化编译
当文件 IO 代码分散在多个源文件时,手动编译效率低下,可通过 Makefile 实现一键编译、清理,以下是适配上述示例的 Makefile。
1. 多文件 Makefile 示例
假设项目包含main.c(主逻辑)、file_io.c(文件 IO 封装)、dir_io.c(目录 IO 封装),Makefile 编写如下:
makefile
# 定义变量:编译器、编译选项、源文件、目标程序
CC = gcc
CFLAGS = -g -Wall -O2 # -g调试信息,-Wall显示警告,-O2优化
SRC = main.c file_io.c dir_io.c
TARGET = io_project
# 默认目标:编译生成可执行文件
all: $(TARGET)
# 编译规则:目标程序依赖源文件
$(TARGET): $(SRC)
$(CC) $(CFLAGS) $^ -o $@ # $^表示所有依赖文件,$@表示目标文件
# 清理规则:删除可执行文件和临时文件
clean:
rm -rf $(TARGET) *.o
# 伪目标声明(避免与同名文件冲突)
.PHONY: all clean
2. Makefile 使用说明
- 编译项目:直接执行
make,自动编译所有源文件生成io_project; - 清理文件:执行
make clean,删除可执行文件和编译产生的.o文件; - 增量编译:修改部分源文件后,
make仅重新编译修改的文件,提升效率。
五、关键注意事项与避坑指南
- 文件描述符泄露 :所有
open打开的文件描述符、opendir打开的目录流,必须通过close/closedir关闭,长期运行的程序(如服务端)泄露会导致系统资源耗尽; - 权限问题 :
open的mode参数仅在创建文件时生效,且最终权限会受umask(默认权限掩码)影响(实际权限 = mode & ~umask); - 缓存刷新 :标准 IO 的缓存区需通过
fflush手动刷新,否则数据可能滞留缓存区未写入磁盘; - 错误处理 :所有 IO 函数的返回值必须检查,通过
perror或strerror(errno)定位错误原因。