文件系统
约 3200 个字 17 张图片 预计阅读时间 11 分钟
为什么Linux中一切皆文件?
在Linux中,无论是硬件设备(硬盘、键盘、显卡)、网络连接(Socket)、还是进程间通信(Pipe),都被抽象成了一个“文件”,可以用一套统一的接口调用,这么设计是为了api的简洁性和一致性。
接口
简介
- File System is the way that controls how data is stored and retrieved in a storage medium.
- File is a sequence of bits,bytes,lines, or records.
文件
文件结构
这里说的实际上是文件的逻辑结构,后面还会讨论文件的物理存储结构
- None:sequence of words,bytes
- Simple record structure:
- Lines
- Fixed length
- Variable length
- Complex Structures
- Formatted document
- Reloacatable load file
文件的结构由操作系统和应用软件定义。
文件属性
- 名称:人类可读
- 标识符:系统内的唯一标识符
- 类型:在支持不同类型文件的系统中需要
- 位置:指向文件位置的指针
- 大小:当前文件的大小
- 保护:谁可读、可写、可执行
- 时间、日期和用户标识符
文件的信息被存储在目录结构中,维护在磁盘上。
文件操作
文件是一种抽象的数据类型,包含以下方法:
- Create
- Write
- Read
- Reposition within file
- Delete
- Truncate
- Open(Fi)
- Close(Fi)
打开文件
Open( )系统调用返回了一个指向打开文件表(open-file-table)中的项的指针:
- 每个进程的打开文件表:包含当前的文件指针和访问权限等内容
- 系统打开文件表:打开数等
需要以下信息来管理打开的文件:
- 文件指针(File Pointer):指向文件最后一次读写的位置
- 文件打开数(File-open count):当前文件被打开的次数
- 文件在硬盘上的位置(Disk location of the file):缓存数据访问信息,因为不是所有操作都需要从磁盘读取
- 访问权限(Access right):每个进程访问模式信息
从上面的内容不难看出,当有多个进程打开同一个文件时,是存在冲突的,所以部分操作系统和文件系统提供了文件锁(Open File Locking)来协调多个进程对文件的访问:
- 共享锁
- 互斥锁
锁还可以分为:
- 强制(mandatory):windows的做法,不满足持有锁则会被禁止访问
- 建议(advisory):linux的做法,不满足持有锁,仍自行决定是否访问
文件类型
扩展名只能起到提示应该拿什么软件处理这个文件的作用,也可以被看做文件名的一部分,所以之后的讨论都忽略扩展名
- MS-DOS
- MAX OS X:每个文件都拥有一个创建者属性,其中包含创建它的程序名
- Unix:使用魔数(magic number),但不是所有文件都有魔数,所以不能拿魔数来判断文件类型
访问方法
- 线性访问(sequencial access)
- 随机访问(random/direct access)
文件组织
目录
目录可以被看做用于翻译文件名到文件控制块(File Control Block, FCB)的符号表,目录和文件都在磁盘上。
典型的文件系统组织如下,磁盘上可以分出很多卷(Volumn),卷是逻辑存储单元,分区(Partition)是物理磁盘的划分

目录上的操作
- 搜索文件
- 创建文件
- 删除文件
- 列出目录内容
- 重命名文件
- 遍历目录
目录的目标
- 高效:快速定位一个文件
- 命名:方便用户
- 两位用户可以为不同的文件取同一个名字
- 同一份文件可以有多个不同的名字
- 分组:根据的文件的性质分组
不同的目录组织
单级目录
所有用户共用一个目录:

但这会引发命名问题和分组问题,不过lab6为了简化实现就是这么干的
二级目录
每个用户有不同的目录:

- 不同的用户可以有相同文件名
- 高效搜索
- 没有分组能力
树形目录

- 每个目录项包含了1 bit说明它是目录还是文件
- 高效搜索
- 分组能力
- 当前工作目录(current working directory)
- 绝对路径和相对路径
无环图目录

- 解决对于文件共享的要求,支持共享子目录和文件
- 支持两个不同名字(别名)
- 如果文件被删除可能会导致悬空指针(dangling pointer)
- 解决方案:维护文件的引用数,以便删除所有指针
- 新的目录类型
- 链接(link):已经存在的文件的另一个名字(指针)
- 解析链接:跟随指针去找到文件
通用图目录

如果允许环,会导致:
- 重复搜索同一个对象,因为遍历陷入了循环
- 文件删除问题,未被使用引用数也不为0
需要通过一些限制来避免环:
- 不允许硬链接到目录
- 垃圾回收
- 每当添加一个链接,都运行一次环检测算法
软链接和硬链接
- 软链接是一个独立的文件,其中存储到原始文件的路径;而硬链接是一个已经存在文件的别名,它会增加一个文件的链接数。
- 软链接有自己的inode,硬链接和原来的文件共用一个inode
- 软链接能够跨越不同的文件系统,因为它只是路径;硬链接不能创建到目录上,以免出现环
挂载
挂载(Mounting) 是指将一个存储设备(如硬盘分区、U盘、光盘等)的内容连接到文件系统中某个特定目录的过程。简单来说,硬件设备本身只是“一块砖”,挂载就是给它安一个“入口”,让你能通过文件夹看到里面的数据。

挂载并不会影响到原来目录中的内容,毕竟挂载的是其它存储设备,原本的存储设备是不受影响的。
文件共享
多用户共享
- User ID:用于标识用户,规定每位用户的权限
- Group ID:允许组中的用户访问资源
远程文件系统
使用网络使文件系统在不同系统中共享:
- 通过程序手动进行,比如FTP
- 自动无缝地进行,使用分布式文件系统
- 半自动地进行,使用world wide web
保护
可以将文件的权限修改为不可读、写和执行吗?
可以的,当然对于root用户来说无所谓,依然可以强行读取或写入该文件,也可以随时把权限改回来
- 文件的拥有者/创建者需要能够控制谁能够做什么,比如读、写和执行
- 可以创建不同的用户组,并将用户组绑定到文件上
实现
文件系统结构
文件系统位于二级存储(比如硬盘)之上,按照分层结构组织:
- 应用程序:发出读写filepath的指令
- 逻辑文件系统:控制文件的元信息,将读写filepath转化为读写逻辑块
- 文件组织模块:将逻辑块翻译为物理块
- 基础文件系统:缓存,如果未命中则调用I/O
- I/O控制:将读写指令翻译成更底层的磁盘读写指令

数据结构
磁盘上的数据结构:
- Boot control block(per volumn):虽然每个卷都有,但除了启动卷都为空
- Volumn control block per volumn(superblock in Unix)
- Directory structure per file system
- Per-file FCB(inode in Unix)
内存中的数据结构:
- In-memory mount table about each mounted volumn
- Directory cache
- System-wide open-file table
- Per-process open-file table
FCB
目录项和文件控制块的关系
在现代文件系统中,目录项可以看做是文件名+指向FCB的指针,但是在FAT32中目录项本身包含了FCB,所以不要被lab6“误导”了
文件控制块中保存了文件的信息

内存中的文件系统如下图所示:
- 打开文件:
- 用户态使用了
open(file name),执行系统调用trap到内核态 - 在内核态,先从磁盘上的目录取出目录项,再取出FCB
- 读文件:
- 用户态使用了
read(index),执行系统调用trap到内核态 - 在内核态,根据索引在进程的打开文件表中找到对应的文件(系统打开文件表的索引),然后根据系统打开文件表的信息(FCB)取出数据块

VFS
虚拟文件系统(Virtual File System)提供了一种面向对象的方式实现文件系统,也就是把具体文件系统再封装一层,以屏蔽不同文件系统的差异。具体而言,VFS允许同一个系统调用接口被用于不同类型的文件系统(多态)。

VFS的四个主要对象类型:
- superblock object:具体挂载的文件系统,对应到(但不等于)磁盘结构中的superblock
- inode object:一个特殊的文件,对应磁盘结构中的FCB
- dentry object:一个独立的目录项
- file object:和进程相关打开的文件相关,只要文件是打开的就一直存在
目录
- 线性表,文件名和指向对应数据库的指针:
- 易于编程
- 执行费时
- 哈希表:
- 减少目录搜索时间
- 可能存在哈希碰撞,可以选择链式冲突解决或者再哈希
分配方法
线性分配
按照块分配可能会导致支持的最大容量不足(如今的磁盘不少是TB级的),所以不少新的系统调整为了按照extent/cluster(多个连续的块)分配
线性分配(contiguous allocation): - 每个文件都占据磁盘上连续的块 - 简单,只需要起始地址(块号)和长度(块数) - 支持随机访问 - 浪费空间,因为随着不断地创建和删除会产生很多外部碎片 - 不支持文件增长

有一种方案可以支持增长,即在块后面加指针,指向另一块,更直接一点就是下面的链接分配。
链接分配
每个文件是磁盘块构成的链表,分配的块可以散布在磁盘中:
- 简单,只需要起始地址
- 能够管理空闲空间,不会造成空间浪费
- 不支持随机访问

在上图所示的链接分配中,每个索引块包含了指向下一个索引块的指针,也被称作隐式索引。微软在此基础上想到可以把链表的信息集中在一起,缓存到内存中,从而提高访问地址的速度,这个想法被称作文件分配表(file allocation table),又叫显式索引。这样做带来一个问题,要是FAT损坏了,那文件信息不就全部丢失了吗?解决方案是FAT实际会存储多份。
应该先写FAT还是先写数据?
应该先写数据,因为假设先写FAT,写完后立刻出错了,某些簇已经被标为占用,但数据还未写入,这会导致文件看起来存在,但某些内容全是磁盘上原有的“垃圾数据”。
索引分配
建立逻辑视图,存储虚拟的连续编号到真实分配的块序号的映射:
- 需要索引表
- 支持随机访问
- 没有外部碎片,但是存储索引块有开销
- 可以建立多级索引

这样对于小文件来说开销非常大,因为始终至少要多用一块去存索引;对于大文件来说,一个块中的索引可能不够用。为了实现大文件存储,有以下三种方案:
- 将索引块链起来,组成一个链表,显然这样已经支持无限大文件了
- 使用多级索引(类似多级页表)
- 混合方案(Linux实际使用)

double indirect索引中,假设每个块大小为4kiB,索引项大小为8B,请问最多能存多大的文件?
每个块能存4kiB/8B=512条索引,512*512*4kB=1GB
空闲空间管理
位图
每个bit代表磁盘上对应block的空闲状态,需要占用物理块存储

空闲链表
在内存池中见过,维护一个链表记录所有的空闲块

但是这样不利于寻找连续内存,所以有两个改进版:
- 将物理上连续的块视作一个整体加入到链表中,需要记录链表每个节点的块数
- 在空闲链表的第一个块中记录有哪些空闲块