认识磁盘
谈文件系统之前,我们先来认识一下磁盘

这个就是我们目前家用电脑上最常见的机械硬盘,他的优点是存储容量大,价格便宜,我们来看看他的内部构造

我们所有的数据都是保存在盘片上的,那么是怎么存储数据的? 在磁盘上面会有很多的磁道

- 所有文件、系统数据都写在磁道上;
- 单个磁道空间太大,会再切分成扇形小块,叫扇区(最小读写单位);
- 一组上下对齐、同编号的磁道合起来叫柱面(柱面就是磁盘的正面和反面合起来的称为柱面)
扇区是磁盘存储数据的基本单位,大小为512字节,是一个块设备。
那么我们现在知道了,数据是存在扇区上面的,我们知道文件=内容+属性,在磁盘里面归根结底,就是占用多少个扇区的问题,那么我们怎么去定位我们存放在磁盘上的数据的呢?
如何定位⼀个扇区呢?
1.可以先定位磁头(header)
2.确定磁头要访问哪⼀个柱⾯(磁道)(cylinder)
3.定位⼀个扇区(sector)
这就是 CHS地址定位!
扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。
磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头
磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道...,靠近主轴的同⼼圆⽤于停靠磁
头,不存储数据
柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
圆盘(platter)数:就是盘⽚的数量
磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数
上面我们说了磁盘的物理结构,下面我们来说说磁盘的逻辑结构
磁盘的逻辑结构
我们想一下,磁盘的磁道就相当于一组一组的同心圆构成,如果我们把里面的同心圆全部拿出来,把他放直,形成线性结构与,就像下面这样

那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,那么磁盘的逻 辑存储结构我们也可以类似于下面这样

这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做 LBA,LBA就是一个逻辑地址,磁盘访问都是用这个地址。
这个只是我们方便理解,但是在现实过程中磁盘的的真实运动过程是这样的
我们要知道一个细节,磁盘上面的磁头是共同进退的,就像下面这张图一样

我们把磁道展开是一维数组,而把柱面展开就是一个二维数组,我们看看下面的图片

那么整个磁盘都展开,这不就是一个三维数组吗

但是在计算机世界里面,我们无法表示除开一维数组以外的所有维度的数组,这时候,我们就需要把三维数组通过算法抽象成一位数组
所以,寻址⼀个扇区:先找到哪⼀个柱⾯(Cylinder) ,在确定柱⾯内哪⼀个磁道(其实就是磁头位置, Head),在确定扇区(Sector),所以就有了 CHS 。
我们把它抽象成一个一维数组,每⼀个扇区都有⼀个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址。所以怎么计算得到这个LBA地址呢?
OS只需要使⽤LBA就可以了!!LBA地址转成CHS地址,CHS如何转换成为LBA地址。谁做啊??磁盘 ⾃⼰来做,我们知道磁盘除开机械部件还有硬件电路,上面有主控缓存等

那么是怎么转换的呢,
CHS转成LBA:
磁头数*每磁道扇区数 = 单个柱⾯的扇区总数
LBA = 柱⾯号C*单个柱⾯的扇区总数 + 磁头号H*每磁道扇区数 + 扇区号S - 1
即:LBA = 柱⾯号C*(磁头数*每磁道扇区数) + 磁头号H*每磁道扇区数 + 扇区号S - 1
扇区号通常是从1开始的,⽽在LBA中,地址是从0开始的
柱⾯和磁道都是从0开始编号的
总柱⾯,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参
数。
LBA转成CHS:
柱⾯号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】
磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数
扇区号S = (LBA % 每磁道扇区数) + 1
"//": 表⽰除取整
有了上面的对于硬件的理解之后,我们来从linux系统角度来看看文件
引入文件系统
引⼊"块"概念
其实硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样
效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个"块"(block)。
硬盘的每个分区是被划分为⼀个个的"块"。⼀个"块"的⼤⼩是由格式化的时候确定的,并且不可
以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 "块"。"块"是⽂件存取的最⼩单位。
我们来看看我们linux下的一个文件

我们可以看到这一个块的大小是4096字节,也就是4kb
磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇
区
每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
知道LBA:块号 = LBA/8
知道块号:LAB=块号*8 + n. (n是块内第⼏个扇区

引入分区的概念
在我们日常生活中,我们的磁盘会被划分成多个分区,就拿windows来说我们一般会划分成c盘d盘
那个c,d就是分区。分区从实质上说就是对硬盘的⼀种格式化。也是一种分治思想但是Linux的设备
都是以⽂件形式存在,那是怎么分区的呢?
柱⾯是分区的最⼩单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起 始柱⾯和结束柱⾯号码。 此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平 ⾯,如下图所⽰:

但是,光用柱面来划分又太麻烦了,linux会以块为单位划分分区,就像下面的图片一样

这样我们就可以对磁盘进行分区了
引入inode概念
我们知道 文件=内容加属性,我们可以用ls -l看到文件的属性,除了可以看到文件名字还可以看到文件的属性

其中的属性包括

ls -l读取存储在磁盘上的⽂件信息,然后显⽰出来
到这我们要思考⼀个问题,⽂件数据都储存在"块"中,那么很显然,我们还必须找到⼀个地⽅储存
⽂件的元信息(属性信息),⽐如⽂件的创建者、⽂件的创建⽇期、⽂件的⼤⼩等等。这种储存⽂件 元信息的区域就叫做inode,中⽂译名为"索引节点"。
我们用ls -li就可以看到文件的inode

每⼀个⽂件都有对应的inode,⾥⾯包含了与该⽂件有关的⼀些信息。为了能解释清楚inode,我们需 要是深⼊了解⼀下⽂件系统。
在了解之前我们需要注意几点
⽂件名属性并未纳⼊到inode数据结构内部
inode的⼤⼩⼀般是128字节或者256,我们后⾯统⼀128字节
任何⽂件的内容⼤⼩可以不同,但是属性⼤⼩⼀定是相同的
深入理解文件系统
有了上面的理解之后,我们需要深入了解文件系统,需要知道在linux操作系统中文件到底在硬盘上面是如何存放的,
ext2文件系统
我们知道,linux系统中文件是以块为单位存放的,我们的分区就是以块为单位,记录一个起始地址和结束地址,就代表一个分区,但一个分区一般是200g或者300g或者更大,这样也不好管理,在Linux系统中,还会把分区继续细化变成以组为单位
ext2⽂件系统将整个分区划分成若⼲个同样⼤⼩的块组 (Block Group),如下图所⽰。只要能管理⼀个 分区就能管理所有分区,也就能管理所有磁盘⽂件。
如下图所示

这个图片我们会详细说明,首先宏观的大体看一下
首先从上至下第一个是如下图所示

第二层

第三层

第四层

data block
数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况:
普通⽂件,⽂件的数据存储在数据块中。
Block 号按照分区划分,不可跨分区
datablock大小为4kb
inode table
我们知道linux中任何文件都要有自己的属性集合,而这些属性集合的大小是一样的而inode_table就是保存这些属性的
首先我们来说说这个 inode table 这里面是存放文件的各种属性包括权限类型等
而这个是一个结构体

一个数据块是4kb 4096/128 等于 32 ,也就是说一个数据块可以保存32个inode 也就是说这里可以存放32个文件的属性
有了inode_table我们就能知道文件的属性了,但是在磁盘中有千千万万个这种文件的属性,那我们怎么可以快速知道哪些inode被使用,哪些没有呢
inode_bitmap
用一个位图我们就能快速知道了
在inode_bitmap中每个bit表⽰⼀个inode是否空闲可⽤。
假设有10万个inode编号,我们需要快速定位,我们就需要10万多个比特位 2的17次方大于10万转换为10进制就是131072,位图初始化内容:一共**131072 个比特,**全部填 0,然后从一开始编号,这个就是我们的位图
计算过程
计算机最小可寻址单元:字节 Byte = 8bit 所以我们用 unsigned char buf[16384] 字节数组承载。buf 数组里,每一个 char 存放连续 8 个 bit。8*16384=131072,这里换算下来就是16Kb,我们就需要4个块(data_block)来存储
查找空闲 inode 流程:
- 遍历 inode_bitmap,寻找第一个值为 0 的 bit
- 算出 bit 下标 → 映射得到 inode 编号
- 将该 bit 置 1(标记已占用)释放 inode:将对应 bit 重新置 0
有了inode_bitmap能够快速帮我们定位inode编号我们同时也需要知道哪些data_block被使用了,这时候data_block也有自己的bit_map
Block Bitmap
Block Bitmap中记录着Data Block中哪个数据块已经被占⽤,哪个数据块没有被占⽤
我们这时候想一个问题一个分区会被细化为多个组,inode编号和data_block编号只能在同一个组中使用吗,答案是不是的
inode和数据块是跨组编号的,但是inode和数据块不能跨分区所有在同一个分区内部inode和块编号都是唯一的。
有了以上的了解,文件系统还需要一个部分来描述整个关于inode data_block的整体使用情况,所以就有了GDT
GDT块描述表
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描
述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data
Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。这个就相当于对整个块的一个总览
Super Block
超级块,是是存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的 总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写 ⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可 以说整个⽂件系统结构就被破坏了 我们再回到上面的图片看一个细节,
超级快既然是对真个硬盘分区的一个描述,那么为什么每一个组前面都要有一个超级块呢,答案是备份! 在我们日常生活中,有时候我们的windows电脑会意外关机,再开机的时候就会看到他在转圈圈,一般要转个几分钟,这个就是磁盘的分区损坏了,在进行恢复
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂
件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信 息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份, 这些super block区域的数据保持⼀致。
有了以上的对于文件系统的了解,我们来谈谈下面的问题
硬盘格式化的本质
我们平时复制东西,一般我们在复制大文件的时候,会复制很久但是如果是删除大文件,一下子就删除完毕了,这个删除是真的像复制那样进行挨个挨个删除吗,答案是不是的,我们知道文件里存放的东西本质就是由inode 和data_block组成,而inode 就是一个文件在磁盘中的编号,我们删除东西,本质就是把inode编号设置成未使用,再把inode_bitmap里面的对应的比特位设置成0还有修改块位图就行了,哪些块属于一个文件呢,再inode_table里面有一个数组int data_block\[\]就可以记录哪些块被使用了,剩下的inode对应的data_bolck里面的东西本质就是乱码,不关心!
而硬盘格式化,就是重置super_bolck GDT inode inode_bitmap bolci_bitmap 就可以了!如果需要完整格式化data_block也需要重置,速度就会慢一些。
一句话总结,本质就是修改文件系统的管理信息
linux只要知道了文件的inode 就可以定位文件了。
如何看待目录
我们访问⽂件,都是⽤的⽂件名,没⽤过inode号啊?
磁盘上只要对应的文件和属性,也没有这个目录的概念
目录的本质也是文件也是有属性加文件内容,但是目录里面的内容是存放对应的文件名和inode对应的映射关系,这其实也是数据!

那我们平时查找文件时都需要提供路径,为什么下面用ls 时就不需要文件路径了呢

我们查找文件,本质就是进程在查找文件,而进程有对应的cwd,当前所在的路径,这里的路径操作系统提供了,我们就不需要再提供了
我们用的文件名访问文件,本质就是路径+文件名,先打开对应的路径,读取里面文件的内容,得到文件名和inode的映射关系进行文件的查找!

linux查找任何文件都必须要从根目录往下面找,才能找到对应的文件!
我们再想一个问题,我要查找一个·文件都必须要从根目录往下面找,是不是太慢了?对于这种情况linux提供了一个叫struct dentry的数据结构,这个本质就是一颗树,记录从根目录到任何一个目录的记录这个本质就是一个路径缓存
路劲缓存
源代码
cpp
struct dentry {
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
/*
* The next three fields are touched by __d_lookup. Place them here
* so they all fit in a cache line.
*/
struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中
形成整个树形结构(注意在叶子节点上也会存在对应的struct dentry结构,为什么通过struct dentry就可以找到文件对应的inode!!)

整个树形节点也同时会⾪属于LRU(Least Recently Used,最近最少使⽤)结构中,进⾏节点淘汰
整个树形节点也同时会⾪属于Hash,⽅便快速查找
更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这
棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry
结构,缓存新路径

我们已经能够根据inode号在指定分区找⽂件了,也已经能根据⽬录⽂件内容,找指定的inode了,在 指定的分区内。可是:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区???
挂载分区
我们开做一个实验

上面的图片我们制作了一个大磁盘块,相当于一个分区

格式化写入文件系统

建立空目录,然后查看可使用的分区

将指定分区挂载到指定目录

我们刚才做了创建磁盘,格式化分区,然后再挂载到指定目录
我们的分区一定要和某一个目录进行关联,进入这个目录,相当于进入这个分区,这个操作叫挂载
所以再次回到我们刚才的问题操作系统是如何知道我们的哪个文件是在哪个分区的?答案是在我们的文件路径中,某一个文件一定是一个挂载点对应一个分区!所以我们能够找到对应的分区下的对应文件!
在最后我们在讲一个inode 和data_block的映射关系

这里用15个块共计60kb就能表示很大的文件了,小文件就直接用前面的12个直接块指针,大文件就用一级二级三级块指针来表示,这样就可以存放大文件,如果这个都不满足的话,就需要使用分布式存储了
这就是linuxext2文件系统