一、认识磁盘
1、磁盘基本认识
磁盘是计算机中唯一一个机械设备,其有容量大、价格便宜的优点,但是其速度较慢。
下面是一张磁盘的现实图片:

我们将其上面的盖子打开,其内部大致如下:

那个很光滑的是盘片,然后那个针是磁头。
这只是其俯视图,其立体结构实际是这样的:
磁盘的存储结构如下:

一个盘片实际上是有正反两面的磁面的,然后每个磁面都有一个磁头,所以一个磁面都有其对应的磁头
然后我们再看一个面上,一个磁面上又分了很多的磁道
然后一个磁道中,会分为好多个区域,这些区域我们称为扇区,然后这些扇区是磁盘存储数据的基本单位,其存储大小为512字节,又称为块设备
如下:


磁头:就是我们数据的写入读取的工具。
传动臂:其类似一个轴,带动磁头转动,可以使得磁头定位到磁盘的不同磁道
然后我们的磁头的转速都是一样的,磁头之间是共进退的。
我们上面提到我们的磁盘的存储单位是扇区,那么我们要如何定位到一个扇区呢?
那么我们可以先确定那个盘面,然后确定那个磁道,然后再确定扇区。
但是实际上确定一个扇区是如下:
首先我们将磁盘中,几个面的同一个磁道,然后我们将其连接一起,那么其就像一个柱面一样:

那么我们确定一个扇区的位置,实际上是先确定一个柱面、然后再确定磁面、然后确定扇区。
这个定位方法,我们称为CHS地址定位。
然后我们再看,我们的柱面,一个磁盘系统中,有多个柱面,那么我们将其看成一个一维数组,数组存储的元素是柱面,然后我们这个柱面也看成一个一维数组,然后我们这个数组存储的是一个一个磁面,然后我们的磁面也看做一个函数,然后磁面也是一个一维数组。
那么我们总的CHS地址定位实际上是一个三维数组。
早期、我们对于磁盘扇区的查找确实使用的CHS定址法,但是其有下面的问题:
系统会用8bit存储磁头的地址,然后还有10bit来存储柱面地址、然后使用6bit位来存储扇区的地址、那么一个扇区有512Byte个大小,那么使用CHS定址法的话,我们算我们的磁盘大小为10GB,然后1MB=1048576B,那么实际上能用的大小为
256*1024*63*512B=8064MB
下面我们先来分析磁盘的逻辑结构。
我们将磁盘中的磁道,将其可以看成一个磁带,那么我们就可以将其拉直,成一个直线,那么其结构类似如下:

然后我们将所有拉直的磁道按照顺序连接起来,然后我们再细分:

就可以将扇区抽象成这种情况了。
那么我们就将定位一个扇区的工作,就变成了一个一维数组,然后就可以通过下标定位扇区了。
这个地址叫做LBA
磁道:
我们将某一磁道展开如下:

那么我们假设一共有n个扇区,然后我们有x个磁道,那么我们一个磁道就有n/x个扇区。
我们每个磁道的扇区是一样的,虽然我们看到磁道的那个圆环的长度是不一样的,但是其就好比我们的密度,短的磁道其密度大,然后长的磁道其密度小。
柱面:
整个磁盘的所有盘面的同一磁道,柱面展开如下:

柱面上的每个磁道的扇区数都是一样的
上面就是一个二维数组
然后我们一个磁盘有多个柱面,那么这样就也是一个三维数组。
所以确定一个扇区、我们先找到一个柱面、再从这个柱面中找到磁面,然后再确定扇区。
然后再我们C\C++角度来看的话,我们的三维数组、 也可以看成是一维数组。

这样每一个扇区都有其唯一的下标,那么这个我们叫做LBA地址,那么这个地址是线性的。然后我们的CHS地址和LBA地址是可以互相转换的。
我们的OS的话只需要使用LBA即可,然后LBA转换CHS的工作是由我们的磁盘自己来完成的。
下面我们来看看CHS和LBA地址之间是如何转换的。
2、CHS&&LBA地址
CHS转成LBA
磁头数*每磁道扇区数=单个柱面的扇区数
LBA=柱面号C*单个柱面的扇区总数+磁头号H*每磁道的扇区总数+扇区号S-1;
即为:
LBA=柱面号C*(磁头数*每磁道扇区数)+磁头号H*每磁道扇区数+扇区号S-1
然后我们的扇区号通常是1开始的,但是在LBA中是以0开始的
柱面和磁道也是从0开始
对于总柱面、总磁道、总扇区等信息、其在磁盘会有其维护的算法,我们用户不用太关心。
LBA转换为CHS
柱面号C=LBA//(磁头数*每个磁道的扇区数)
或者:
柱面号C=LBA//单个柱面的扇区总数
磁头号H=(LBA%(磁头数*每磁道扇区数))//每个磁道扇区总数
扇区号S=(LBA%每个磁道扇区数)+1
其中://表示除取整数
所以我们后续对于CHS地址、不用太关心、我们都是直接使用LBA地址的,在磁盘内部其会自动转换。
所以我们现在对于磁盘的访问,就当其为一维数组即可,我们都通过下标进行访问,数组的下标就是一个LBA地址。
二、文件系统
1、块概念
其实我们的硬盘是一个典型的块设备、操作系统在对磁盘进行信息的读取的时候不会是一个扇区一个扇区的读取的,而是一次性连续去读取好多个扇区的,我们会将其分为一个一个块区这样。
硬盘的每个分区是被划分的一个一个的块,而一个块的大小在格式化的时候就已经定了的,后续是不可以修改的,我们最常见的大小是4KB的大小的,即为4096字节,那么一个扇区为512字节,那么一个块区就有八个扇区。
所以八个扇区组成一个块区,块是文件存取的最小单位。

我们就看两个信息,一个IO Block 这个是我们的块区,可以看到为4096,然后Blocks其表示扇区数量。
因为我们上面那个文件很小,所以可以看到其最小分区,所以反向也证明了我们块是文件存取的最小单位。
前面我们提到了磁盘的存储结构在物理上实际上就是一个三维数组,然后逻辑结构上为一维数组,每个元素都是一个扇区。
那么我们上面引入的块概念提到,一个块区由八个扇区组成,那么我们的块区也会有其地址。
若我们知道LBA
那么块号=LBA/8
知道块号:LBA=块号*8+n(指其在块内的第几个扇区)
2、分区概念
我们一开始买笔记本的时候,如果是在线下店买的,那么店员大概率会问你是否要帮你将硬盘进行分区。
我们打开我们的此电脑,那么上面显示的CDEF盘就是分区操作。
那么Linux下,是如何进行分区的呢?
柱面是分区的最小单位,我们可以利用柱面号码来进行分区,因为我们的柱面的号码也是顺序的,所以我们只需要记录好一个区域的开始柱面号和结束柱面号即可。
我们将其平铺如下所示:

柱面大小,扇区个位一致,那么我们只需要知道每个分区的起始和结束柱面号,知道一个柱面多少个扇区,该分区多大。

3、inode
首先我们先来重新认识一下文件系统。
若我们要想在硬盘上存储文件,那么我们需要先将硬盘格式化为某种格式的文件系统。文件系统的目的是为了管理和组织硬盘中的文件。
在Linux系统中,我们最常见的就是ext2系列的文件系统。
ext2文件系统将整个分区划分为若干个同样大小的块组、实际上就是若干个块区组成一个区域,然后这一个组会有若干信息,如下图,那么我们能对一个组进行管理,那么就可以对若干个组进行管理。

那么上图中的Block Group就是我们的分组了,然后我们发现一个组中,又有各种信息。
Super Blcok(超级块)
这个部分是用于存放这个组块文件系统的信息的。其主要记录的信息为Block和inode的使用量,还有剩余的Block和inode的数量,一个Block和inode的大小、最近一次挂载的时间、最近一次写入的时间、最近一次检验硬盘的时间还有其他文件系统的信息,如果我们的Super Block信息被损坏,可以说我们这个文件系统就毁了。
GDT
这个是块组描述符、描述块组的属性信息的、整个分区分成多个块组就对应有多少个块组描述符,每个块组描述符存储一个块组的描述信息,如在这个块组从那个位置开始是inode Table。从那个位置开始是Data Blocks,空闲的inode和数据块有多少。块组描述符在每个块组的开头都会进行一份拷贝。
我们从上面的分组信息可以看到我们一个组块中就有一个Block Group,然后其里面含有Super Block超级块和块的位图,inode结点和inode位图以及inode Table、最后就是Data Blcks数据块,其里面就是存储的我们对文件写入的数据。
Data Block
我们之前一直强调的:文件=文件内容+文件属性
其实文件的内容和文件属性都是数据,那么都是数据那么我们的文件系统就要将其都存储起来。
然后我们明确一点就是:在Linux系统中、对于文件内容和属性是分开存储的
然后对于数据的存取是以块为单位的,也就是4KB为单位
对于文件的内容,我们都是存放在数据区的,但是对于不同的文件类型,又有下面几种情况:
对于普通文件、文件的数据存储在数据块中
对于目录,那么这个目录下的所有文件的属性都是存在所在目录的数据区中,除了文件名。
Block号其是按照分区划分的,不可以跨分区
每一个数据块都有唯一的编号
那么我们如何去判断某一个数据块是否被其他文件使用?
Block Bitmap
这里记录的是Data Block中那个数据块被使用了,那个数据块没被使用
其使用的是位图操作,那个位次的数据块被使用,那么其对应的二进制位就为1。
inode Table
在Linux中,这个就是存储我们文件的属性的,其本质上是一个结构体,其表示的文件属性的大小是固定的,一般是128或者256字节大小,因为文化大部分属性的大小都是固定的,就是属性的内容不一样罢了,除了文件名
inode编号是以分区为单位的,其不可以跨分区
inode Bitmap
其表示的是inode Table表中的inode使用的情况,其也是使用的位运算。
inode和Data Block的映射关系
但是如果我们是一个大文件,那么就会存在跨块的情况,那么我们的inode就不够用,那么又是如何解决的呢?
inode内部,其有15个位置,后面三个位置位间接块索引表指针,详情如下图:

如果一个文件的内容不是很大、占的数据块不大的话、那么就使用前面12个直接块指针即可,前12个指针其是直接映射的我们的数据块,那么一个数据块是4KB,那么可以映射48KB的数据块
当文件内容大于这个大小的时候,那么直接映射就不够了,所以就需要使用我们的一级二级三级间接块索引表指针
一级间接块索引表指针:
inode中有一个一级间接块指针,其指向一个索引块,然后这个索引块中存储的是另外一个inode和data block的映射,那么这个索引块大小为4KB,那么其可以存放1024个指针,那么就可以表示4MB的数据了
二级间接块索引指针:
这个就是我们的二级指针,那么其指向的是一个二级间接块指针,然后这个二级间接块指针中存放的是一级索引的指针,那么其一共可以存储1024个一级索引块指针,然后我们的一级索引块又可以存储下1024个数据块指针,那么一共可以有1024*1024*4=4GB的大小的数据块
三级间接块索引指针:
这个就指向一个三级索引指针,这个指针中存储的是二级索引指针,所以一共可以存储1024*1024*1024*4=4TB的数据块。
所以总的来说,我们的文件系统对于大小文件都有其对应的措施。
补充:
数据块的分配是全局的
前面我们说磁盘是被分为好多个组,然后每个组都有自己的inode table和data block区域
不过我们知道每一个块区的编号都是唯一的,所以其是全局的,不局限于那个组
inode table和data block的映射关系
inode和结构体中的结构体指针,其指向的是全局编号的数据块。
那么我们只需要知道块号,那么就可以定位到其具体位置了。
数据块也是可以跨区分配的
当一个文件很大的时候,或者本组的数据块使用完的时候,那么文件的数据可以分配到其他组的空闲块、一个文件的数据块可以分布在磁盘的任意位置,不一定在一个同一个块组中
然后我们的group会有下面两种情况:
1、inode用完、然后我们的block没用完
这种情况的话,那么我们无法在这个group创建新的文件或者目录了,不过block可以被其他的group的文件使用
这种情况就是在大量小文件的情况。
2、inode没用完、block用完
那么我们就没办法在这个group中再分配数据块了,但是可以创建新文件,然后这个新文件会分配在别的group的block。
常见于少量大文件的场景
总结:
我们创建一个新文件主要有下面四个操作
1、内核先找到空闲的inode结点、然后将文件的属性信息记录其中
2、存储数据
3、记录块区的分配情况
4、添加文件名
删除文件:
拿到inode的编号,然后在inode bitmap中找到,然后确定其是否有效、然后再通过inode读取inode的结构、获取这个文件的属性信息、获取到文件的大小。若不为0,那么就通过inode结构体内的数据块指针数组找到数据 块区,然后将Block Bitmap中的1置为0,那么就删除完成了。
所以我们删除一个文件其实并没有知道去删除,只是将其占用的数据块的状态符给修改了。
三、目录和文件名
1、目录和文件名
上面我们讲到,我们对于文件的打开、查找、删除都是通过inode这个结点操作的,但是inode不是存储在inode的。
那么我们文件的文件名是存储在那边的呢?
首先、目录也是文件,那么目录也有其属性和内容。
然后目录的属性就在其对应的inode中存储着,那么目录的文件名也不是存储在其对应的inode的,那么是如何存储的呢?
我们提到目录也有其内容,那么我们的目录的内容就是其路径下的文件名,所以文件的名字实际上是存储在其上一级目录中的
所以访问文件,必须先打开其所在的目录,然后根据其文件名找到其对应的inode号,然后才能进行文件访问。
这也是为啥,同一个目录中,不可以存在同一个名字的文件。
这也是为啥,我们的目录文件其也有权限符了。
然后我们要在这个目录找文件,那么就必须要有这个目录的读权限。
路径缓存:
如果每次访问文件都要去对这个文件做路径解析,那么这样效率就比较慢了。
实际上Linux中有一个目录树
当用户访问文件的时候,Linux会对其路径进行解析,然后在内核中形成目录树和路径缓存
不过在第一次对其进行缓存的时候,其速度是比较慢的,后面的时候就会从dentry树结构中进行解析了。
每个文件都要有其对应的dentry结构、包括普通文件。这样被打开的文件在内存中就可以存在于目录树中了。
然后对于一些不使用的目录,其会对这个结点进行淘汰
整个树形结构会隶属于hash结构、提高其查找效率

这个树形结构,整体构成了Linux的路径缓存结构、打开访问任何文件、都要在这个树下根据路径进行查找,找到就返回inode和内容、没找到就从磁盘加载路径,添加dentry结构,缓存新的路径。
