文件管理
核心概念:FCB 与 inode
FCB(File Control Block) 是 OS 用来描述一个文件的控制块。Unix 里叫 inode,Windows 里嵌在目录项或 MFT 中。每个文件都有唯一的 FCB/inode。
磁盘上的 FCB/inode 至少包含:
| 字段 | 含义 |
|---|---|
| 文件类型与权限 | 普通文件/目录/设备,rwx |
| 文件大小 | 字节数 |
| 时间戳 | 创建/最后访问/最后修改时间 |
| 硬链接计数 | 指向本 inode 的文件名数量 |
| 数据块指针 | 文件的物理块号(直接块 / 一级间接 / 二级间接) |
关键:FCB/inode 里不存文件名——文件名存在目录里。
打开文件:两条路径
每级路径分量都先查 dentry 缓存,命中则跳过读磁盘目录,未命中则从磁盘加载目录内容并建 dentry。
open("/docs/note.txt") — 假设全部 dentry 未命中(最慢路径):
① "/"
dentry 缓存未命中 → 读 "/" 的目录磁盘块
→ 找到 ("docs", inode=512) → 建 dentry("/", docs) 写入缓存
② "docs"
dentry 缓存未命中 → 读 docs 的目录磁盘块(inode 512 先载入内存)
→ 找到 ("note.txt", inode=1024) → 建 dentry("docs", note.txt) 写入缓存
③ "note.txt"
dentry 缓存未命中 → inode 1024 从磁盘载入内存 inode 缓存
→ 是普通文件 → 建 dentry("note.txt", inode 1024)
④ 建立进程侧访问结构
系统打开文件表: 偏移=0, O_RDONLY → inode 1024
进程 fd 表: fd=3 → 系统表条目
返回 3
再次 open("/docs/note.txt") — dentry 全部命中(最快路径):
① "/" → dentry 命中 → 直接拿到 inode,跳过读目录磁盘
② "docs" → dentry 命中 → 直接拿到 inode 512
③ "note.txt"→ dentry 命中 → 直接拿到 inode 1024
④ 同上,返回 fd
| 结构 | 位置 | 存什么 | 何时创建 |
|---|---|---|---|
| 目录 | 磁盘数据区 | (文件名, inode号) 表 | 创建文件/目录时写入 |
| inode | 磁盘 inode 区 → 内存 inode 缓存 | 属性 + 数据块指针 | 文件创建时写磁盘,访问时载入内存 |
| dentry | 仅内存 | 文件名 + 内存 inode 指针 + 父子关系 | 路径解析时创建,持续缓存 |
| 打开文件表 | 仅内存 | 偏移量 + 打开模式 + 指向 inode | open 创建,close 销毁 |
| fd 表 | 仅内存(每进程一张) | fd → 打开文件表条目 | open 创建,close 销毁 |
文件共享
| 类型 | 原理 | 删除源文件后 |
|---|---|---|
| 硬链接 | 多个文件名指向同一个 inode,inode 里记录链接计数 | 文件内容仍在(计数 > 0 就还在) |
| 软链接 | 单独新建一个文件,内容是目标路径字符串 | 软链接失效(断链) |
一句话:硬链接连 inode,软链接连路径。
访问控制
基本模型:rwx × 所有者-组-其他人
实际检查顺序:进程访问文件时,OS 先拿进程的 uid/gid 和文件的 inode 权限位对比,通过才放行。
文件系统布局
磁盘上的区域
| 区域 | 内容 |
|---|---|
| 引导块 | 启动引导代码,通常放卷开头 |
| 超级块 | 文件系统全局信息:块大小、总块数、空闲块数、inode 总数 |
| inode 区 | 所有 inode(FCB),每个文件一个 |
| 数据区 | 文件内容 + 目录内容(目录的”数据”就是文件名→inode表) |
文件的物理分配方式
数据区的文件块如何分布:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 连续分配 | 文件占一串连续磁盘块 | 顺序读写快 | 外部碎片,文件拓展困难 |
| 链接分配 | 无外部碎片,一个文件可以拆开链接存储 | 开销大 | |
| └ 隐式链接 | 每块末尾存下一块指针,链在数据块里 | 无外部碎片,易拓展 | 随机访问差:找第 N 块得把前 N-1 块都读一遍 |
| └ 显式链接(FAT) | 链接指针集中存一张文件分配表(FAT),链在内存里 | 随机访问快(FAT 常驻内存,跟链条不读数据块) | FAT 表本身占内存 |
| 索引分配 | 一个索引块集中存所有数据块号 | 随机访问好 | 索引块本身也占空间 |
现代文件系统普遍用索引分配(ext4 多级索引、NTFS MFT 运行列表)。
内存中
| 结构 | 内容 |
|---|---|
| 挂载表 | 已挂载文件系统的超级块副本 |
| inode 缓存 | 正在使用的 inode 副本 |
| dentry 缓存 | 路径分量 → inode 的映射,加速路径解析 |
| 打开文件表 | 当前系统内所有打开的文件及其读写位置 |
空闲空间管理
| 方法 | 原理 | 适合场景 |
|---|---|---|
| 空闲表法 | 记录 (起始块号, 长度),类似动态分区 | 连续分配 |
| 空闲链表法 | 把所有空闲块串成链表 | 简单但效率低 |
| 位示图法 | 每个块对应 1 bit:0 空闲 / 1 已用 | 现代主流,易找连续空闲块 |
| 成组链接法 | 把连续空闲块号分组,一组存下一组指针,批量出入 | Unix 传统做法 |
虚拟文件系统(VFS)
为什么需要 VFS
不同文件系统的物理存储方式完全不同。以三种常见文件系统为例:
| 文件系统 | 文件定位方式 | 怎么找到数据块 |
|---|---|---|
| ext4 | inode + 多级索引 | inode 里存 12 个直接块 + 1 个一级间接块 + 1 个二级间接块 + 1 个三级间接块 |
| FAT32 | 文件分配表(FAT)链 | 目录项存首簇号,FAT 表中每个簇指向下一个簇,跟着链条找到所有簇 |
| NTFS | MFT + 运行列表 | MFT 记录里存 (起始簇号, 连续簇数) 的运行列表,直接描述一段段连续区域 |
如果每个文件系统都要应用程序自己适配,open 一个 ext4 文件和 open 一个 FAT32 U 盘就得写两套代码。
VFS 的解决方案:在系统调用和具体文件系统之间加一个抽象层:
应用程序
↓ open/read/write/close(统一接口,只认 fd)
VFS 层(把调用分发到对应文件系统)
↓ ↓ ↓
ext4 驱动 FAT32 驱动 NTFS 驱动
↓ ↓ ↓
[ext4 盘] [FAT32 U盘] [NTFS 盘]
应用程序只管调 read(fd, buf, n),VFS 根据 fd 指向的文件所属文件系统,把调用翻译成 ext4_read() 或 fat32_read() 或 ntfs_read()。
VFS 四个核心对象
| 对象 | 含义 | 存在位置 |
|---|---|---|
| super_block | 代表一个已挂载的文件系统实例 | 磁盘 → 内存挂载表 |
| inode (Vnode) | 代表一个文件的属性 + 数据位置 | 磁盘 inode 区 → 内存 inode 缓存 |
| dentry | 目录项,关联一个路径分量到 inode | 仅内存,解析时创建 |
| file | 代表文件在一次打开中的状态(偏移量、打开模式) | 仅内存,进程打开文件表 |