7.1 文件系统全家桶
文件系统的基本组成
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
Linux的目录项和目录不是一个东西,目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存,用于提高效率。
文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。
虚拟文件系统
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
文件的使用
读取一个文件的过程:
- 首先用
open系统调用打开文件,open的参数中包含文件的路径名和文件名。 - 使用
write写数据,其中write使用open所返回的文件描述符,并不使用文件名作为参数。 - 使用完文件后,要用
close系统调用关闭文件,避免资源的泄露。
操作系统在打开文件表中维护着打开文件的状态和信息:
- 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的;
- 文件打开计数器:因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目;
- 文件磁盘位置:问了满足修改文件数据方便罗盘;
- 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等);
文件的存储
有以下两种:
- 连续空间存放方式
- 非连续空间存放方式
- 分为「链表方式」和「索引方式」
连续空间存放方式
这种模式下,文件的数据都是紧密相连,读写效率很高,因为一次磁盘寻道就可以读出整个文件。
文件头里需要指定「起始块的位置」和「长度」,有了这两个信息就可以很好的表示文件存放方式是一块连续的磁盘空间,才能方便后续寻找。
- 缺陷:磁盘空间碎片,删除的文件空间不足以存放新文件,被浪费掉了(可以通过空间整理解决,但效率低)
- 文件长度不易扩展:文件想要扩展长度,因为连续空间存放不会预留空间,唯一的办法就只能是挪动的方式,效率低
非连续空间存放方式
链表
存放是离散的,不用连续的,可以消除磁盘碎片,文件的长度可以动态扩展
链表可分为「隐式链表」和「显式链接」两种形式。
- 隐式链表:文件头包含头尾指针,每个数据块通过链表连接,问题是访问需要通过遍历获取,如果指针丢失或损坏会导致文件数据丢失
- 显示链接:将每个数据块的指针用链接表保存,这个表放在内存中,称为文件分配表(File Allocation Table,FAT)
- 直接存放大文件的全部链接造成分配表过大,所以会采用多级索引的方式
Unix 文件的实现方式
那早期 Unix 文件系统是组合了前面的文件存放方式的优点,如下图:

它是根据文件的大小,存放的方式会有所变化:
- 如果存放文件所需的数据块小于 10 块,则采用直接查找的方式;
- 如果存放文件所需的数据块超过 10 块,则采用一级间接索引方式;
- 如果前面两种方式都不够存放大文件,则采用二级间接索引方式;
- 如果二级间接索引也不够存放大文件,这采用三级间接索引方式;
那么,文件头(Inode)就需要包含 13 个指针:
- 10 个指向数据块的指针;
- 第 11 个指向索引块的指针;
- 第 12 个指向二级索引块的指针;
- 第 13 个指向三级索引块的指针;
所以,这种方式能很灵活地支持小文件和大文件的存放:
- 对于小文件使用直接查找的方式可减少索引数据块的开销;
- 对于大文件则以多级索引的方式来支持,所以大文件在访问数据块时需要大量查询;
空闲空间管理
几种常见的方法:
空闲表法
- 建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数
- 每次使用,扫描表里的内容,找到一个合适的空闲区域
- 释放时也要扫描填入
- 如果空闲区少可以用,如果存储空间中有着大量的小的空闲区,则空闲表变得很大
- 适用于建立连续文件
空闲链表法
- 只要在主存中保存一个指针,令它指向第一个空闲块
- 当创建文件需要一块或几块时,就从链头上依次取下一块或几块。反之,当回收空间时,把这些空闲块依次接到链头上。
- 也不适合用于大型文件系统,空闲链表会很大
位图法
- 利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。
- 是 Linux 文件系统采用的
目录的存储
普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
在目录文件的块中,最简单的保存格式就是列表。
但是如果一个目录有超级多的文件,我们要想在这个目录下找文件,按照列表一项一项的找,效率就不高了。
所以Linux 系统的 ext 文件系统哈希表,对文件名进行哈希计算,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。
这种方法的优点是查找非常迅速,插入和删除也较简单,不过需要一些预备措施来避免哈希冲突。
目录会放在内存中,方便检索。
软链接和硬链接
Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link) 的方式来给某个文件取个别名。
硬链接是文件系统中一个目录项对文件的引用。每个文件至少有一个硬链接,即它的文件名。
- 硬链接相当于存放了文件的地址
- 删除或移动原始文件不会影响硬链接的访问(只是相当于删除了一个对象引用)。
- 硬链接是不可用于跨文件系统的。(每个系统的具体文件inode时不同的)
- 删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接的内容是另外一个文件的路径名,所以访问软链接的时候,实际上相当于访问到了另外一个文件
- 软链接是可以跨文件系统的(因为每个系统的文件名可以相同)
- 目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。(死链接)
文件 I/O
- 缓冲与非缓冲 I/O
- 直接与非直接 I/O
- 阻塞与非阻塞 I/O VS 同步与异步 I/O
缓冲与非缓冲 I/O
根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O:
- 缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
- 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
- 可能出现部分数据写入的情况
这里所说的「缓冲」特指标准库内部实现的缓冲。
直接与非直接 I/O
根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
- 直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘。
- 非直接 I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。
- 默认非直接 I/O
如果用了非直接 I/O 进行写数据操作,内核什么情况下才会把缓存数据写入到磁盘?
以下几种场景会触发内核缓存的数据写入磁盘:
- 在调用
write的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上; - 用户主动调用
sync,内核缓存会刷到磁盘上; - 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
- 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上;
阻塞与非阻塞 I/O VS 同步与异步 I/O
- 阻塞 I/O:执行read,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,
read才会返回。(两阶段)- 会轮训内核是否准备好,所以出现了 I/O多路复用
- 非阻塞 I/O:read 请求在数据未准备好的情况下立即返回,数据准备好,通知内核将数据拷贝到应用程序缓冲区,
read调用才可以获取到结果。(拷贝阶段同样会阻塞,等待拷贝完成) - 两种都是通过 I/O 需要内核拷贝数据到程序缓冲区,所以效率不高
- 异步 I/O 是内核准备数据和数据拷贝到程序缓冲区都不需要等待。
当我们发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图:

