小林coding 《图解系统:文件系统》笔记
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:
- 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
- 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的
/proc
和/sys
文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。 - 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。
VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。
文件系统
文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息:inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。
总之,除了文件名以外的所有文件信息,都存在inode之中(inode 在磁盘中)。
可以用stat命令,查看某个文件的inode信息:
硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定。
由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。
每个inode都有一个号码,操作系统用inode号码来识别不同的文件。
Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。
目录也是一种文件
Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。
目录文件的结构非常简单,就是一系列目录项(dirent)的列表。
每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。通过这个 inode,就可以找到真正的文件。
目录查询是通过在磁盘上反复搜索完成,需要不断地进行 I/O 操作,开销较大。所以,为了减少 I/O 操作,把当前使用的文件目录缓存在内存。
表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:文件名 ——>对应的inode号码——>inode信息——>文件数据所在的block
读取文件过程
- 首先用
open
系统调用打开文件,open
的参数中包含文件的路径名和文件名。 - 使用
write
写数据,其中write
使用open
所返回的文件描述符,并不使用文件名作为参数。 - 使用完文件后,要用
close
系统调用关闭文件,避免资源的泄露。
我们打开了一个文件后,操作系统会跟踪进程打开的所有文件,所谓的跟踪呢,就是操作系统为每个进程维护一个打开文件表,文件表里的每一项代表「文件描述符」,所以说文件描述符是打开文件的标识。
操作系统在打开文件表中维护着打开文件的状态和信息:
- 文件指针:系统跟踪上次读写位置作为当前文件位置指针
- 文件打开计数器:多个进程可能打开同一个文件,该计数器跟踪打开和关闭同一文件的进程数量,当该计数为 0 时,系统关闭文件,删除该条目;
- 文件磁盘位置:保存在内存中,以免每个操作都从磁盘中读取;
- 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等)
用户习惯以字节的方式读写文件,而操作系统则是以数据块来读写文件,那屏蔽掉这种差异的工作就是文件系统了。
我们来分别看一下,读文件和写文件的过程:
- 当用户进程从文件读取 1 个字节大小的数据时,文件系统则需要获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分。
- 当用户进程把 1 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘。
所以说,文件系统的基本操作单位是数据块。
文件的存储
磁盘读写的最小单位是扇区,扇区的大小只有 512B
大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。
所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB
,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。
数据块在磁盘上的存放方式有以下两种:
- 连续空间存放方式
- 非连续空间存放方式
其中,非连续空间存放方式又可以分为「链表方式」和「索引方式」。
那早期 Unix 文件系统是组合了前面的文件存放方式的优点,如下图:
最前面那块就是 inode
它是根据文件的大小,存放的方式会有所变化:
- 如果存放文件所需的数据块小于 10 块,则采用直接查找的方式;
- 如果存放文件所需的数据块超过 10 块,则采用一级间接索引方式;
- 如果前面两种方式都不够存放大文件,则采用二级间接索引方式;
- 如果二级间接索引也不够存放大文件,这采用三级间接索引方式;
所以,这种方式能很灵活地支持小文件和大文件的存放:
- 对于小文件使用直接查找的方式可减少索引数据块的开销;
- 对于大文件则以多级索引的方式来支持,所以大文件在访问数据块时需要大量查询;
空闲空间管理
- 空闲表法:为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个空闲表是连续分配的。如果存储空间中有着大量的小的空闲区,则空闲表变得很大,这样查询效率会很低。
- 空闲链表法:每一个空闲块里有一个指针指向下一个空闲块。只要在主存中保存一个指针,令它指向第一个空闲块。其特点是简单,但不能随机访问。
空闲表法和空闲链表法都不适合用于大型文件系统,因为这会使空闲表或空闲链表太大。
- 位图法:利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。0 表示对应的盘块空闲,1 表示对应的盘块已分配。
块组
文件读写的最小单位是块,按照块存放的数据类型划分,有不同类型的块:
- inode(index node)索引块:每个文件都有一个inode来唯一标志文件
- 数据块:包含文件的有用数据。
- 数据位图块 和 inode位图块:用于表示对应的数据块或 inode 是空闲的,还是被使用中。
- 超级块:包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。
- 块组描述符:包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。
基于inode的文件共享(硬链接)
一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。
ln 命令可以创建硬链接:ln 源文件 目标文件
基于文件名的文件共享(软链接)
软链接相当于重新创建一个软链接文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的文件名,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已,打开文件就会报错:"No such file or directory"。
这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。
ln -s 命令可以创建软链接:ln -s 源文文件或目录 目标文件或目录。
inode 的特殊作用
由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。
1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。
第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。