一、文件 IO 核心概念:系统调用 vs 标准 IO
在 Linux 中操作文件有两种核心方式:文件 IO(系统调用) 和标准 IO(C 库函数),二者底层关联但适用场景不同。
1.1 核心定义
| 类型 | 本质 | 核心特征 | 适用场景 |
|---|---|---|---|
| 文件 IO | 操作系统对外提供的系统调用函数 | 无缓冲区、文件描述符(int)、功能强大 | 设备文件(如/dev下设备)、实时性要求高的场景 |
| 标准 IO | C 标准库封装的文件操作函数 | 带缓冲区、文件流指针(FILE*)、跨平台 | 普通文件(文本 / 二进制)、通用文件操作 |
1.2 核心关系
- 底层依赖:标准 IO(C 库)是对文件 IO(系统调用)的封装,跨平台特性由 C 库保证,而系统调用是 Linux 内核直接提供的功能;
- 缓存差异:标准 IO 的缓冲区减少了系统调用次数(提升普通文件操作效率),文件 IO 无缓存,直接操作内核资源(适合设备文件);
- 操作对象:文件 IO 用 "文件描述符(fd)" 标识文件,标准 IO 用 "文件流指针(FILE*)" 标识。
1.3 共同点与区别
| 维度 | 共同点 | 区别 |
|---|---|---|
| 核心目标 | 均用于文件读写操作 | 文件 IO:无缓存、fd、系统调用;标准 IO:有缓存、FILE*、C 库封装 |
| 跨平台性 | - | 文件 IO:依赖具体系统(如 Linux);标准 IO:跨平台(Windows/Linux 通用) |
| 效率 | - | 普通文件:标准 IO 更高(缓存);设备文件:文件 IO 更优(实时) |
二、文件 IO 核心函数:open/read/write/close
文件 IO 的操作流程遵循 "打开→读写→关闭" 的固定范式,核心依赖open/read/write/close四个系统调用。
2.1 打开文件:open
函数原型
c
运行
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
核心参数
| 参数 | 说明 | 常用取值 |
|---|---|---|
| pathname | 文件路径(绝对 / 相对路径) | "1.txt"、"/etc/passwd" |
| flags | 打开模式(必选 + 可选组合) | 必选:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)可选:O_CREAT(创建文件)、O_TRUNC(清空文件)、O_APPEND(追加写) |
| mode | 文件权限(仅O_CREAT时有效) |
0666(读写权限)、0777(读写执行权限) |
返回值
- 成功:返回非负整数(文件描述符,fd,唯一标识打开的文件);
- 失败:返回 - 1(可通过
perror查看错误原因)。
示例
c
运行
// 以只写+创建+清空模式打开1.txt,权限0666
int fd = open("1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open failed");
return 1;
}
2.2 读取文件:read
函数原型
c
运行
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
核心参数
| 参数 | 说明 |
|---|---|
| fd | 目标文件的文件描述符(open 返回值) |
| buf | 接收数据的内存缓冲区(需提前分配空间) |
| count | 本次读取的最大字节数(建议大于文件实际大小) |
返回值
-
0:实际读取到的字节数;
- ==0:文件读取到末尾;
- <0:读取失败(如 fd 无效、文件不存在)。
示例
c
运行
char buf[1024] = {0};
// 从fd读取最多1024字节到buf
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 0) {
perror("read failed");
close(fd);
return 1;
}
printf("读取到%d字节:%s\n", n, buf);
2.3 写入文件:write
函数原型
c
运行
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
核心参数
| 参数 | 说明 |
|---|---|
| fd | 目标文件的文件描述符 |
| buf | 待写入文件的数据缓冲区 |
| count | 待写入数据的有效长度(如strlen(buf)) |
返回值
-
0:实际写入的字节数;
- ==0:未写入任何字节(普通文件不会出现,设备文件可能);
- <0:写入失败。
示例
c
运行
char *data = "Hello Linux File IO!";
// 写入data到fd,长度为字符串有效长度
ssize_t n = write(fd, data, strlen(data));
if (n < 0) {
perror("write failed");
close(fd);
return 1;
}
printf("写入%d字节\n", n);
2.4 关闭文件:close
函数原型
c
运行
#include <unistd.h>
int close(int fd);
核心说明
- 功能:释放文件描述符对应的内核资源(必须调用,否则会导致资源泄漏);
- 返回值:成功返回 0,失败返回 - 1。
示例
c
运行
if (close(fd) == -1) {
perror("close failed");
return 1;
}
三、目录操作:opendir/readdir/closedir
除了普通文件,Linux 中目录也是一种特殊文件,需通过专门的函数操作,流程为 "打开目录→读取目录→关闭目录"。
3.1 打开目录:opendir
函数原型
c
运行
#include <dirent.h>
DIR *opendir(const char *name);
- 功能:打开指定目录,返回目录流指针;
- 参数:
name为目录路径(如"."、"/home"); - 返回值:成功返回
DIR*(目录流指针),失败返回 NULL。
3.2 读取目录:readdir
函数原型
c
运行
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 功能:从目录流中读取一个文件 / 子目录的信息;
- 参数:
dirp为opendir返回的目录流指针; - 返回值:
- 成功:返回
struct dirent*(包含文件名、文件类型等信息); - 失败 / 目录末尾:返回 NULL。
- 成功:返回
核心结构体(struct dirent)
c
运行
struct dirent {
ino_t d_ino; // 索引节点号
char d_name[256]; // 文件名(核心字段)
// 其他字段:文件类型、偏移量等
};
3.3 关闭目录:closedir
函数原型
c
运行
#include <dirent.h>
int closedir(DIR *dirp);
- 功能:关闭目录流,释放资源;
- 返回值:成功返回 0,失败返回 - 1。
3.4 目录操作示例
c
运行
#include <stdio.h>
#include <dirent.h>
int main() {
// 打开当前目录
DIR *dir = opendir(".");
if (dir == NULL) {
perror("opendir failed");
return 1;
}
// 遍历目录中的文件
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("文件名:%s\n", entry->d_name);
}
// 关闭目录
closedir(dir);
return 0;
}
四、Makefile:Linux 工程编译的 "一键工具"
当工程包含多个源文件时,手动执行gcc编译效率极低,Makefile 可实现 "一键编译",自动管理编译依赖和规则。
4.1 Makefile 核心概念
- 命令 :执行
make命令时,系统会查找当前目录的makefile/Makefile文件并执行; - 核心规则 :
目标:依赖\n\t规则(TAB 缩进是关键,不能用空格);- 目标:要生成的文件(如
a.out)或操作(如clean); - 依赖:生成目标所需的文件(如
main.c、func.c); - 规则:生成目标的编译命令(如
gcc main.c func.c -o a.out)。
- 目标:要生成的文件(如
4.2 Makefile 编写进阶(从基础到通用)
版本 1:基础规则(固定依赖)
makefile
# 目标:a.out;依赖:main.c、func.c
a.out:main.c func.c
gcc main.c func.c -o a.out
# 无依赖的目标(清理编译产物)
clean:
rm -f a.out
- 执行
make:编译生成a.out(仅当依赖文件修改时重新编译); - 执行
make clean:删除a.out; - 缺点:依赖文件 / 编译命令固定,修改时需全量改动。
版本 2:内置变量(简化规则)
Makefile 提供内置变量简化编写,核心内置变量:
$^:当前规则的所有依赖文件;$@:当前规则的目标文件。
makefile
a.out:main.c func.c
gcc $^ -o $@ # 等价于 gcc main.c func.c -o a.out
clean:
rm -f $@ # 等价于 rm -f a.out
版本 3:自定义变量(通用模板)
通过自定义变量适配不同工程,可灵活修改源文件、目标名、编译选项:
makefile
# 自定义变量:源文件、目标名、编译选项
SRC = main.c
SRC += func.c # 追加源文件
APP = a.out
FLAG = -g # 编译调试信息
# 规则:依赖为SRC,目标为APP
$(APP):$(SRC)
gcc $^ -o $@ $(FLAG) # 加入编译选项
# 清理规则
clean:
rm -f $(APP)
4.3 Makefile 核心特性
- 增量编译 :仅当依赖文件(如
main.c)修改时,才重新编译生成目标(a.out),未修改则提示is up to date; - 多目标支持 :可定义多个目标(如
all、clean、install),执行make 目标名即可触发对应规则; - 工程管理:支持跨目录源文件编译、自定义编译脚本,是大型 Linux 工程的标配。
五、核心实战:文件 IO + Makefile 完整示例
5.1 源文件(main.c)
c
运行
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 1. 打开文件
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open failed");
return 1;
}
// 2. 写入文件
char *data = "Hello File IO!\n";
write(fd, data, strlen(data));
// 3. 关闭文件
close(fd);
// 重新打开读取
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
char buf[1024] = {0};
read(fd, buf, sizeof(buf));
printf("读取内容:%s", buf);
close(fd);
return 0;
}
5.2 Makefile
makefile
SRC = main.c
APP = file_io_demo
FLAG = -g
$(APP):$(SRC)
gcc $^ -o $@ $(FLAG)
clean:
rm -f $(APP) test.txt
5.3 编译 & 运行
bash
运行
# 编译
make
# 运行
./file_io_demo
# 清理
make clean
六、核心总结
- 文件 IO:基于系统调用,无缓存、用文件描述符,适合设备文件 / 实时场景;标准 IO 基于 C 库封装,有缓存、跨平台,适合普通文件;
- 文件操作流程:打开(open)→ 读写(read/write)→ 关闭(close),目录操作需用 opendir/readdir/closedir;
- Makefile:核心规则为 "目标:依赖 + 规则",通过内置 / 自定义变量实现通用编译模板,支持增量编译,是 Linux 工程编译的核心工具;
- 实战中需结合场景选择文件操作方式,通过 Makefile 简化编译流程,提升开发效率。