022 基础 IO —— 文件

022 基础 IO —— 文件
小米里的大麦基础 IO
—— C
语言文件 I/O
操作基础
前言
1. 文件的基本概念
文件的定义:
- 文件 = 内容(数据) + 属性(如权限、修改时间、所有者等)。
- 属性是文件管理的重要依据,贯穿文件的存储、访问和控制全流程。
2. 文件的两种状态
打开的文件
- 触发条件:由进程主动打开(如读写操作)。
- 存储位置:加载到内存中。
- 核心机制:
- 进程关联:每个打开的文件需与进程绑定,形成“进程-文件”的 多对一关系(1: n 关系:一个进程可打开多个文件)。
- 内核管理:操作系统为每个打开的文件创建 文件打开对象(如
struct XXX
),记录文件属性、状态及链表指针(如next
),便于统一管理。未打开的文件
- 存储位置:磁盘上。
- 核心问题:如何高效组织海量未打开文件,支持 快速增删查改。
- 管理目标:通过目录结构、文件系统层级等实现文件分类与定位。
3. 操作系统如何管理打开的文件
管理原则 —— 先描述,后组织:
- 描述:为每个打开的文件创建内核对象(如
struct file
),记录文件属性(如读写位置、权限)和操作接口。- 组织:通过链表、哈希表等数据结构管理所有打开的文件对象,实现高效访问。
关键数据结构示例
1
2
3
4
5
6
7 struct file
{
mode_t permissions; // 文件权限
off_t read_offset; // 当前读位置
struct inode *inode; // 指向磁盘文件的元数据(如 inode)
struct file *next; // 链表指针,用于组织多个打开的文件
};核心目标
- 高效管理大量打开的文件。
- 确保进程间文件操作的隔离性与安全性(如通过文件描述符隔离)。
1. C 语言文件操作函数汇总
1. 文件打开与关闭
函数 | 参数与模式 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fopen |
(const char *filename, const char *mode) 模式: "r" (读)、"w" (写覆盖)、"a" (追加)、"rb" (二进制读)等 |
FILE* (成功)NULL (失败) |
打开文件并返回文件指针 | FILE *fp = fopen("test.txt", "r"); |
fclose |
(FILE *stream) |
0 (成功)EOF (失败) |
关闭文件流并释放资源 | fclose(fp); |
freopen |
(const char *filename, const char *mode, FILE *stream) |
FILE* (新流)NULL (失败) |
重定向已打开的流到新文件 | freopen("log.txt", "a", stdout); |
2. 字符与字符串读写
函数 | 参数与说明 | 返回值 | 功能描述 | 注意事项 |
---|---|---|---|---|
fputc |
(int char, FILE *stream) |
写入的字符(成功)EOF (失败) |
向文件写入一个字符 | 适用于文本文件 |
fgetc |
(FILE *stream) |
读取的字符(成功)EOF (失败或结尾) |
从文件读取一个字符 | 需用 feof 检测结尾 |
fputs |
(const char *str, FILE *stream) |
非负值(成功)EOF (失败) |
向文件写入字符串(不自动加 \n ) |
确保字符串以 \0 结尾 |
fgets |
(char *str, int n, FILE *stream) |
str (成功)NULL (失败或结尾) |
从文件读取一行字符串(最多 n-1 字符) |
保留换行符,末尾补 \0 |
3. 格式化读写
函数 | 参数与格式说明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fprintf |
(FILE *stream, const char *format, ...) |
写入的字符数(成功) 负值(失败) |
按格式向文件写入数据 | fprintf(fp, "Value: %d", 42); |
fscanf |
(FILE *stream, const char *format, ...) |
成功匹配的参数数量(成功)EOF (失败) |
按格式从文件读取数据 | fscanf(fp, "%d", &num); |
4. 二进制文件读写
函数 | 参数与说明 | 返回值 | 功能描述 | 注意事项 |
---|---|---|---|---|
fwrite |
(const void *ptr, size_t size, size_t count, FILE *stream) |
成功写入的项数 | 向二进制文件写入数据块 | 参数顺序:数据指针、项大小、项数量 |
fread |
(void *ptr, size_t size, size_t count, FILE *stream) |
成功读取的项数 | 从二进制文件读取数据块 | 需检查返回值以确认实际读取量 |
5. 文件定位与状态
函数 | 参数与说明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fseek |
(FILE *stream, long offset, int origin) origin :SEEK_SET (文件头)、SEEK_CUR (当前位置)、SEEK_END (文件尾) |
0 (成功)非零(失败) |
移动文件指针到指定位置 | fseek(fp, 10, SEEK_SET); |
ftell |
(FILE *stream) |
当前偏移量(成功)-1L (失败) |
获取文件指针当前位置 | long pos = ftell(fp); |
rewind |
(FILE *stream) |
无 | 重置文件指针到文件开头 | rewind(fp); |
feof |
(FILE *stream) |
非零值(到结尾)0 (未到结尾) |
检测文件指针是否到达结尾 | if (feof(fp)) { ... } |
ferror |
(FILE *stream) |
非零值(有错误)0 (无错误) |
检测文件操作是否出错 | if (ferror(fp)) { ... } |
6. 其他辅助函数
函数 | 参数与说明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fflush |
(FILE *stream) |
0 (成功)EOF (失败) |
强制将缓冲区数据写入文件 | fflush(fp); |
remove |
(const char *filename) |
0 (成功)非零(失败) |
删除指定文件 | remove("temp.txt"); |
rename |
(const char *oldname, const char *newname) |
0 (成功)非零(失败) |
重命名或移动文件 | rename("old.txt", "new.txt"); |
2. 什么是当前路径?
1 |
|
由上我们知道,当 fopen
以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?答案是 进程的当前路径——cwd
。验证:
1 |
|
3. w
总是先清空,再写入
fopen
的 w
模式,当以 w 模式打开文件时:
- 如果文件不存在,则创建新文件。
- 如果文件已存在,则清空原有内容并重新开始写入。
1 |
|
好像没什么问题,w
会先清空文件,再将新文件内容进行写入,但是当我们打开这个 temp.txt
文件就会发现一点问题:
注意:w
即使不写入数据,只打开文件也会清空文件数据! 因为 w
是先清空再写入。
echo
与 fopen
的关系
- 底层实现:
echo
是 shell 命令,但其重定向功能依赖于操作系统提供的文件 I/O 机制。 - 类比
fopen("w")
:当执行echo ... > file
时,系统调用类似fopen(file, "w")
的操作,确保输出内容替换原有数据。
虽然 echo
本身不直接调用 fopen
,但其重定向功能在底层实现了与 fopen("w")
相同的效果:覆盖原有文件内容。因此,可以认为 echo
的重定向机制在功能上模拟了 fopen
的 w
模式。
1 | echo "现在有文件数据哦!" > temp.txt # 创建初始文件 |
4. a
是追加写
特性 | 说明 |
---|---|
追加模式 | 写入内容总在文件末尾,不覆盖原有数据。 |
自动创建文件 | 若文件不存在,a 模式会创建新文件。 |
文本模式 vs 二进制 | 默认为文本模式("a" ),若需二进制追加,使用 "ab" 。 |
缓冲区依赖 | 写入内容需等待缓冲区满或显式刷新(fflush )后才写入磁盘。 |
1. 基本追加写入
1 |
|
特性说明
- 追加行为:每次写入均追加到文件末尾,不覆盖原有内容。
- 自动创建:若文件不存在,
a
模式会自动创建新文件。
2. 文件不存在时的自动创建
1 |
|
特性说明
- 权限问题:若当前目录无写权限,
a
模式会失败(需处理fopen
返回的NULL
)。
3. 未关闭文件导致的数据丢失
1 |
|
坑点说明
- 未关闭文件:若程序异常终止或忘记调用
fclose
,缓冲区中的数据可能未写入磁盘。 - 解决方案:始终确保写入后调用
fclose
,或显式调用fflush(file)
强制刷新缓冲区。
4. 并发写入的潜在问题
1 |
|
坑点说明
- 并发写入:若多个进程/线程同时追加文件,内容可能交错(如
"来自 file2 的消息\n"
)。 - 解决方案:在多线程/多进程场景中,需通过锁机制或原子操作保证顺序。
5. open()
函数
题外话:比特位方式的标志位传递。其核心原理是通过 二进制位的每一位 来表示不同的标志状态,利用位运算符(如 |
和 &
)高效地组合和检测多个标志(了解,以后详解)。
1 |
|
open()
是 Linux 系统调用中最核心、最常用的函数之一,是一切文件/设备操作的起点。
1. 头文件
1 |
2. 函数原型 & 参数解释
1 | int open(const char *pathname, int flags); |
3. 参数说明
参数 | 说明 |
---|---|
pathname |
文件路径(绝对或相对路径) |
flags |
文件打开方式和行为控制(重点!) |
mode |
权限位(只有当创建新文件时才用!) |
4. 返回值
返回值 | 含义 |
---|---|
>= 0 |
打开成功,返回的是 文件描述符(整数) |
< 0 |
打开失败,返回 -1 ,具体错误原因通过 errno 查看 |
1 | if (fd < 0) |
1. flags
参数详解(支持“位或 |”组合使用)
1. 访问方式(必须选一个)
常量 | 说明 |
---|---|
O_RDONLY |
只读打开(Read Only) |
O_WRONLY |
只写打开(Write Only) |
O_RDWR |
读写都打开(Read + Write) |
2. 控制行为(可选,多个之间用 |
连接)
常量 | 含义 |
---|---|
O_CREAT |
文件不存在就创建(需要配合 mode 参数) |
O_EXCL |
和 O_CREAT 同用,文件存在则失败(避免重复创建) |
O_TRUNC |
打开文件时清空原内容(通常配合写) |
O_APPEND |
写入内容追加到文件末尾 |
O_NONBLOCK |
非阻塞模式打开文件(常用于设备/管道) |
O_CLOEXEC |
在 exec 调用时关闭该文件描述符 |
O_SYNC |
写入时直接同步到硬盘(安全但慢) |
2. mode
参数(仅在 O_CREAT
创建新文件时才用)
1 | open("file.txt", O_WRONLY | O_CREAT, 0664); |
mode_t
用来设置 新文件的权限,与chmod
类似- 常用组合:
权限数字 | 含义说明 |
---|---|
0664 |
用户读写,组读写,其他只读 |
0644 |
用户读写,其他只读 |
代码示例解读:
1 | // 代码 1: |
代码编号 | 是否自动创建文件 | 权限设定 | 是否受 umask 影响 | 常见用途 |
---|---|---|---|---|
1️⃣ | ❌ 否 | 无 | - | 仅打开已存在文件 |
2️⃣ | ⚠️ 可能报错 | ❌ 缺少权限参数(可能是乱码) | - | 不推荐用法 |
3️⃣ | ✅ 创建 | 0666 - umask(与 0666 不符,原因 umask ) |
✅ 是 | 正常用法 |
4️⃣ | ✅ 创建 | 0666 | ❌ 不受影响 | 特殊场合 |
3. 战代码示例:打开 + 写入 + 关闭
1 |
|
6. 文件描述符
先出结论:open()
的返回值 就是文件描述符(fd),它是一个 数组下标,指向当前进程的 打开文件表(fd table) 中的一个 struct file *
指针。
1 | int fd = open("a.txt", O_WRONLY); // 返回 3 |
我们通过一段完整的 C 代码 演示:“访问文件的本质,其实是 数组下标访问”。接着再来讲解内核中是如何通过 struct file
、struct files_struct
等结构体来描述和管理已打开的文件。
1. C 代码示例:文件描述符本质是数组下标
1 |
|
输出示例(实际运行):
1 | fd1: 3 |
解释说明:
为什么编号是从 3
开始的?0
,1
,2
去哪了?
答案:Linux
进程默认情况下会有 3
个缺省打开的文件描述符,分别是标准输入 0
,标准输出 1
,标准错误 2
。0,1,2 对应的物理设备一般是:键盘,显示器,显示器。
- 标准输入(stdin):文件描述符是 0
- 标准输出(stdout):是 1
- 标准错误(stderr):是 2
后续打开的文件就是从 下标 3 开始往上分配。所以:fd
本质上是一个 打开文件表的下标(int 类型的索引)
2. 内核层的结构体解析(推荐 linux-2.6.11.1.tar.gz —— 09-Mar-2005 00:59 44M 这个版本查看源码)
用户层调用 open()
后,内核做了什么?
Linux 内核有三层数据结构来描述一个打开的文件:
1. files_struct
(表示进程级的打开文件表)
1 | struct files_struct |
- 每个进程都有一个
files_struct
实例。 fd_array[i]
中存的是指向struct file
的指针。- 这个数组的下标就是我们用户层看到的
fd
!
2. struct file
(表示一个已打开的文件实例)
1 | struct file |
- 表示一次文件打开操作。
- 不同进程打开同一个文件,会有 不同的
struct file
。 struct file
直接或间接包含的属性:在磁盘的什么位置、基本属性(权限、大小、读写位置、谁打开的……)、文件的内核缓冲区信息、struct file *next
指针、引用计数count
等。- 类似于“文件打开上下文”,记录偏移、标志等状态。
3. inode
(文件元数据结构)
1 | struct inode |
- 一个
inode
代表文件系统中一个“实际的文件”。 - 所有打开该文件的
file
都指向同一个inode
。
3. 三者之间的联系总结
小结:用户空间的文件描述符(fd)就是内核中的 struct file *
数组的下标!通过这个下标,进程就能访问并操作该文件在内核中对应的资源。
4. 文件描述符的分配规则
先出结论:Linux 内核始终分配当前未被使用的最小下标,作为新的文件描述符。当你调用 open()
或 dup()
等函数时,内核会在 fd_array[]
中从头开始查找:寻找当前未被使用的最小下标,作为新的文件描述符(fd)。
关闭 fd 后: 它会被回收,下一次打开文件就可能重复使用这个编号。
1 |
|
运行与验证:
编译并运行程序,将输出重定向到文件:
1
gcc test.c -o test && ./test > output.txt 2>&1
查看
output.txt
:1
2
3fd1=0
fd2=1
fd3=2
说明:关闭 0/1/2
后,新打开的文件的描述符依次复用这些最小下标。
1 |
|
输出内容会写入 stderr
(fd = 2),因为 stdout
被关闭。示例输出:
1 | fd1 = 1 ← 因为 fd=1 被关闭,最小空位就是 1 |
注意: 当你只关闭了 fd=1
,而 0
和 2
保持开启,新文件会被分配到 1。不会跳到 2,因为 2 是正在使用中的文件描述符(stderr)。所以:
- 内核分配新 fd 的顺序是:从低到高,找第一个没用的。
- 如果你关闭了某个 fd,那么它会被下一次
open()
回收复用。 - 只有当 0、1、2 都被关闭,才会让新文件从 0 开始重新分配。
7. 文件描述符 VS FILE *
一句话总结:文件描述符(fd)
是 Linux 系统内核的低层 I/O 机制,而 FILE \*
是 C 标准库(stdio.h)封装的高级 I/O 结构,它内部依赖文件描述符实现功能。
1. 文件描述符(int fd
)
- 是 Linux 内核分配的一个 整数索引
- 用于标识当前进程打开的某个文件(实际上是指向内核
struct file
的下标) - 使用
open()
,read()
,write()
,close()
等系统调用操作 - 属于 低级 I/O
示例:
1 | int fd = open("a.txt", O_RDWR); // 打开文件 "a.txt",以读写模式(O_RDWR)打开,返回文件描述符 fd |
2. FILE *
指针
- 是
stdio.h
定义的 高级抽象结构体 - 内部其实就是封装了一个
int fd
+ 缓冲区 + 文件状态等信息 - 使用
fopen()
,fread()
,fwrite()
,fprintf()
,fclose()
等函数操作 - 属于 高级 I/O
示例:
1 | FILE* fp = fopen("a.txt", "w"); // 以写模式("w")打开文件 "a.txt",返回文件指针 fp,注意:以 "w" 模式打开会清空文件原有内容 |
3. 二者之间的关系图
1 | FILE *fp ───► struct __FILE (库层) ───► int fd ───► 内核打开文件表(files_struct) |
4. 相互转换方法
从 FILE *
获取 fd
:
1 | int fd = fileno(fp); |
从 fd
获取 FILE *
:
1 | FILE *fp = fdopen(fd, "w"); |
5. 区别对比总结
特性 | 文件描述符(int fd) | FILE * 指针 |
---|---|---|
所属层次 | 内核层(系统调用) | C 标准库(用户空间) |
使用头文件 | <fcntl.h> , <unistd.h> |
<stdio.h> |
是否有缓冲机制 | ❌ 无缓冲 | ✅ 有缓冲 |
速度 | 快(系统级) | 慢(用户态带缓冲) |
函数接口 | open/read/write | fopen/fread/fwrite |
可否转换 | ✅ 可以互相转换 | ✅ 可以互相转换 |
控制精细程度 | 更细粒度(如非阻塞、异步) | 比较抽象、功能丰富 |
实战建议:
场景 | 建议使用 |
---|---|
写系统调用、驱动、IO 重定向等底层功能 | fd (文件描述符) |
做格式化文本输出、文件读写、缓存优化等 | FILE * (标准库) |