文件系统定义

  • 操作系统中实现文件统一管理的一组软件和相关数据的集合
  • 负责组织、存储、检索、共享和保护文件信息

文件系统功能

  • 实现”按名存取”,将用户提供的文件名映射为物理存储位置
  • 提供方便的操作和统一的调用接口,屏蔽底层存储设备细节
  • 组织、分配、回收文件的存储空间
  • 负责文件的存储、检索、共享和保护

文件系统分层架构

文件系统通常采用层次化设计,各层分工明确:

  1. 用户接口层:提供系统调用接口(open/read/write/close 等)
  2. 目录管理层:解析路径名,定位文件目录项
  3. 访问控制层:验证用户权限,确保安全性
  4. 逻辑文件系统层 + 缓冲区管理层:将用户请求转换为逻辑块号,管理文件缓冲区
  5. 物理文件系统层:将逻辑块号转换为物理块号,与设备驱动交互
  6. 设备驱动层:直接与硬件设备(磁盘、SSD)通信,完成实际的 I/O 操作

分层优点:模块化、易于维护和扩展,各层可独立实现和优化。

传统文件系统调用

创建 / 删除

  • create(filename, attributes)
    • 检查文件是否已存在(遍历目录)
    • 分配一个新的 FCB(inode)
    • 将 FCB 写入磁盘
    • 更新目录项,添加新文件名到父目录
    • 参数:文件名(路径名)、设备名(卷名)、FCB 信息(权限、类型等)
  • delete(filename)
    • 检查文件是否存在及权限
    • 释放文件占用的所有数据块(回收至空闲空间管理)
    • 释放 FCB(inode)
    • 从目录中删除对应的目录项
    • 如果是硬链接且链接数 > 1,只减少链接计数,不删除数据

打开 / 关闭

  • open(filename, flags)
    • 根据文件名搜索目录,找到对应的 FCB
    • 将 FCB 从磁盘读入内存(到系统级打开文件表)
    • 创建文件描述符(fd)或文件句柄,返回给用户进程
    • 建立用户与文件的连接,维护文件的读写位置指针
    • 文件描述符:一个整数索引,指向进程级打开文件表中的表项
  • close(fd)
    • 写回 FCB 到磁盘(更新文件大小、修改时间等)
    • 释放进程级打开文件表中的表项
    • 递减系统级打开文件表中的引用计数,引用计数为 0 时释放并写回 FCB

读 / 写

  • read(fd, buf, count)
    • 根据文件描述符找到对应的打开文件表项
    • 从当前读写位置开始读取 count 字节
    • 通过文件系统的映射机制找到数据所在的磁盘块
    • 将数据读入缓冲区(buf),然后拷贝到用户空间
    • 更新文件读写位置指针
  • write(fd, buf, count)
    • 类似读取,但方向相反
    • 如果当前块写满,需分配新的磁盘块
    • 更新文件大小和修改时间
    • 更新读写位置指针
  • lseek(fd, offset, whence):定位读写位置,实现随机访问

文件描述符与打开文件表

文件描述符是进程和打开文件之间的桥梁。

  • 每进程打开文件表(进程级)
    • 每个进程维护自己的打开文件表
    • 表项包含:文件描述符、指向系统级打开文件表项的指针
    • fork() 时,子进程继承父进程的打开文件表
  • 系统级打开文件表(全局)
    • 整个系统共享一张打开文件表
    • 表项包含:文件读写位置指针、打开模式、引用计数、FCB 指针(指向内存中的 inode)
    • 多个文件描述符可以指向同一个系统级表项(如 dup() 复制文件描述符时)
  • 内存 inode 表
    • 存储从磁盘读入内存的 inode(FCB)
    • 多个系统级表项可共享同一个内存 inode(如硬链接打开的文件)
    • 当引用计数为 0 时,回写并释放

文件缓冲区管理

  • 引入缓冲区的目的:缓解 CPU 与 I/O 设备之间的速度差异,减少物理 I/O 次数
  • 预读(Read-Ahead):程序顺序读取文件时,系统预测性地提前将后续块读入缓存
    • 基于”局部性原理”:如果程序读取第 n 块,很可能接下来读取第 n+1 块
    • 每次读入更大块(如 8KB 或 16KB),减少 I/O 次数
  • 写缓冲(Write Buffer / Delayed Write):写操作先写入内存缓冲区,延迟写入磁盘
    • 避免每次 write() 都触发磁盘 I/O
    • 缓冲区满或显式调用 sync() / fsync() 时真正写入磁盘
    • 缺点:系统崩溃可能导致数据丢失(文件系统日志 / Journaling 可缓解此问题)

UNIX inode(索引节点)

  • inode 是 UNIX/Linux 文件系统的核心数据结构,存储文件的全部元数据(FCB 信息)
  • 文件名不存储在 inode 中,而是存储在目录文件中,使得同一个 inode 可以有多个文件名(硬链接)
  • inode 包含以下关键字段:
    • 文件类型与权限(st_mode
    • 文件链接计数(st_nlink
    • 文件所有者和所属组(st_uid, st_gid
    • 文件大小(st_size
    • 时间戳(st_atime, st_mtime, st_ctime
    • 数据块指针数组(直接块 + 间接块指针)

混合索引结构(UNIX)

UNIX 采用混合索引结构来管理文件数据块的地址,兼顾小文件和大文件:

  • 索引节点中共有 13 个块指针(以传统 UNIX V7 和早期 Linux ext2 为例):
    • 10 个直接块指针:直接指向数据块
    • 1 个单级间接块指针:指向一个索引块,该索引块包含若干数据块指针
    • 1 个二级间接块指针:指向索引块→索引块→数据块
    • 1 个三级间接块指针:指向索引块→索引块→索引块→数据块

最大文件大小计算(以 1KB 块大小为例)

  • 每个索引块(间接块)可存放 1024 / 4 = 256 个块指针(假设指针大小 4 字节)
  • 直接块:10 × 1KB = 10KB
  • 单级间接:256 × 1KB = 256KB
  • 二级间接:256 × 256 × 1KB = 64MB
  • 三级间接:256 × 256 × 256 × 1KB = 16GB
  • 最大文件大小 ≈ 16GB + 64MB + 256KB + 10KB ≈ 16.06GB

不同块大小的最大文件大小:

块大小最大文件大小
1KB~16GB
2KB~256GB
4KB~4TB

(实际 ext4 等现代文件系统使用 extent 树代替了简单的混合索引,支持更大文件)

为什么采用混合索引?

  • 适应不同大小的文件
    • 小文件(< 10KB):只需使用直接块,无需额外索引块,开销极小
    • 中等文件:通过单级或二级间接,无需三级间接,节省索引块
    • 大文件:通过多级间接,理论上支持 TB 级大文件
  • 空间与性能的权衡:多数文件很小(据统计,大部分文件 < 10KB),混合索引为小文件优化
  • 灵活性:文件动态增长时可逐级扩展索引结构,不需要预先分配

内存映射文件(Memory-Mapped File)

  • 另一种文件访问接口
  • 将文件的一部分或全部映射到进程的虚拟地址空间
  • 进程通过指针直接读写内存,操作系统自动将修改写回文件
  • 优点:无需 read/write 系统调用开销,简化编程,数据共享方便(多个进程映射同一文件)
  • 缺点:不适合超大文件(地址空间有限),映射区域大小需与页大小对齐

文件系统的组成

  • 卷管理:管理磁盘分区(卷),跟踪分区信息和超级块
  • 目录管理:维护目录层次结构,支持路径解析和目录操作
  • 文件空间管理:分配和回收磁盘块(空闲空间管理)
  • 文件操作实现:实现 create/delete/open/close/read/write 等系统调用
  • 文件保护与安全:访问控制、权限验证、加密等