文件系统实现
概述
- 文件系统是纯软件实现
- 文件系统的两个重点:数据结构、访问方法
- 如何组织和存储元数据?数组、树等等
- 如何高性能的访问文件?
- 心智模型:研究文件系统时,聚焦于核心问题,而不要囿于代码的细节
- 下面讨论的都是简单文件系统(VSFS)
整体组织
-
数据区:最重要的部分,真实存储数据的区域
- 块大小:只用一种块大小,通常4KB
-
inode:文件系统必须记录每个文件的信息,包括:数据块、文件的大小、所有者和访问权限、访问和修改时间等等。因此必须有一块区域存放这些元数据
- VSFS使用数组存放
-
记录数据块和inode是否空闲:使用data bitmap和inode bitmap
-
记录磁盘总体信息:使用Superblock块,记录:文件系统中的剩余inode、数据块数目,inode表的起始地址,幻数(标志文件系统类型)等等
-
长这样:
文件组织:inode
-
文件的低级名称:简单文件系统中inode存在数组中,则一个inumber(类似数组下标,但是经过平移)对应一个inode,inumber即低级名称(因为inumber和文件一一对应)
- 可以根据inumber计算出inode的地址
-
inode的内容
-
文件类型(例如,常规文件、目录等)
-
大小
-
分配给它的块数
-
保护信息(如谁拥有该文件以及谁可以访问它)
-
一些时间信息(包括文件创建、修改或上次访问的时间)
-
有关其数据块驻留在磁盘上的位置的信息(如某种类型的指针)
-
ext2的inode:存的东西还是很多的
-
-
如何存储大文件
- 多级索引:inode中有一部分索引是直接指针,指向数据区的某些块;还有间接指针,指向包含更多指针的块,每个指针指向数据
- 双重间接指针:多套一层,可以支持更大的文件范围!
- 实现:不平衡树
- 原理:大多数文件很小
- 基于范围的方法:不需要指向每个块的指针,而是记录起始地址 + 长度
- 不够灵活,但是更紧凑,适用大文件
- 基于链接的方法:inode只存一个指针,然后每个数据块的末尾链接下一个数据块
- 缺点:类似链表,访问尾部、随机访问等等操作难以进行
- 改进:不把指针信息存在数据块的末尾,而是存在内存的高速缓存中
- 多级索引:inode中有一部分索引是直接指针,指向数据区的某些块;还有间接指针,指向包含更多指针的块,每个指针指向数据
目录组织
-
目录也是一种文件!因此完全不影响文件系统的组织,只不过存储的内容稍有不同:
-
VSFS:基本只包含一个二元组(文件名,inode号)。可能还包含一些别的参数,如文件名的长度等等,如下:
-
删除文件:会留下一个空白的条目,因此会有一些方法来标记它(如将inode设为0表示空闲)
-
目录数据的结构:不一定非要是数组,也可以是树或其他的
- XFS:B树,文件创建操作更快(因为需要搜索目录,确保文件名没有冲突)
-
空闲空间管理
- 位图(最棒!)
- 空闲列表:早期文件系统使用的方法,即链表。超级块中保存第一个空闲块的指针,然后每个空闲块都链接下面的空闲块
- 预分配策略:创建文件的同时分配一小部分数据块(因为创建文件的下一步大概率就是写),保证部分连续,提高性能
访问路径:读取和写入
-
所有的遍历都从根开始,因此系统需要知道根的inode号,一般为2
- 0:保留作为空闲inode的标识
- 1:跟踪所有的坏块(特殊用途)
-
open函数遍历的过程:读目录文件的inode -> 读目录文件的data -> 读下层目录的inode -> …. -> 找到最终要读的文件的inode号
-
下一步,取inode信息,对用户权限进行检查。如果没有问题,修改进程的打开文件表,分配文件描述符
-
read读取内容后会修改文件的偏移量(在文件描述符中)
-
例子:
-
缺点:io次数与路径长度成正比;对于大型目录,可能需要读取很多数据块才能定位到需要的条目
- 优化:B树!
-
-
write写入磁盘的过程:一般都需要分配新数据块,故需要多次io
- 读、写data的bitmap
- 读、写inode的bitmap(修改数据块的指针)
- 真正写入数据块本身
- 如何降低io的成本?引入高速缓存!
缓存和缓冲
-
缓冲资源划分方法
-
静态划分:给定固定大小的内存用作缓存
- 优点:确保有一定的资源可用,提供可预测的性能,实现简单
- 缺点:可能导致浪费
-
动态划分:虚拟内存页面和文件系统页面集成到统一页面缓存中,按需分配
- 缺点:实现可能比较复杂,可能导致空闲资源被占用,需要使用时花费较长时间回收
-
-
为什么要缓存
- 读操作可以得到极大的优化
- 第一次读文件时可能需要很多io,但随后如果读取同一目录下的文件,则搜索路径的过程大部分命中缓存,不需要io
- 写入流量不会减少(因为数据确实都要写入磁盘),但是可以将多个io积累为一批写入,同时部分写操作可能会因为拖延而完全避免
- 缺点:考虑系统崩溃导致缓存没有写入的问题
- 读操作可以得到极大的优化
-
特殊情况:某些应用程序(如数据库)不希望写缓冲,可以通过调用fsync()绕过缓存直接io,或者使用原始磁盘接口从而完全避免使用文件系统