前言:
我们知道,一个进程通过文件描述符来标识一个打开的文件,进程拿着描述符就可以在内核中找到目标文件并对其进行各种读写操作。在此之前,文件存储在磁盘中,操作系统是如何在偌大的磁盘中找到自己想要的文件并打开的呢?对于这些没有被打开的文件,操作系统又是如何做管理的呢?本篇文章将带领大家较为深入的理解linux的文件系统。
磁盘概述
磁盘是计算机的主要存储介质,可以存储大量的二进制文件,并且断电也不会丢失数据。下面就是一个磁盘
磁盘的构造
磁盘是由盘片构成的。每个盘片有两面或者称为表面,表面覆盖着磁性记录材料。对于每个磁性单位,磁化表示1,未磁化表示0。盘片中央有一个可以旋转的主轴,它使得盘片以固定的旋转速率旋转。在旋转的过程中,磁臂带动着磁头来回摆动来读取信息。盘面和磁头的数量是1:1,也就是说,每一个盘面都有一个负责读取其数据的磁头。
此外,磁头与磁盘之间是有一定空隙的,如果磁头和磁盘接触,加上旋转带来的摩擦,很容易刮花盘面,从而导致数据丢失。这也为什么磁盘在摔过之后就很容易坏掉。
总的来说,一个磁盘的主要组成成分包括:盘片、主轴、磁头、磁臂以及驱动马达等,它们被封装在一个密封的包装里,整个装置被称为磁盘驱动器。
盘面的结构
每个盘面都是由一组称为磁道(track) 的同心圆组成。每个磁道被划分为一组 扇区(sector)。每个扇区包含相等数量的数据位(512 byte),这些数据编码在扇区的磁性材料中。扇区之间由一些间隙分隔开,这些空隙不存储数据位。间隙用来存储用来标识扇区的格式化位。 文件存在磁盘的一个个扇区里。(打开的文件理论上也是拷贝了一份磁盘文件到内存)。
我们将所有盘片表面上到主轴中心的距离相等的磁道集合称为柱面。例如,假设一个磁盘驱动器里有三个盘片(六个盘面),每个表面的磁道的编号都是一样的,那么柱面x,就是六个x号磁道的集合。
一般来说,扇区是磁盘的最小存储单元。如果每次读写数据都是以一个扇区为单位就会使得读写的效率非常低。假设我们要读1G(1024*1024*1024byte)大小的文件,那就要连续读1024*1024*2次。所以文件系统把多个扇区组成一个块(逻辑块),每个块占4kb的字节大小(包含八个扇区)。每次读写就是按块来读写,提高了整体的读写效率。
需要弄清楚的是,扇区是磁盘的最小存储单位而不是读取数据的最小单位。 块才是。
CHS寻址
既然扇区是磁盘存储数据的单元,那如何在磁盘中找到目标扇区呢?
要想确定一个扇区的位置,需要以下步骤:
- 找到目标扇区所在的盘面。
- 找到目标扇区所在的磁道。
- 在磁道中确定哪一个扇区。
同样的,如果我们确定了一个柱面(Cylinder),再确定柱面上的一个磁头(Head),也就能找到一个唯一的磁道,再在该磁道找到的扇区(Sector)就是唯一的目标扇区了。我们把上面的寻址方法称为CHS寻址法。对于CHS寻址法,只需要知道上面不同的三个参数,找到一个唯一的扇区!
磁盘的抽象存储结构
为了便于寻址,我们可以先将整个磁盘抽象成一个线性的连续的数组,每个元素当然就对应着一个盘面
这样划分显得太粗糙了,我们再接着继续细分。把每一个盘面划分成诺干个大小相同的磁道,于是整个数组的最小元素就变成了磁道。
即使是这样,也还是不够,再继续细分。把每个磁道划分为诺干个大小相同的扇区,于是整个数组的最小元素就变成了一个扇区。
这样一来,整个数组就是磁盘中所有扇区的集合。每个元素都对应着一个扇区的地址,每一个扇区都有唯一的一个下标映射。将来我们想要找某一个扇区,只需要用它在数组中的下标就能找到该扇区的CHS。具体计算方法如下:
假设每个磁道上有100个扇区,每个盘面有100个磁道。现在想找到下标index=50505对应的扇区的CHS地址:
根据假设信息,一个盘面有100*100=10000个扇区。
- 盘面位置(H)=index/10000=5,即该扇区在第五盘面。
- 磁道位置( C)=(index%10000)/100=5,即该扇区在第五盘面的第五磁道上。
- 扇区位置(S)=index%100=5,即该扇区位置在第五盘面的第五磁道的第五个扇区的位置。
这样一来,系统只需要管理好这个数组,就等于变相的管理好整个磁盘了!又因为读写数据的最小单位是数据块而不是扇区,为了方便文件系统读写数据,我们还需要将上面的数组进一步抽象成以数据块为单位的数组。
LBA地址
当我们整个磁盘按块划分之后,就天然的得到了一个Blocks数组,数组的每个元素表示4kb即八个扇区。对于每一个块的起始地址,我们称为LBA(Logical Block Address)地址,也称逻辑块地址。LBA其实也就是每个块的第一个扇区的地址。从此往后,我们对于扇区的管理就变成了对数据块的管理,即对blocks数组的管理。对于操作系统来说,对文件的增删查改就变成了对blocks内容增删查改。
理解文件系统
虽然我们已经可以将磁盘分为一个个数据块,但是对于空间巨大的磁盘来说还是太小了!(直接管理的对象太多了)现在我们的计算机大一点的都是以t为单位了,一个t就有多少数据块?为了便于管理,我们又将磁盘划分为诺干个分区。比如一个500GB的磁盘,我们可以划分为5个分区,每个分区管理100GB。只要操作系统能管理好一个分区,我们用同样的方式就能管理好其它所有的分区 。
那问题回到,如何管理好一个分区呢?
将一个分区按组划分,假设每个组的大小为2GB,那么一个分区就有50个组。根据分治思想,只要我们想办法管理好一个组,也就能用样的方法管理好所有组 (Block group[]),进而管理好整个分区。
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同)。磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
启动块Boot Block存放的是操作系统的核心数据,每当计算机开机都需要先从Boot Block读取数据才能正常启动。
每个Block group里都存着以下信息:
- Super Block(超级块):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- inode节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- Data blocks 数据区:存放文件内容
我们常说文件=内容+属性,其实可以理解为文件=inode+Data blocks。
结合上图可以看到,每个组包含了诺干个大小不一的文件,文件系统将这些文件的属性和内容分开存放。除此之外还设置了一些区域存放用来管理组内文件的信息,比如GDT、Block Bitmap等。每一个文件的属性inode表里记录着文件内容所占数据块的位置。
理解inode
inode又称为索引节点。每一个文件都有唯一对应的inode。inode表现为一个号码,存放着除了文件名之外的所有信息:文件大小,文件最近修改时间,文件内容在Data blocks中的位置等。
struct inode{ inode 编号; 文件类型; 权限; 引用计数; 拥有者; 所属组; ACM时间; int datablocks[10];//文件内容所占的块号位置 }
在linux中我们可以用带-i
选项的ls
指令来查看文件的inode:
此外也可以用stat
指令查看文件的详细信息,也可以查看到indoe:
给出结论:要想在磁盘中找到一个完整的文件,就必须要知道该文件的inode
那为什么inode节点信息不包括文件名呢?
原因:文件名长度不确定,不利于用一个统一空间的角度看待inode。为除了文件名以外的信息所占空间都是固定的,这也就导致了每一个文件的inode节点的空间大小是一样的,这使得系统更好的管理(只需要计算偏移量就能确定一个inode的位置)。一旦加入了文件名这个不确定字符串长度,每个文件的inode大小都不一样,不利于读取inode信息。
在文件系统中,inode节点的大小是固定的128byte(不同文件系统之间可能不一样)。inode table 其实就是一个inode数组,里面每一项都是一个inode。跟文件描述符类似,每个组的inode编号其实对应inode table的下标。
如何通过一个inode在整个磁盘中找到目标文件呢?
首先是找到哪一个组。对于一个分区来说,每个的组的inode编码都是不重复的,且都有自己的起始位置,衔接上一个组inode的末尾位置。比如第一个组的inode范围为1000-2000,第二个组的范围为2001-3001,依次往后面推。所以我们只需要比较一下inode在那个范围就能确定在那个组了。再用该inode减去该组的起始值,就能在inode table里找到目标inode真正位置了。这样就找到了目标文件的属性。然后再通过inode节点中的datablocks[]找到各个数据块的地址。拿着这个地址再去Data blocks里面去找对应的数据块。这样就能找到文件内容了。
总结:要想找到文件内容必须先找到文件属性。
如何获得一个新的inode呢?
在inode Bitmap中记录着每一个inode的空闲状态。当我们要创建一个文件,内核会在inode Bitmap中快速找到有哪个位置的比特位是0。假如找到了第10个比特位为0,那么就将这个比特位置1,表示已经征用这个inode号了,再将该inode加上该组的起始值后返回给操作系统。当我们后面要对这个文件写入的时候,操作系统又会去该组的block Bitmap去找可用的数据块位置。跟上面是同样的方法。
理解大文件的索引方式
刚才我们提到,每个文件的indoe内容大小是固定的,这对于大文件也是一样的吗?indoe中用来记录数据块的地址是一个datablocks数组,内部的元素个数是确定的,一般是15个。
如果每个datablocks[i]对应一个data block(数据块),那就最多占15*4kb=60kb的空间。这样是肯定不够的!
我们来看看实际datablocks[]的内容
datablock中并非所有元素都直接映射一个数据块,而是会根据实际情况建立二级索引、三级索引。对于二级索引来说,每一个元素对应的是一张数据块表,即表中的内容就是真正数据块的位置。这样一来,用一个块去存放一个块表,大大提高了存储的效率。而即使是较大的文件,三级索引也基本上够用了。