文件系统实现

文件块索引结构

文件块索引:连续分配

  1. 分配连续的磁盘块给文件
    • 文件粒度分配
    • bitmap:找到N个连续的"0"
    • 链表:找到 size>=N 的区域
  2. 文件元数据
    • 记录第一个块的地址
    • 块的个数N
  3. 优点
    • 顺序访问性能高
    • 随机访问时定位数据块也较容易
  4. 缺点
    • 不知道文件最终多大,无论创建时,还是写数据块的时候
    • 文件难以变大
    • 外部碎片化

文件块索引:链表结构

  1. 分配不连续的磁盘块给文件
    • 块粒度分配
  2. 文件元数据
    • 记录第一个块的地址
    • 每个块指向下一个块的地址
    • 最后一个块指向NULL
  3. 优点
    • 无外部碎片,而且文件变大很容易
    • 空闲空间链表:与文件块类似
  4. 缺点
    • 随机方式性能极差:定位数据块需按指针顺序遍历链表
    • 可靠性差:一个坏块意味着其余的数据全部丢失
    • 块内要保存指向下一块的指针,有效数据大小不一定是2的幂次

文件块索引:文件分配表(FAT)

  1. 一张有N的项的表,假设磁盘有N块
    • 每个磁盘块有一个表项:要么为空,要么为该文件下一块的地址
    • 位于磁盘分区的头部
  2. 文件元数据
    • 记录第一块的地址:链表头指针
    • 每个磁盘块全部存数据,无指针
  3. 优点
    • 简单
    • 文件块为2的幂次
  4. 缺点
    • 随机访问性能不好
      • 定位数据需要查找FAT表
    • 浪费空间
      • 需要额外的空间存储FAT表

文件块索引:单机索引

  1. 文件元数据
    • 用户定义文件长度上限 max size
    • file header: 一个指针数组指向每个块的磁盘地址
  2. 优点
    • 文件在限制内可变大
    • 随机访问性能高:数据块直接定位
  3. 缺点
    • 不灵活,文件长度难以事先知道

文件块索引:两级索引

  1. 思路:
    • 采用可变段分配
    • 段内的块连续分配,段间运行不连续
  2. 文件元数据
    • 小文件有10个指针,指向10个可变长度段(base,size)
    • 大文件有10个间接指针,每个指向可变长度的间址块
  3. 优点
    • 支持文件变大
  4. 缺点
    • 段间碎片

文件块索引:多级索引(UNIX)

  1. 块粒度分配
  2. 文件元数据:13个指针
    • 10个直接指针
    • 第11个指针:一级间接指针
    • 第12个指针:二级间接指针
    • 第13个指针:三级间接指针
  3. 优点
    • 小文件访问方便
    • 支持文件变大
  4. 缺点
    • 文件有上限
    • 存在大量寻道

文件块索引:Extents

  1. Extent是若干个连续磁盘块(长度不固定)
    • 同一Extent中的所有块:要么多少空闲块,要么都属于某个文件
    • Extent:<starting block, length>
  2. XFS提出的方法
    • 无论文件块还是空闲块都采用Extents来组织
      • 块大小为8KB
      • Extent的大小 <= 2M个块
    • 文件块索引
      • 采用B+树,中间结点记录文件块号和子节点的磁盘块地址
      • 叶节点记录文件快好和其所属的Extent
    • 文件元数据
      • 记录B+树的根结点地址

目录项的组织结构

目录访问

  1. 路径解析
    • 在目录里查找指定目录项:文件名
  2. 修改目录
    • 创建/删除目录、创建/删除文件、硬链接、符号链接、重命名
    • 在目录里添加/删除目录项
  3. 读目录
    • 扫描目录内容

目录项的组织结构

  1. 线性表
    • 原理
      • <文件名,ino> 线性存储
        • 每一项不定长:<ino,名字长度,下一项起始偏移,名字>
      • 创建文件
        • 先查看有没有重名文件
        • 如果没有,在表末添加一个entry: <ino,newfile>
      • 删除文件
        • 用文件名查找
        • 删除匹配的entry
        • 紧缩:将之后的entry都向前移动
    • 优点
      • 空间利用率高
    • 缺点
      • 大目录性能差:线性查找,目录项数据从磁盘读取,磁盘的I/O多
      • 删除时的紧缩很耗时
  2. B+树
    • 原理
      • 在磁盘上使用B树索引目录的数据块
      • 用B树来存储<文件名,ino>,以文件名排序(字典序)
      • 创建/删除/查找:通过B树实现
    • 优点
      • 大目录性高:B树的查找减少磁盘的I/O
    • 缺点
      • 小目录不高效
      • 占用更多空间
      • 实现复杂
  3. 使用哈希表索引
    • 原理
      • 在VFS中使用哈希表索引目录项
      • 用哈希表将文件名映射到ino
        • hash_func(filename) -> hval -> 哈希桶
        • 在哈希桶中线性查找 filename
      • 创建/删除需要分配/回收空间
    • 优点
      • 简单
      • 查找速度快
    • 缺点
      • 哈希表空间不好估计
        • 表较大浪费空间
        • 表小容易产生大量哈希冲突

创建文件或目录

例子:创建文件/home/os22/fs02.ppt

  1. 解析父目录"/home/os22",得到其ino,假设为100
  2. 读取其i-node,检查用户是否具有创建的权限
  3. 根据i-node,读取父目录的内容
  4. 查找是否已经存在名字为"fs02.ppt"的目录项
  5. 如果找到,同时flag为目录,则返回失败,否则转11
  6. 为"fs02.ppt"分配一个空闲的inode,假设其ino为116
  7. 填充inode的内容ino,size,uid,gid,ctime,mode...
  8. 在父目录的内容中添加一个目录项<"fs02.ppt",116>
  9. 修改父目录的inode:size,atime,mtime
  10. 把修改写到磁盘"fs02.ppt"的inode、父目录的inode、父目录内容
  11. 创建一个打开文件结构,指向"fs02.ppt"的inode
  12. 分配一个空闲的打开文件结构指针,指向打开文件结构
  13. 返回指针的数组下标

删除文件或目录

例子:删除文件/home/os22/fs02.ppt

  1. 路径解析父目录"/home/os22",得到其ino为100
  2. 读取其i-node,检查用户是否具有删除的权限
  3. 根据i-node,读取父目录的内容
  4. 查找是否已经存在名字为"fs02.ppt"的目录项
  5. 如果不存在,则返回失败
  6. 得到"fs02.ppt"的ino为116
  7. 如果nlink为1,则释放inode及文件块,否则nlink-1
  8. 在父目录的内容中删除目录项<"fs02.ppt",116>
  9. 修改父目录的inode:size,atime,mtime
  10. 把修改写到磁盘:inode、父目录inode、父目录内容、空闲块
  11. 返回

文件缓存

例如:解析路径"/home/foo",并读/写,会产生大量I/O

  1. 读根目录的i-node和它的第一块
  2. 读home目录的i-node和它的第一块
  3. 读foo文件的i-node和它的第一块 / 创建文件foo并写其第一块

文件缓存(File buffer cache/page cache)

  1. 使用内核空间的一部分内存来缓存磁盘块
  2. 读操作read():先检查该块是否在缓存中
    • 在:将缓冲块的内容拷贝到用户buffer
    • 不在:
      • 分配一个缓存块(可能需要替换)
      • 把磁盘块读到缓冲
      • 再把缓存块拷贝到用户buffer
  3. 写操作write():先检查该块是否在缓存中
    • 在:将用户buffer的内容拷贝到缓冲块
    • 不在:
      • 分配一个缓存块(可能需要替换)
      • 把用户buffer的内容拷贝到缓存块
    • 将该缓存块写回磁盘(根据缓存管理策略)
  4. 缓存设计问题
    • 缓存什么、缓存大小、何时放进缓存、替换谁、写回策略

缓存什么

  1. 不同类型的块
    • i-nodes
    • 间址块
    • 目录
    • 文件块

缓存大小

  1. 文件缓存与进程使用的虚存竞争有限的内存空间
  2. 两种方法
    • 固定大小:用特权命令设置文件缓存大小
    • 可变大小:
      • 文件缓存和VM都按需申请内存:页替换
      • 文件缓存大小不可控

替换谁

  1. 为什么缓存位于内核空间
    • DMA
      • DMA数据传输
    • 多用户进程
      • 共享缓存
  2. 通常的替换策略
    • 全局LRU
    • 进程工作集

何时放进缓存

  1. 何时放进缓存:按需取 vs 预取
  2. 文件访问具有局部性
    • 时间局部性
    • 空间局部性
  3. 最优
    • 在要用之前搞好预取进来
  4. 通常的策略
    • 针对顺序访问的预取:访问第i块时,预取随后的k个块
      • 文件块尽量分配连续的磁盘块
      • Linux采用的方法
    • 针对inode的预取:在读取目录项时,同时读取对应的inodes
  5. 高级策略
    • 预取同一目录下所有的小文件

写回策略

  1. 写操作
    • 数据必须写到磁盘才能持久化
  2. 缓存中的数据何时写到磁盘上
    • Write Through
      • 每个写操作,不仅更新缓存块,而且立即更新磁盘块
      • 好处:简单&可靠性高,最新数据都落盘
      • 坏处:磁盘写没有减少
    • Write Back
      • 每个写操作,只更新缓存块,并将其标记为脏块
      • 之后再将它写到磁盘
      • 写操作快 & 减少磁盘写:缓存吸纳多次写,批量写磁盘
  3. 写回的复杂性
    • 丢数据
      • 宕机时,缓存中的“脏”数据将全部丢失
      • 推迟写磁盘 -> 更好的性能,但损失更大
    • 什么时候写回磁盘
      • 当一个块被替换出缓存时
      • 当文件关闭时
      • 当进程调用fsync时
      • 固定的时间间隔(Unix是30s)
    • 问题
      • 执行写操作的进程并不知道数据什么时候落盘了
        • 通过fsync:用户显式写回数据
      • 不能保证不丢数据:宕机或掉电可能发生在任何时候

文件系统 vs 虚存

  1. 相似点
    • 位置透明性:用户不感知物理地址
    • 大小无关性:固定粒度分配(块/页),不连续分配
    • 保护:读/写/执行权限
  2. FS 比 VM 容易的地方
    • FS的地址转换可以慢
    • 文件比较稠密(空洞少),经常是顺序访问
    • 进程地址空间非常稀疏,通常是随机访问
  3. FS 比 VM 难的地方
    • 路径解析可能引入多次I/O
    • 文件缓存的空间(内存)总是不够的
    • 文件大小差距大:很多不足10KB,有些又大于GB
    • FS的实现必须是可靠的

虚存页表 vs 文件块索引

  1. 页表
    • 维护进程地址空间与物理内存的映射关系
    • 虚页号 -> 物理页框号
    • 查检访问权限、地址合法性
    • 硬件实现地址转换,如果映射关系在TLB中,很快完成转换
  2. 文件块索引
    • 维护文件块与磁盘块之间的映射关系
    • 文件块号 -> 磁盘逻辑块号
    • 查检访问权限、地址合法性
    • 软件(OS)实现地址转换,可能需要多次磁盘I/O

FFS(Unix文件系统)

最初的 Unix FS

  1. 简单的磁盘布局
    • 文件块大小 = 扇区大小 = 512B
    • i-node区在前,数据区灾后
    • 空闲块/inode链表:Superblock中记录头指针
  2. 文件块索引采用三级间指,目录采用线性表
  3. 存在的问题
    • 带宽很低:顺序访问只有20KB/s,即2%的磁盘带宽

导致带宽低的原因

  1. 数据块的存储位置
    • 数据块存储在内层的柱面
    • inode 存储在外层的柱面
  2. 频繁长距离寻道
    • inode 与其数据块离得很远
    • 同一目录里的文件,其inode也离得很远
    • 一个文件的数据块散布在磁盘上任意位置
      • 即使顺序读写文件 -> 随机磁盘 I/O
  3. 未考虑给文件分配连续磁盘块
    • 空闲块采用链表组织
    • 链表上相邻的块,其物理地址不连续
  4. 小粒度访问多
    • 采用512B的小块
    • 无法发挥磁盘带宽

BSD FFS(Fast File System)

  1. 大文件块:4KB/8KB vs 512B
    • 数据块大小记录在Superblock中
    • 空间利用率问题
      • 小文件
      • 大文件的最末一块可能非常小
    • FFS的解决办法
      • 数据块划分为若干更小的子块
      • 子块为512B,每块8/16个子块
  2. Bitmap:取代空闲块链表
    • 尽量连续分配
    • 预留 10% 的磁盘空间

FFS的磁盘布局

  1. 柱面组(Cylinder Group)
    • 柱面:所有盘片上半径相同的磁道构成一个柱面。
    • CG:每N个连续的柱面为一个CG
    • 把磁盘划分为若干柱面组,将文件和目录分散存储于每个柱面组
    • 每个CG类似于一个 Sub FS
      • 包含Superblock,空闲inode bitmap,空闲块bitmap,inode表,数据块

FFS的放置策略

  1. 减少长距离寻道
    • 原则:把相关的东西放在同一CG
  2. 目录放置
    • 选择CG:目录个数少 & 空闲inode个数多 & 空闲块多
  3. 文件放置
    • 文件inode选择其目录所在的CG
    • 文件块选择其inode所在的CG
      • inode与文件块一起读的概率是只读inode的4倍
  4. 大文件处理
    • 应避免它占满一个CG
    • inode所在CG:存放前10个磁盘块(直接指针指向)
    • 每个间址块及其指向的块放在同一CG(4MB)
    • 不同间址块及其指向的块放在不同CG

FFS的效果

  1. 性能提升
    • 读写性能和CPU利用率提升
    • 小文件性能提升
  2. 进一步的优化空间
    • 块粒度分配和多级索引,大文件访问不高效
      • 用 Extent 来描述连续的数据块
    • 元数据采用同步写,影响小文件性能
      • 异步写,并保证一定的顺序
      • 日志