首先声明作者暂时还没有阅读过linux源码本篇文章试图通过“大部分理论少部分代码”的形式为读者展开linux中VFS虚拟文件系统的神秘面纱。从磁盘眼中的文件到操作系统眼中的文件再到用户眼中的文件这篇文章都会给予较为完整自洽的解释。虽然没有硬核的源码分析但是作者相信读完这篇文章读者会感觉不虚此行磁盘中的文件磁盘的物理结构如上图磁盘由多个盘片组成每个盘片的正反两个盘面都是可以存储数据的并且每个盘面都对应一个磁头用于数据读写。需要注意的是这些磁头并不是独立移动的所有的磁头在传动臂的带动下一起移动是一个整体。单个盘面可以分为多圈磁道传动臂左右摆动起始就是在定位一个磁道每个磁道又可以分成多个多个扇区而扇区是磁盘读写的最小单位。所有盘面的相同半径的磁道可以组成一个抽象的曲面称作柱面。检验一下你是否理解了上述概念吧磁盘的容量 柱面(磁道)数 * 每个磁道的扇区数 * 每个扇区的字节数 *盘面(磁头)数磁盘寻址方式基本的磁盘寻址是CHS寻址法先确定哪一个柱面(cylinder)再确定哪一个磁头(head)再确定哪一个扇区(sector)这样就可以确定一个具体的扇区进行读写。然而如果OS内使用给出C、H、S的值的方式读写磁盘那么兼容性是很不好的因为我们知道外部存储设备不止是磁盘还有硬盘未来还会有新的存储设备而这些存储设备不见得都有柱面、磁头、扇区强行写死的话就要不断修改OS的代码。所以实际上OS永远只使用LBALogical Block Addressing寻址法就是把整个存储设备上所有可读写的扇区看做一个线性数组从 0 开始统一编号。OS只需要给出想要读/写的块号即可而具体逻辑块怎么映射到存储设备的一个存储单元由存储设备的驱动自己实现。接下来我们以磁盘为例讲一个存储设备如何实现LBA到具体存储单元的映射将每一个盘面上的每个磁道看做一维数组其中每一个元素代表本磁道上的一个扇区。一个柱面上有多个磁道就可以将整个柱面看做一个二维数组。而这个二维数组也可以展开成一个大的一维数组因此整个柱面的扇区可以看做一个大的一维数组。整个磁盘由多个柱面组成那么就可以将磁盘看做多张二维数组表也就是三维数组。同理也可以转换成一维数组。自此整个磁盘就被抽象为了一个一维数组而OS就把磁盘当做线性数组访问OS只需要给出想访问的下标磁盘自动把下标转换成CHS地址并访问相应位置如果换了别的种类的外部存储设备磁盘依旧使用下标访问只要让这个存储设备有相应的转换功能即可。这方便了OS的使用以及管理。LBA和CHS如何互相转换如上所述像访问一维数组一样访问整个磁盘。不过一个扇区大小一般是512B对于OS来说以扇区为单位和磁盘进行IO交互有些太少了我们都知道IO交互是比较耗费时间的根据局部性原理可以一次性多读一些数据。因此文件系统一般把一个块多个扇区作为OS一般把磁盘抽象出来的一维数组每8个扇区划分成一个“块”这个块的大小是8*512B即4KB每次IO操作都是以块为单位。如下图所示OS以块为基本单位进行IO操作并把块号转换成若干个LBA下标交给磁盘由磁盘进行扇区号和CHS地址的转换。所以OS访问磁盘总共经过两次转换。OS访问磁盘的基本单位如上所述OS像访问一维数组一样访问整个磁盘。不过一个扇区大小一般是512B对于OS来说以扇区为单位和磁盘进行IO交互有些太少了我们都知道IO交互是比较耗费时间的根据局部性原理可以一次性多读一些数据因此OS一般把磁盘抽象出来的一维数组每8个扇区划分成一个“块”这个块的大小是8*512B即4KB每次IO操作都是以块为单位。如下图所示OS以块为基本单位进行IO操作并把块号转换成若干个扇区号交给磁盘由磁盘进行扇区号和CHS地址的转换总共两次转换。磁盘上的文件系统磁盘上不仅需要保存文件本身还需要保存文件的组织信息否则你就不知道如何访问文件首先我们要知道并不是一整个磁盘只有一个分区我们可以选择性的把磁盘被分成几个分区每个分区都是完全独立互不影响的他们甚至可以安装不同的文件系统进行文件组织我们平时说的C盘D盘其实大概率只是同一块磁盘的不同分区罢了接下来我将把EXT4文件系统作为例子来介绍一个文件系统如何组织文件。文件系统组成EXT4文件系统把一个分区按组为单位划分每个分组内又包含分为若干个不同用途的部分。一旦文件系统被安装整个分区的所有结构就固定了包括每个分组的大小分组内的结构划分分组的存储块以及属性块个数等等总之一切都变成了固定的。类似于管理一个国家不同的分区代表不同的国家不同的组代表一个国家中不同的省这样划分开方便管理SuperBlock(超级块)数量超级块是文件系统的实体每个分区都有超级块存放在每个分区内固定的某一个或某几个分组的开头SuperBlock肯能存储多份原因是可以在文件系统受损后利用拷贝来恢复文件系统而不至于使系统直接崩溃。内容里面存储的是该分区使用的文件系统的信息以及整个分区的使用情况分组情况等。功能存放文件系统。BootBlock(引导块)数量每个磁盘只有一个引导块一般存放在磁盘的前几个扇区也就是根分区的最前面的位置内容它里面存放的是引导程序不讨论以及分区表整个磁盘分为哪几个分区每个分区的起止位置是什么云云等开机信息。功能大体来说在第一次使用磁盘的时候他会引导你分区安装文件系统等初始化操作。之后他就是记录分区等信息帮助OS初始化管理磁盘的数据结构。GDT(Group Descriptor Table)数量每个组都有存放在每个组内相对固定的位置。内容里面存储的是这个组的整体管理信息比方说该组多少块被使用还剩下的容量以及该分组的大小等信息。功能存放一个分组的管理信息。InodeTable数量每个组都有存放在每个组内相对固定的位置。内容里面存储的是该组所有文件的struct inode也就是每个文件的属性部分在linux中文件内容和属性是分开存储的。功能存放组中文件的属性。DateBlock数量每个组都有存放在每个组内相对固定的位置。内容这是一个分组中占比最大的部分这部分包含的是用来存储文件内容的存储块。功能存储数据。BlockBitmap数量每个组都有存放在每个组内相对固定的位置。内容里面存储的是一个位图每一位都作为相对位置代表DataBlock中的一个特定位置的存储块例如第0个bit代表的是DateBlock中的第一个存储块。功能表示本组中的块的使用情况使用了就把相应位置置1反之置0。InodeBitmap数量每个组都有存放在每个组内相对固定的位置。内容里面存储的是一个位图每一位都作为相对位置代表InodeTable中的一个特定位置的属性块例如第0个bit代表的是InodeTable中的第一个属性块即struct inode。功能表示本组中的InodeTable中struct inode的使用情况使用了就把相应位置置1反之置0。需要注意的是OS以块为基本单位进行IO所以一个DataBlock不是一个扇区而一般是4KB而若干个inode结构体因为单个体积太小所以挤在一起不是一个inode一个块属性块struct inode的内部结构如下注inode号i节点编号用来唯一的标识一个文件。块号用来唯一标识一个存储块。12个直接指针指向的是存放数据的DataBlock而间接块索引表指针指向的DataBlock不存放数据只存放其他DataBlock的块号以此来动态地增大一个inode所能管理的DataBlock的个数。细节问题看到这儿你可能已经明白整个文件系统是怎么回事了不过这里我还要补充一些细节inode号和块号如何是怎么得出的每个分组都设置不同的起始inode号和块号保存在GDT中inode号 本组起始inode号inode在InodeTable中的偏移量块号 本组起始块号块在DataBlock中的偏移量。示例如下一个文件最大的大小是一个分组中所有存储块大小的和吗远远不是由于起始号块号使得整个分区中的所有存储块的块号都是不同的这也就是说如果一个分组中的文件想使用另一个分组的存储块非常简单只需要读取另一个分组的BlockBitmap找到空闲块并给相应bit位置1然后把块号写入到文件的struct inode中维护映射即可。理论上说一个文件最大可以达到整个分区那么大。删除是将存储空间都置0吗不本质上删除就是把Bitmap的相应位置置0罢了至于存储块以及属性块的数据是不会动的因此才有数据恢复这一说。实际例子创建一个文件的过程首先确定自己在那个分区在该分区SuperBlock中的inode属性块是否用完。如果用完了那就创建失败否则进入下一步。遍历分区中所有组的GDT查看哪个组的InodeTable不为全1然后进入下一步利用本组中的InodeBitmap中找一个bit位为0的位置记作offset_1也就是找到一个空闲的struct inode并把该bit位置1表示已经占用。在本组中的InodeBitmap中找到第offset_1个struct inode并把想要创建的文件的属性写到这个属性块里面文件的inode号 分组起始inode号offset_1。利用本组中的BlockBitmap中找一个bit位为0的位置记作offset_2并把该bit位置1表示已经占用此时DataBlock中偏移为offset_2的块被使用将起始块号offset_2存入inode的作为该文件关联的一个数据块块号。在该DateBlock中写入文件数据。更新管理结构GDTSuperBlock等返回文件的inode号。读取一个文件的过程首先确定自己所在的分区。并拿到文件的inode号可以通过查看每个分组的起始inode号找到该inode号所属的组。利用inode减去起始inode号得到offset_1。在InodeTable中找到第offset_1个struct inode,即可获取文件的属性交给调用者如果想获取文件内容则读取struct inode中的存储块号。存储块号减去起始块号得到offset_2。读取DataBlock中第offset_2个存储块的内容并返回给调用者。删除一个文件的过程首先确定自己所在的分区。并拿到文件的inode号利用inode减去起始inode号得到offset_1。在InodeTable中找到第offset_1个struct inode,读取struct inode中的存储块号。将所有存储块号都减去分组起始块号得到offset_2.1、offset_2.2、offset_2.3... ...将本分组BlockBitmap中第offset_2.1、offset_2.2、offset_2.3... ...位置的bit位置0。将本分组InodeBitmap中第offset_1位置的bit位置0更新管理结构GDTSuperBlock等。正如我们前文所说计算机启动时要把这些管理信息读入到内存中以便于对文件和外存的管理。实际上在OS中这些管理信息被描述为内核数据结构OS通过对他们操作来管理文件和外存。例如