Skip to content

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:ext2ext3ext4、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 3write logical block 17
    • 向下传递硬件操作指令:read physical block 43write physical block 421
  • 基础文件系统(Basic file system):在 Linux 系统里对应块 I/O 子系统(block I/O subsystem)

    • 负责分配、管理各类缓冲区,缓冲区中存放文件系统元数据、目录信息与数据块
    • 这类缓冲区属于高速缓存,作用是优化系统读写性能
    • 输入:read physical block #43write physical block #421
    • 输出:向更底层提交同一 physical block 的读写请求
  • I/O 控制层(I/O control):由设备驱动程序(device drivers)中断处理程序(interrupt handlers)构成

    • 输入:read physical block #43write physical block #124
    • 输出:向设备控制器的内存(controller memory)写入指令,执行磁盘读写操作,同时响应相关硬件中断

Layering Tradeoff

  1. 分层设计有利于降低复杂度、减少冗余,但会带来额外开销,有可能降低运行性能。
  2. 通过维护文件控制块(在 UNIX 系统里是inode 索引节点),把文件名解析转换成文件编号、文件句柄、文件存储位置。
  3. 系统设计者可根据需求,选用任意编程方案实现各逻辑分层。

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

  1. 应用进程请求创建新文件
  2. 逻辑文件系统分配一个新的 FCB / inode
  3. 在对应目录中加入新 file name 与 FCB 的映射
  4. 初始化权限、owner、timestamps、size 等元数据

3. 2 open()

  1. 搜索系统全局打开文件(system-wide open-file table),判断文件是否已在使用
  2. 若已在使用:创建 per-process open-file table entry,指向已有 system-wide open-file table entry
  3. 若未在使用:搜索 directory 找到 file name,从磁盘加载 FCB 到内存,放入 system-wide open-file table
  4. 在 per-process open-file table 中建立 entry,指向 system-wide open-file table entry,保存 current file position 和 access mode
  5. 增加 system-wide entry 中的 open count
  6. 返回指向 per-process open-file table entry 的 handle / file descriptor

后续 readwriteseekclose 都基于这个 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,最大文件大小约为:

\[ 12 \times 512 + 128 \times 512 + 128^2 \times 512 + 128^3 \times 512 \]

若 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 990X159,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() 的返回值是文件描述符,它是一个取值很小的非负整数;后续 readwritelseekfcntl 等系统调用,都通过这个编号来操作已打开的文件。调用成功时,函数会返回当前进程中编号最小、尚未被占用的文件描述符。

文件描述符(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] 组合上。

stdinstdoutstderr 通常分别是 fd 012

示例
# 执行指令:使用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.cmain.c 常见 fd 一般是 3。因为 012 已经分别用于标准输入、标准输出和标准错误,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 判断是否真的释放文件实体。

同一个文件可以在多个目录下拥有多个文件名,这类别名统称为链接(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_fopsfile->f_op

示例:FS Organization

假设:

  • block size = 4 KB
  • total blocks = 64
  • data region = 56 blocks
  • inode table = 5 blocks
  • 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 的地址偏移为:

\[ 4 KB + 8 KB + 8 KB = 20 KB \]

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
  • 连续 3 次 read() 读 bar 内容
    • 第 1 次read:读 bar-data0写 bar-inode(刷新文件访问时间 atime)
    • 第 2 次read:读 bar-data1写 bar-inode(再次更新 atime)
    • 第 3 次read:读 bar-data2写 bar-inode(再次更新 atime)

每次读文件内容,内核都要修改 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 和一致性