
🎉博主首页: 有趣的中国人
🎉专栏首页: 操作系统原理

本文是 Linux 文件系统 + 文件描述符 fd 的一次"底层视角"梳理
从
open / read / write一路拆到fd → struct file → inode → data block
文章目录
-
- [一、两种世界:用户态 vs 内核态](#一、两种世界:用户态 vs 内核态)
-
- [1️⃣ 用户态看到的世界](#1️⃣ 用户态看到的世界)
- [2️⃣ 内核态真正发生的事](#2️⃣ 内核态真正发生的事)
- [二、文件描述符 fd 是怎么来的?](#二、文件描述符 fd 是怎么来的?)
-
- [1️⃣ 每个进程都有一张 fd 表](#1️⃣ 每个进程都有一张 fd 表)
- [2️⃣ struct file 描述的是"一次打开"](#2️⃣ struct file 描述的是“一次打开”)
- [三、open 系统调用发生了什么?](#三、open 系统调用发生了什么?)
-
- [1️⃣ 创建 struct file](#1️⃣ 创建 struct file)
- [2️⃣ 分配 fd](#2️⃣ 分配 fd)
- [3️⃣ 返回 fd](#3️⃣ 返回 fd)
- [四、为什么 fd 一定是"最小可用值"?](#四、为什么 fd 一定是“最小可用值”?)
- [五、0 / 1 / 2 是怎么来的?](#五、0 / 1 / 2 是怎么来的?)
-
- [shell 重定向的本质](#shell 重定向的本质)
- [六、C 语言缓冲区存在的原因](#六、C 语言缓冲区存在的原因)
-
- [为什么 `printf` 不等于 `write`?](#为什么
printf不等于write?)
- [为什么 `printf` 不等于 `write`?](#为什么
- 七、文件系统的真实结构(磁盘视角)
-
- [一个分区的布局(ext 系)](#一个分区的布局(ext 系))
- [inode 里有什么?](#inode 里有什么?)
- 目录的本质
- 八、路径解析全过程(重点)
- [九、创建 / 删除文件的本质](#九、创建 / 删除文件的本质)
-
- [创建文件(creat / open O_CREAT)](#创建文件(creat / open O_CREAT))
- 删除文件(unlink)
- 十、总结
一、两种世界:用户态 vs 内核态
1️⃣ 用户态看到的世界
在用户看来,文件操作非常简单:
c
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
write(fd, "hello\n", 6);
close(fd);
我们只关心三件事:
open返回一个 整数 fdread / write对 fd 操作close释放
但 fd 并不是真正的文件,它只是一个"索引"。
2️⃣ 内核态真正发生的事
内核内部,真正的链路是:
text
fd
↓
当前进程 task_struct
↓
files_struct
↓
fd_array[fd]
↓
struct file
↓
file_operations -> read / write
↓
VFS
↓
具体文件系统 / 设备驱动
↓
磁盘 / 终端 / socket
一句话总结:
fd 是进程私有的下标,struct file 才是内核中的"打开文件对象"
二、文件描述符 fd 是怎么来的?
1️⃣ 每个进程都有一张 fd 表
在 task_struct 中:
c
struct files_struct {
struct file *fd_array[NR_OPEN_DEFAULT];
};
fd_array[0]→ stdinfd_array[1]→ stdoutfd_array[2]→ stderr
👉 fd 本质就是 fd_array 的下标
2️⃣ struct file 描述的是"一次打开"
c
struct file {
struct file_operations *f_op;
loff_t f_pos; // 当前偏移
int f_flags; // 打开方式
void *private_data;
};
⚠️ 重要区分:
| 对象 | 含义 |
|---|---|
| inode | 文件本身(属性、大小、块号) |
| struct file | 某个进程打开它的一次实例 |
👉 同一个 inode,可以对应多个 struct file
三、open 系统调用发生了什么?
内核大致做三件事:
1️⃣ 创建 struct file
- 解析路径
- 找到 inode
- 填写
f_flags / f_pos - 设置
file_operations
2️⃣ 分配 fd
- 在当前进程的
fd_array中找最小空槽 - 例如 3
fd_array[3] = struct file*
3️⃣ 返回 fd
text
用户拿到的 int fd
其实就是:
fd_array 的下标
四、为什么 fd 一定是"最小可用值"?
因为:
- fd_array 是一个数组
- 内核从下标 0 开始扫描
- 找到第一个空位就用
所以:
c
close(1);
int fd = open("a.txt", O_WRONLY);
此时 fd == 1
五、0 / 1 / 2 是怎么来的?
在进程创建时(fork + exec):
- 内核提前帮你打开好
- 并放进 fd_array
| fd | 含义 |
|---|---|
| 0 | stdin |
| 1 | stdout |
| 2 | stderr |
shell 重定向的本质
bash
./a.out > out.log 2>&1
shell 在 exec 之前做的是:
text
close(1)
open(out.log) -> fd = 1
dup2(1, 2)
👉 并没有什么"神奇输出"
👉 只是 fd 指向被换了
六、C 语言缓冲区存在的原因
为什么 printf 不等于 write?
因为:
printf属于 C 标准库- 内部维护 用户态缓冲区
- 减少系统调用次数
三种缓冲策略:
| 类型 | 场景 |
|---|---|
| 全缓冲 | 普通文件 |
| 行缓冲 | 终端 |
| 无缓冲 | stderr |
触发刷新的时机:
- 缓冲区满
- 碰到
\n - 手动
fflush - 进程退出
七、文件系统的真实结构(磁盘视角)
一个分区的布局(ext 系)
text
| super block |
| GDT |
| block bitmap |
| inode bitmap |
| inode table |
| data blocks |
每一块通常是 4KB
inode 里有什么?
- 文件类型
- 权限
- 大小
- 时间戳
- 数据块指针
⚠️ inode 不存文件名
目录的本质
目录 = 特殊文件
目录 data block 中存的是:
text
文件名 → inode 编号
八、路径解析全过程(重点)
访问 /a/b/c.txt:
- 从根目录 inode 开始
- 在目录 data block 中找
a - 拿到
a的 inode - 再找
b - 最终找到
c.txt的 inode - 根据 inode 找 data block
👉 路径解析是逐级 inode + data block 完成的
九、创建 / 删除文件的本质
创建文件(creat / open O_CREAT)
-
分配 inode(inode bitmap)
-
分配 data block(block bitmap)
-
在父目录 data block 中写入:
text文件名 → inode
删除文件(unlink)
- 清 inode bitmap
- 清 block bitmap
- 从目录中删除映射关系
⚠️ 如果还有进程持有 struct file:
真正释放会延迟到最后一个引用消失
十、总结
- fd 是进程私有索引
- struct file 描述一次打开
- inode 描述文件本体
- data block 存内容
- 目录存"名字 → inode"
- VFS 用
file_operations实现多态 - 重定向 = 换 fd 指向