Chapter 9 File System Implementation¶
File System History
| 阶段 | 时间 | 特点 |
|---|---|---|
| Early File Systems | 1950s-1970s | 基础、定制化,常为特定 OS 或特定计算机模型设计 |
| Hierarchical File Systems | 1970s-1980s | 引入 directories,典型如 UNIX file system 和 Microsoft FAT |
| Network File Systems | 1980s-1990s | 支持网络访问,典型如 NFS |
| Journaling File Systems | 1990s-2000s | ext3、ext4、NTFS 等先记录变更,再执行操作,便于 crash recovery |
| Modern File Systems | 2000s-Present | ZFS、Btrfs 提供 snapshots、dynamic volume management、data integrity checks;GFS、S3 等面向大规模分布式存储 |
1 File-System Structure¶
文件(File)是存放一组关联信息的逻辑存储单元(logical storage unit),文件系统位于外存(secondary storage)/磁盘(disk)上,磁盘驱动(disk driver)提供磁盘块读写接口。
文件系统面向用户/程序提供存储接口,完成逻辑地址到物理地址的映射。保存单个文件相关信息的存储结构称为文件控制块(file control block)。
常见 File Systems
- Linux:
ext2、ext3、ext4、ReiserFS、Btrfs 等 - Windows:FAT、FAT32、NTFS
- 其它:ZFS、GoogleFS、Oracle ASM、FUSE
文件系统通常分层实现,以降低复杂度和重复代码。

-
逻辑文件系统(Logical file system)
- 保存文件系统运行必需的所有元数据(metadata),即除文件正文内容之外的全部信息
- 存储目录结构
- 维护用于描述文件的数据结构文件控制块(FCB)
- 文件名、文件所有者、访问权限
- 引用计数、各类时间戳、指向其他文件控制块(FCB)的指针
- 指向磁盘数据块的指针
- 接收上层传来的指令
open/read/write filepath - 向下层下发指令
read/write logical blocks
-
文件组织模块(File-organization module)
- 维护逻辑文件块(编号从 0 到 N)与对应物理文件块的映射关系,完成二者间的地址转换
- 负责管理磁盘空闲存储空间(free space)
- 接收上层下发命令:
read logical block 3、write logical block 17 - 向下传递硬件操作指令:
read physical block 43、write physical block 421
-
基础文件系统(Basic file system):在 Linux 系统里对应块 I/O 子系统(block I/O subsystem)
- 负责分配、管理各类缓冲区,缓冲区中存放文件系统元数据、目录信息与数据块
- 这类缓冲区属于高速缓存,作用是优化系统读写性能
- 输入:
read physical block #43、write physical block #421 - 输出:向更底层提交同一 physical block 的读写请求
-
I/O 控制层(I/O control):由设备驱动程序(device drivers)与中断处理程序(interrupt handlers)构成
- 输入:
read physical block #43、write physical block #124 - 输出:向设备控制器的内存(controller memory)写入指令,执行磁盘读写操作,同时响应相关硬件中断
- 输入:
Layering Tradeoff
- 分层设计有利于降低复杂度、减少冗余,但会带来额外开销,有可能降低运行性能。
- 通过维护文件控制块(在 UNIX 系统里是inode 索引节点),把文件名解析转换成文件编号、文件句柄、文件存储位置。
- 系统设计者可根据需求,选用任意编程方案实现各逻辑分层。
2 File System Data Structures¶
文件系统需要持久保存元数据,同时在内存中维护高速访问结构。
2. 1 On-Disk Structures¶
| 含义 | 示例 | |
|---|---|---|
| Boot control block | 若 volume 存放 OS,第一块可保存启动信息 | UFS boot block、NTFS partition boot sector |
| Volume control block | 保存 volume 的 block 数、block size、free-block count、free-FCB count 等 | UFS superblock、NTFS master file table |
| Directory | 文件名到 ID / FCB pointer 的映射 | directory entries |
| Per-file FCB | 每个文件的 metadata | NTFS 中 FCB 可看作 relational database 的一行 |
2. 2 In-Memory Structures¶
On-disk structures 是持久事实,但每次操作都读磁盘会非常慢。内核把频繁访问的 metadata 和 data blocks 缓存在内存中,以减少磁盘 I/O。
| 作用 | |
|---|---|
| Mount table | 每个 mounted volume 一个 entry |
| Directory cache | 加速 path translation |
| Global open-file table | 系统范围内已打开文件对象 |
| Per-process open-file table | 每个进程自己的 file descriptors |
| Buffers | 保存正在传输或缓存的 disk blocks |
2. 3 Inode¶
File Control Block

FCB(File Control Block) 保存文件 metadata 和定位文件数据所需的信息。Unix 系统中常见实现是 inode(index node)。

Unix / UFS 中 inode number 通常只在同一个文件系统内唯一,跨文件系统时需要结合设备号或 mount 信息才能唯一标识文件。
3 File Operations¶
3. 1 File Creation¶
- 应用进程请求创建新文件
- 逻辑文件系统分配一个新的 FCB / inode
- 在对应目录中加入新 file name 与 FCB 的映射
- 初始化权限、owner、timestamps、size 等元数据
3. 2 open()¶
- 搜索系统全局打开文件(system-wide open-file table),判断文件是否已在使用
- 若已在使用:创建 per-process open-file table entry,指向已有 system-wide open-file table entry
- 若未在使用:搜索 directory 找到 file name,从磁盘加载 FCB 到内存,放入 system-wide open-file table
- 在 per-process open-file table 中建立 entry,指向 system-wide open-file table entry,保存 current file position 和 access mode
- 增加 system-wide entry 中的 open count
- 返回指向 per-process open-file table entry 的 handle / file descriptor

后续 read、write、seek、close 都基于这个 handle 执行。
- 单个进程关闭文件:移除对应 per-process open-file table entry,system-wide open count 减 1
- 所有进程都关闭该文件:把必要的 in-memory directory / metadata 信息写回磁盘,从 system-wide open-file table 中移除该 entry
文件系统挂载(Mounting File Systems)
启动时,boot loader 会定位并挂载 root partition,外部 file systems 需要挂载到设备和目录树上。
- 引导块(Boot block):由一组连续磁盘块构成,存放引导加载程序镜像(boot loader memory image),定位并挂载根分区(root partition),完成内核的定位、加载与启动运行。
- 内存挂载表(In-memory mount table):记录挂载目录(mount points)、已挂载的文件系统格式(mounted FS types)、对应文件系统的访问路径。
- Unix 系统实现:内存挂载表中存放指针,指向对应设备上文件系统的超级块(superblock)。
4 Virtual File Systems¶
OS 定义一套通用 FS interface,所有具体 file systems 都必须实现这套 interface,system calls 基于通用 interface 实现,同一 syscall API 可用于不同 FS 类型。
虚拟文件系统(VFS, Virtual File Systems)把 generic FS operations 与 implementation details 分离,使得 OS 可以根据对象所属 FS 类型分派到对应实现。

Linux 定义了四类核心 VFS objects:
| Object | 含义 |
|---|---|
| 超级块(superblock) | 描述 file system type、size、status 和其它 metadata |
| 索引节点(inode) | 描述某个文件 metadata,如 location、access mode、owner |
| 目录项(dentry) | 把 name 与 inode 关联,维护目录层级结构 |
| 文件对象(file) | 关联文件的实际数据内容 |
VFS 预先定义了一套作用于内核对象的操作集合,各类具体文件系统必须完成这些接口的实现。
代码:struct file_operations(文件操作函数表)
struct file_operations {
struct module *owner; // 归属内核模块
loff_t (*llseek)(struct file *, loff_t, int); // 修改文件读写偏移(文件寻址)
ssize_t (*read)(struct file *, char __user *, size_t, loff_t); // 同步读文件
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t);// 同步写文件
ssize_t (*read_iter)(struct kiocb *, struct iov_iter *); // 批量迭代读(异步/分散IO)
ssize_t (*write_iter)(struct kiocb *, struct iov_iter *); // 批量迭代写
int (*iopoll)(struct kiocb *kiocb, bool spin); // IO轮询
int (*iterate)(struct file *, struct dir_context *); // 遍历目录项
int (*iterate_shared)(struct file *, struct dir_context *); // 共享方式遍历目录
__poll_t (*poll)(struct file *, struct poll_table_struct *); // 事件轮询(poll)
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long); // 设备控制指令
long (*compat_ioctl)(struct file *, unsigned int, unsigned long); // 兼容32位程序的ioctl
int (*mmap)(struct file *, struct vm_area_struct *); // 文件内存映射
unsigned long mmap_supported_flags; // mmap支持的属性标记
int (*open)(struct inode *, struct file *); // 打开文件
int (*flush)(struct file *, fl_owner_t id); // 刷新文件缓冲区
int (*release)(struct inode *, struct file *); // 关闭、释放文件资源
};
示例

What's the call path when writing a NFS file?
write syscall -> vfs_write -> indirect call -> nfs_file_write_iter
如果写的是 NFS file,间接调用会分派到 NFS 对应的 file operations,而不是 ext4 的实现。NFS 是网络文件系统,nfs_file_write_iter 内部不走本地磁盘 IO,而是通过网络协议把数据发给远端服务器。
file->f_op 什么时候设置
一般在 open() / lookup 过程中,内核根据 inode 所属 file system 和文件类型初始化 struct file,并把 file->f_op 指向对应实现的 operation table,后续 read / write / ioctl 等通过这些函数指针分派。
5 Directory Implementation¶
Directory 本质上是一种特殊文件,保存 file name -> inode 的映射。

目录自身拥有 1 个 inode,inode 指针指向目录的数据块,目录数据块内部由多个 ext2_dir_entry 目录项连续拼接组成。
示例:磁盘目录项

其中 rec_len 是整条目录项占用总字节长度,取值为 4 的整数倍,末尾多余 \0 是空字节填充。rec_len 允许目录项被删除后留下可复用空间,便于后续插入新 entry。
| 实现 | 做法 | 优缺点 |
|---|---|---|
| Linear list | 存储文件名,附带指向文件元数据的指针 | 简单,但搜索慢 |
| Ordered list / B+ tree | 按名字排序或使用树结构 | 查找更快,实现更复杂 |
| Hash table | 用 file name hash 定位 bucket | 查找快,但需处理 collisions |
Unix vs Windows
- Unix 把 directories 当作包含特殊数据的 files
- Windows 对 directory 和 file 区分更强,创建和操作 directory 需要专门 system calls
open() 搜索系统全局打开文件表(system-wide open-file table),若查找命中(文件已被其他进程打开)则在 per-process open-file table entry 中新建一条表项,若没找到就遍历目录结构找文件名,找到后把 FCB 复制到内存中的系统全局打开文件表。
How about create a new file?
- 分配新的 FCB / inode
- 在目录文件中加入 directory entry
- directory entry 把 file name 映射到新 inode number
- 初始化 inode metadata 和空 data block mapping
6 Disk Block Allocation¶
文件需要分配磁盘块保存数据,不同 allocation methods 在连续性、随机访问、文件增长、碎片和可靠性之间取舍。
6. 1 Contiguous Allocation¶
连续分配(Contiguous allocation) 为每个文件分配一段连续磁盘块,目录只需保存起始磁盘地址和占用磁盘块总数量。

这种方法顺序读写时磁盘磁头移动少,磁盘寻道耗时短,但难以找到足够大的连续空闲空间,会产生外部碎片(external fragmentation),可能需要 compaction 或 defrag,开销大。
文件扩容困难
- 复制到更大的 hole:开销高
- 让用户预估最大文件大小:不方便且可能产生 internal fragmentation
- 优化方案:把文件拆成多个 contiguous chunks,形成盘区(extent)
6. 2 Linked Allocation¶
链式分配(Linked allocation) 把文件组织成多个磁盘块的单向链表,每个磁盘块中存放下一个磁盘块的指针,用空指针(nil pointer)标识文件末尾。

- 优点:磁盘块可离散分布在磁盘各处,无外部碎片,不需要磁盘碎片整理,文件扩容较容易。
- 缺点:
- 随机定位文件某一块需要多次磁盘 I/O 多次磁头寻道,效率低
- 指针占用存储空间,512 bytes block 中有 4 bytes pointer,浪费约
0.78% - 指针损坏会破坏后续链表,可靠性差
- 优化方案:可以把多个 blocks 组成 cluster,减少 pointer 数量、提高吞吐,但 cluster 可能造成 internal fragmentation。
File-Allocation Table
FAT(File Allocation Table) 是 linked allocation 的变体,每个 block 的 next pointer 不放在 block 内部,而放在一张集中表中。

FAT 把链表指针集中起来,减少数据 block 内部指针污染,也方便缓存整张或部分 allocation table。但 FAT 本身可能很大,且仍保留 linked allocation 的链式访问特征。
6. 3 Indexed Allocation¶
索引分配(Indexed allocation) 为每个文件维护索引块(index blocks),索引块中保存指向文件数据块的指针。
- 优点:支持 random / direct access,没有 external fragmentation,支持 sparse file 中的 holes。
- 缺点:索引块需要单独占用磁盘空间,小文件场景下空间浪费严重

Index Blocks 如何扩展?
- Linked index blocks:多个 index blocks 链接起来支持大文件
- Multiple-level index blocks:多级索引,例如二级索引
- Combined scheme:小文件用 direct pointers,大文件逐步使用 indirect pointers
示例:UNIX FCB / inode 指针
假设给出 inode 中前 15 个 pointers,其中前 12 个是 direct blocks,后 3 个是 indirect blocks。

若 block size = 512 bytes,pointer size = 4 bytes,则一个 indirect block 可存 \(512 / 4 = 128\) 个 block pointers,最大文件大小约为:
若 block size 改为 4 KB,一个 indirect block 可存 4096 / 4 = 1024 个 pointers,最大文件大小会大幅增加。
| Method | Sequential Access | Random Access | Fragmentation | 文件增长 |
|---|---|---|---|---|
| Contiguous | 很好 | 很好 | external fragmentation | 困难 |
| Linked | 可接受 | 很差 | 无 external fragmentation,可能有 cluster 内碎片 | 容易 |
| Indexed | 较好 | 较好 | 无 external fragmentation,有 index overhead | 较容易 |
Disk I/O 非常慢,文件系统设计要尽量减少 disk I/O
以 2011 年 Intel Core i7 Extreme Edition 990X 约 159,000 MIPS 为例:
-
普通磁盘约
250 IOPS:\[ 159000 / 250 \approx 630 \]即一次磁盘 I/O 时间内 CPU 可执行约
630 million条指令。 -
快速 SSD 约
60,000 IOPS:\[ 159000 / 60000 \approx 2.65 \]即一次 I/O 仍相当于数百万条指令时间。
7 Free-Space Management¶
文件系统需要维护 free-space list,跟踪哪些 blocks / clusters 可用,删除文件后其占用空间必须被回收。
7. 1 Bitmap Free-Space Management¶
位图(Bitmap) 为每个 block 使用一个 bit 标记空闲或已分配,空间紧凑,容易找到连续 free blocks,适合配合硬件位操作或内存扫描优化。

Bitmap 大小
若 block size = 4 KB = 2^12 bytes,disk size = 1 TB = 2^40 bytes,则:
block 数:\(n = 2^{40} / 2^{12} = 2^{28}\)
bitmap 需要 2^28 bits = 256 Mbits = 32 MB。
若 cluster = 4 blocks,则只需 64 Mbits = 8 MB。
7. 2 Linked Free Space¶
空闲链表(Linked free space)把所有 free blocks 串成 linked list,不浪费额外专门空间,可使用 free block 内部保存 pointer,但难以快速分配连续的空闲磁盘空间。常规分配无需遍历全链表,直接取出链表头部空闲块进行分配即可。

7. 3 Grouping and Counting¶
简单 free-block linked list 效率不高,因为分配多个 free blocks 可能需要多次遍历或额外 I/O。
- 分组(Grouping):在第一个 free block 中保存
n-1个 free block addresses,再保存指向下一个 index block 的 pointer。这样一次读 index block 就能得到多个 free blocks,不必每分配一个 block 都顺链访问一次。 - 计数(Counting):使用
(starting block, count)表示一段连续 free blocks。因为磁盘空间经常成片分配和释放,counting 能紧凑描述连续空闲区域,也更容易分配连续 blocks。
8 File System Performance¶
影响 FS Performance 的因素
- Disk allocation algorithms
- Directory algorithms
- Directory entry 中保存哪些 metadata
- Metadata structures 是预分配还是按需分配
- Data structures 是 fixed-size 还是 varying-size
提升文件系统性能的优化手段:
- 把 data 和 metadata 放近,提高 spatial locality
- 使用 cache,把频繁访问 blocks 留在内存
- 使用异步写(asynchronous writes),数据可被 buffered / cached,返回更快
- 对必须同步落盘的数据使用同步写(synchronous writes),app 可主动要求,OS metadata 有时也必须这样做
- 滞后释放(Free-behind),顺序访问时移除已经读过且不再需要的 previous page
- 预读(Read-ahead):预测顺序访问,提前读入后续 pages
Reads frequently slower than write: really?
从应用视角看,write 可能很快返回,因为它只是写入 page cache / buffer cache,稍后异步落盘;而 read 若 cache miss,就必须等待磁盘把数据读回来。因此很多情况下 read 看起来比 write 慢。
但若要求 synchronous write,write 必须等数据真正到达稳定存储,可能比普通 read 更慢。
OS 中有不同层次的 cache:
- Page cache:缓存 memory-mapped I/O 的 pages,例如 memory-mapped files
- Buffer / disk cache:文件系统用于 disk I/O 的 block cache
早期系统中,memory-mapped file 和普通 file I/O 可能被缓存两次,造成 double caching。
Unified buffer cache 使用同一套 page cache 同时缓存 memory-mapped pages 和普通 disk I/O,避免同一文件数据在系统中出现两份缓存。

文件系统必须在 crash 后尽量恢复一致状态。
一致性检查(Consistency checking)会比较 directory data 和磁盘 metadata,检查是否一致。
但是全盘检查可能很慢,某些损坏不一定能自动修复,系统越大,crash recovery 时间越难接受。
Log-Structured File System
日志结构文件系统(LSFS, Log-Structured File System) 把 metadata updates 顺序写入环形日志(circular log)。
- 更新先顺序写入 log,修改一旦写入日志即完成提交,系统调用便可返回
- 后与此同时,日志条目在文件系统上重放,完成实际更新
- 事务重放(replay)后可从 log 中移除
- log 是环形结构,但 un-replayed entries 不会被覆盖
- 垃圾回收可回收(reclaim)、压缩日志条目
- 系统崩溃时,仅需重放日志内现存事务
Logging 把随机 metadata 更新转成顺序写,并让 crash recovery 只需处理 log 中未完成的事务,而不是扫描整个文件系统。
9 File Interfaces¶
Two Key Abstractions
文件(File) 可以看作字节线性数组(linear array of bytes),支持对任意字节位置读写。OS 通常只管理 file 的 metadata 和 blocks,不理解该文件到底是图片、数据库页还是文档。
| Abstraction | Low-level name | 主要内容 |
|---|---|---|
| File | inode number | byte array、metadata、data block pointers |
| Directory | inode number | user-readable name -> inode number 的映射列表 |
Directory entry 可以指向普通文件,也可以指向另一个 directory,所以目录树才能递归构成完整 namespace。
External Name V.S. Internal Name
- External name:用户可见的符号化文件名(symbolic name),在 hierarchical file system 中表现为 pathname,例如
/foo/bar - Internal name:文件系统内部使用的 low-level name,Unix 中典型是 inode number
- Directory 的核心作用就是把 external name 翻译成 internal name
创建文件时常见的 open() flags:
| Flag | 含义 |
|---|---|
O_CREAT |
文件不存在时创建;注意不是 O_CREATE |
O_WRONLY |
以 write-only 方式打开 |
O_TRUNC |
若文件已存在,把长度截断为 0 bytes |
open() 创建文件
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, mode);
open() 的返回值是文件描述符,它是一个取值很小的非负整数;后续 read、write、lseek、fcntl 等系统调用,都通过这个编号来操作已打开的文件。调用成功时,函数会返回当前进程中编号最小、尚未被占用的文件描述符。
文件描述符(File descriptor)

文件描述符是进程专属文件描述符表(per-process file descriptor table)的索引号,文件描述符表的每一个表项都存储了指向文件对象(file object)的引用,每个文件对象又存储着指向 inode 的引用。
| 结构 | 作用 |
|---|---|
| file descriptor table | per-process table,fd 是其中的 index |
| file object / opened file | 表示一次打开状态,保存 current read/write offset、non-blocking flag 等非持久状态 |
| inode | 表示 filesystem object,保存 owner、permission、data block references 等 metadata |
多次调用open()打开同一个文件路径,生成的多个文件描述符会指向不同的文件对象,但所有文件对象最终指向同一个 inode;而通过 dup2() 或 fork() 复制出来的文件描述符共用同一个文件对象。
BSD 锁、打开文件描述锁依附于文件对象,POSIX 记录锁则绑定在 [inode, pid] 组合上。
stdin、stdout、stderr 通常分别是 fd 0、1、2。
示例
# 执行指令:使用strace追踪静态程序 a.static 的全部系统调用
strace ./a.static
# ========== 下方为strace捕获到的系统调用拆解注释 ==========
# 1.execve:内核加载、启动可执行程序 ./a.static,入参:程序路径、argv数组、环境变量;返回0代表程序启动成功
execve("./a.static", ["./a.static"], 0x7ffc89bb12f0 /* 68 vars */) = 0
# 2.brk(NULL):查询进程当前堆区边界地址,返回堆起始地址 0xc17000
brk(NULL) = 0xc17000
# 3.brk(0xc181c0):扩容堆内存至指定地址,成功修改堆边界
brk(0xc181c0) = 0xc181c0
# 4.arch_prctl:CPU架构相关寄存器配置,用于FS段寄存器初始化,返回0成功
arch_prctl(ARCH_SET_FS, 0xc17880) = 0
# 5.uname:获取本机操作系统信息(系统名Linux、主机名parallels等),返回0成功
uname({sysname="Linux", nodename="parallels", ...}) = 0
# 6.readlink:读取/proc/self/exe软链接,获取当前运行程序的磁盘绝对路径,读取字节数30
readlink("/proc/self/exe", "/home/wenbo/os-course/a.static", 4096) = 30
# 7.两次brk:再次扩容进程堆内存,动态分配运行所需堆空间
brk(0xc391c0) = 0xc391c0
brk(0xc3a000) = 0xc3a000
# 8.access:检查/etc/ld.so.nohwcap文件是否存在;返回-1、ENOENT=无该文件(静态链接程序无需动态链接器配置)
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
# 9.fstat(1):查询【fd=1 标准输出stdout】的文件属性;S_IFCHR代表字符设备(终端),返回0成功
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
# 10.write(1,...):向fd=1(标准输出)写入13字节字符串hello world!\n,成功写入13字节,终端打印文本
write(1, "hello world!\n", 13hello world!
) = 13
# 11.exit_group(0):进程整组退出,退出码0(正常结束)
exit_group(0) = ?
# strace收尾标记:进程退出码为0
+++ exited with 0 +++
strace cat main.c 中 main.c 常见 fd 一般是 3。因为 0、1、2 已经分别用于标准输入、标准输出和标准错误,open("main.c", ...) 通常返回最小可用 fd。
示例
# strace cat main.c :跟踪cat读取并打印main.c的系统调用全过程
# 1. 以当前工作目录为基准,只读打开main.c,成功返回文件描述符 fd=3(0/1/2预留标准IO)
openat(AT_FDCWD, "main.c", O_RDONLY) = 3
# 2. 获取fd=3对应文件属性:普通文件,权限674,文件总大小89字节
newfstatat(3, "", {st_mode=S_IFREG|0674, st_size=89, ...}, AT_EMPTY_PATH)
# 3. 告知内核:该文件会顺序读取,内核启用预读缓存优化,调用成功返回0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
# 4. mmap申请一块139264字节匿名私有内存,作为文件读写缓冲区;无返回值是strace省略地址
# PROT_READ|PROT_WRITE:内存可读可写;MAP_ANONYMOUS匿名映射(不关联磁盘文件)、MAP_PRIVATE私有
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
# 5. 从fd=3(main.c)读取最多131072字节,实际读到89字节(文件全部内容),存入缓冲区
read(3, "#include <stdio.h>\n\nint main () "... , 131072) = 89
# 6. 将读到的89字节数据写入fd=1(标准输出stdout),终端打印main.c内容,写入成功89字节
write(1, "#include <stdio.h>\n\nint main () "... , 89) = 89
# 7. 再次调用read读fd=3,已经读到文件末尾EOF,返回0代表无更多数据
read(3, "", 131072) = 0
# 8. munmap释放之前mmap申请的缓冲区内存,释放成功返回0
munmap(0xffff969f7000, 139264) = 0
# 9. 关闭main.c的文件描述符fd=3
close(3) = 0
# 10. 关闭标准输出fd=1
close(1) = 0
# 11. 关闭标准错误fd=2
close(2) = 0
# 12. exit_group(0):结束整个进程,进程终止、无返回值,strace用?标记
exit_group(0) = ?
# strace标记:进程正常退出,退出码0
+++ exited with 0 +++
-
write():仅把数据交给文件系统,未来某个时间点写入磁盘持久存储(persistent storage)。为了提升性能,文件系统会先把待写入数据缓存在内存中,可能等待 5 秒、30 秒或由策略决定的时间后再真正写到设备。 -
fsync():强制把该文件相关的全部脏数据(dirty data)写回磁盘。如果程序需要保证 crash 后数据仍在,例如数据库 commit、编辑器保存重要文件,仅调用write()不够,需要在合适位置调用fsync()或等价机制。
示例
// 以不存在则新建、只写、文件存在则清空模式打开 foo 文件
int fd = open ("foo", O_CREAT | O_WRONLY | O_TRUNC);
assert (fd > -1); // 断言校验:文件打开成功,fd 为合法正数
// 把 buffer 缓冲区的 size 字节数据写入文件,数据暂存内核页缓存,不保证立刻落盘
int rc = write (fd, buffer, size);
assert (rc == size); // 断言校验:实际写入字节数等于预期长度
rc = fsync (fd); // 强制刷盘:把当前文件所有缓存脏数据落地磁盘
assert (rc == 0); // 断言校验:刷盘操作执行成功
struct stat
struct stat {
unsigned long st_dev; /* 文件所在磁盘设备号 */
unsigned long st_ino; /* 文件inode编号(文件唯一底层序列号) */
unsigned int st_mode; /* 文件类型 + 权限位(0664这类权限存于此) */
unsigned int st_nlink; /* 硬链接计数:指向该inode的目录项数量 */
unsigned int st_uid; /* 文件所有者用户ID */
unsigned int st_gid; /* 文件所属用户组ID */
unsigned long st_rdev; /* 若为设备文件:保存主次设备号;普通文件无意义 */
unsigned long __pad1; /* 结构体对齐预留填充位,无用 */
long st_size; /* 文件大小,单位:字节 */
int st_blksize; /* 文件IO最优块大小(读写缓冲区推荐尺寸) */
int __pad2; /* 结构体对齐填充 */
long st_blocks; /* 文件占用磁盘块数,**每块固定512字节** */
long st_atime; /* 文件最后访问时间(秒级时间戳) */
unsigned long st_atime_nsec;/* 访问时间纳秒小数部分 */
long st_mtime; /* 文件内容最后修改时间(秒级) */
unsigned long st_mtime_nsec;/* 修改时间纳秒部分 */
long st_ctime; /* 文件属性最后变更时间(权限/属主/硬链接变化) */
unsigned long st_ctime_nsec;/* 属性变更时间纳秒部分 */
unsigned int __unused4; /* 预留未使用字段 */
unsigned int __unused5; /* 预留未使用字段 */
};
这些信息大多最终来自 inode,只是通过 system call 以统一格式返回给 user programs。
示例:stat foo 返回值
File: 'foo' # 文件名
Size: 6 Blocks: 8 IO Block: 4096 regular file
# Size=6:文件内容占6字节;Blocks=8:占用8个512B磁盘块(8×512=4KB);IO Block=4096:系统最优IO块4KB;regular file:普通文件
Device: 801h/2049d Inode: 1328649 Links: 1
# 设备号十六进制0x801=十进制2049;inode编号1328649;硬链接数1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ os) Gid: ( 1000/ os)
# 文件权限0664 → -rw-rw-r--;所有者UID=1000(用户名os),所属组GID=1000(组名os)
Access: 2018-12-19 00:24:02.448286431 +0800 # st_atime:最后读取访问时间
Modify: 2018-12-19 00:24:01.316296543 +0800 # st_mtime:文件内容修改时间
Change: 2018-12-19 00:24:01.316296543 +0800 # st_ctime:inode属性变更时间
Birth: - # Linux不支持存文件创建时间(Birth),显示-
Unix 删除文件常叫 unlink,因为真正被删除的是 directory 中的一个名字到 inode 的链接。
Why we just remove or delete the file, but using unlinkat?
删除某个路径时,OS 先 unlink 这个 directory entry,再根据 inode reference count 判断是否真的释放文件实体。
10 Hard Link and Soft Link¶
同一个文件可以在多个目录下拥有多个文件名,这类别名统称为链接(link)。
| Link | 本质 | inode 关系 | 跨 FS | 指向 directory | 目标删除后的结果 |
|---|---|---|---|---|---|
| Hard link | directory entry | 与原文件相同 inode | 通常不允许 | 通常不允许用户随意创建 | link count 减少,计数为 0 后才释放 |
| Soft link / symlink | 保存 pathname 的特殊文件 | symlink 自己有不同 inode | 可以 | 可以 | 可能变成 dangling link |
- 硬链接(Hard link):
.指向当前目录自身的硬链接,..指向上级父目录的硬链接。

示例
# 创建 file1 并写入内容 hello
echo hello > file1
# 查看源文件内容
cat file1
# 输出:hello
# ln不 加 -s = 创建硬链接 file2,file1 与 file2 共用同一个 inode
ln file1 file2
# 查看文件详细属性:第 2 列数字 2 是硬链接计数(st_nlink = 2)
ls -l file*
# -rw-rw-r-- 2 parallels parallels 6 Dec 17 21:02 file1
# -rw-rw-r-- 2 parallels parallels 6 Dec 17 21:02 file2
# 访问硬链接,读取到和源文件完全一致的数据
cat file2
# 输出:hello
# ls -i:查看 inode 编号,二者 inode 号完全相同,为同一个文件实体
ls -i file1 file2
# 3670573 file1 3670573 file2
- 软链接(Soft link):存放目标文件的路径的独立文件,又称符号链接(Symbolic link / symlink)。

示例
# 查看原文件内容
cat file1
# 输出:hello
# ln -s 创建软链接 file2,file2 存目标路径 file1
ln -s file1 file2
# ll查看属性
ll file*
# -rw-rw-r-- 1 parallels parallels 6 Dec 17 21:02 file1
# 硬链接计数 = 1;普通文件
# lrwxrwxrwx 1 parallels parallels 5 Dec 17 21:14 file2 -> file1
# l开头 = 软链接文件,箭头指向源文件,软链接是独立文件
# -i 查看 inode:二者 inode 不同,是两个独立 inode
ls -i file*
# 3670573 file1 3670575 file2
# 删除源文件 file1
rm file1
# 软链接残留,指向已消失文件(失效悬空链接)
ll file*
# lrwxrwxrwx 1 parallels parallels 5 Dec 17 21:14 file2 -> file1(标红)
11 On-disk layout of FS¶
同一个文件系统对象在不同层次有不同表示。
| View | 示例 | 含义 |
|---|---|---|
| VFS data structure | struct inode |
内核统一文件系统接口使用的通用对象 |
| In-memory private structure | ext2_inode_info |
具体 FS 在内存中的扩展结构,可同时包含 ext2 私有信息和 VFS inode |
| On-disk structure | ext2_inode |
持久化在磁盘上的 inode 格式 |
文件系统需要把 open/read/write 等 calls 映射到自己的结构和实现函数上。Linux 中常见路径是通过 inode / file object 上的 operation table,例如 inode->i_fops 或 file->f_op。
示例:FS Organization
假设:
- block size =
4 KB - total blocks =
64 - data region =
56blocks - inode table =
5blocks - inode size =
256 bytes
那么一个 4 KB block 可容纳 \(4096 / 256 = 16\) 个 inodes,因此 5 个 inode blocks 可容纳 \(5 \times 16 = 80\) 个 inodes,也就是最多大约 80 个 files / directories。

| 区域 | 作用 |
|---|---|
| Superblock | 保存 FS 总体信息:inode / data block 数量、inode table 起点、data region 起点、magic number |
| Inode bitmap | 标记哪些 inodes 空闲 |
| Data bitmap | 标记哪些 data blocks 空闲 |
| Inode table | 连续保存 on-disk inodes |
| Data region | 保存 file contents 和 directory contents |
若要读取 inode number 32,则 \(32 \times 256 = 8192 bytes = 8 KB\),考虑 4 KB superblock 和 8 KB bitmaps,因此 inode 32 的地址偏移为:

Linux ext2_inode

Read /foo/bar
读取 /foo/bar 时,假设 foo 是 directory,bar 是其中的 file,则路径可拆分为:根目录/ → foo目录 → bar文件。

open("/foo/bar")三步寻址:- 读 root inode → 读根目录数据块:在根目录里检索
foo目录项,拿到 foo 的 inode - 读 foo inode → 读 foo 目录数据块:在 foo 目录里检索
bar文件项,拿到 bar 的 inode - 读 bar inode:完成打开,得到文件 fd
- 读 root inode → 读根目录数据块:在根目录里检索
- 连续 3 次
read()读 bar 内容- 第 1 次read:读
bar-data0→ 写 bar-inode(刷新文件访问时间 atime) - 第 2 次read:读
bar-data1→ 写 bar-inode(再次更新 atime) - 第 3 次read:读
bar-data2→ 写 bar-inode(再次更新 atime)
- 第 1 次read:读
每次读文件内容,内核都要修改 inode 里的最后访问时间(atime),因此伴随一次 inode 落盘写操作。
Write to Disk: /foo/bar
写入 /foo/bar,若 bar 不存在,则不仅要写 file data,还要修改 directory 和 metadata。

如果没有 caching,每次打开文件都可能在 path 的每一级 directory 上产生两次读 read inode + read directory,层数越深,开销越大。
早期系统会分配固定大小 cache,保存 popular blocks,现代系统通常用 unified page cache 同时缓存 virtual memory pages 和 file system pages。
写缓冲(write buffering)让 writes 暂时留在内存中,过一段时间再同步写入磁盘。数据库这类系统可能使用 direct I/O 或 raw data,绕开部分 OS cache 策略,由数据库自己管理 buffer、logging 和一致性