之前我们讨论的都是内存级被打开的文件,如果一个文件没有被打开,文件会存储在磁盘上,存储在磁盘的哪个位置?又是怎么存储的?存储完毕后又怎么找到文件?磁盘级文件的保存是由磁盘级文件系统来决定,要学习文件系统需要对磁盘有所了解。
1. 磁盘
1.认识磁盘
磁盘是计算机中唯一的一个机械设备,磁盘具有永久存储数据的能力。磁盘在冯诺依曼体系中是外设。

磁盘一旦拆开,进入灰尘之后,磁盘就报废了,在未打开磁盘时,内部整个环境都是无尘的。磁盘拆开后如下图所示:

磁盘长这样是怎么存储数据的呢?将磁盘想象成一条无限长的磁带,上面原本所有小磁针都朝右。写入数据时,你在某些位置"拨动"一下,让后面的磁针全部反向。读取时,你只关心"哪里发生了方向变化"------每个变化点就代表一个"1"。这就是基于翻转的记录方式。磁化方向的变化(翻转) 被用来编码 1,无变化表示 0,那么向磁盘写入01的本质上是在规定的时序位置上是否发生磁化方向的翻转。
磁盘怎么消除数据?最简单的方法就是对磁盘盘片进行消磁或者使用特殊方法将磁盘中的所有01位清0或清1。怎么对磁盘进行消磁,与对磁铁消磁的原理一致,高温状态下磁盘就会消磁(最粗暴的方法)。
2.磁盘的物理结构
磁盘打开后,各个组件的名称:

主轴通电后会带动磁盘转动,磁头臂带动磁头左右摆动。磁头与盘片是不接触的,磁头与盘片的距离非常小,盘片高速旋转,磁头左右偏动,若磁头磁盘接触,就会摩擦生成热,高温就会消磁,消磁就会丢失数据。
3.磁盘的存储结构
一个盘片和多个盘片的视图如下所示:

一圈为一个磁道,一个磁道上有多个扇区。一个盘片有两面,正反两面都可以存储数据。

有多少个盘面就有多少个磁头,磁头与盘面的数量是1:1的。每一个盘面都有一个磁头,盘片高速旋转,磁头随着机械臂杆左右摆动,所有磁头的运动轨迹都是一样的,即所有磁头共进退,所有盘片共旋转。一个磁盘的侧视图如下所示:

对于磁盘,半径相同的磁道组成一个柱面。后续在讲磁道和柱面时是一个概念。
扇区是磁盘存储数据的基本单位,一个扇区的大小是512字节。读写数据时,大小必须是以512字节为单位,因此磁盘存储设备被称为块设备。很明显越接近圆心位置,扇区越小,尽管扇区的物理大小是不一样的,但是我们认为扇区的存储容量都是相同的,都是512字节。
若要向磁盘写入512字节大小的数据,首先就需要找到对应扇区的位置。怎么找?要找到对应扇区的位置就需要确定它在哪个盘面上。怎么确定它在哪个盘面上呢?每个磁盘都对应存在一个磁头,确定扇区在哪个磁盘上,通过磁头来确定。确定它存在哪一面上后,还需要确定扇区在哪一个磁道中,即确定在哪一个柱面上。确定在哪个柱面上之后,再确定是第几个扇区。
总结:如何定位一个扇区(不准确的说法)
- 先定位磁头
- 再确定磁头要访问哪一个柱面
- 最后确定在哪一个扇区
这就是 CHS 地址定位,根据 CHS 地址定位就可以定位一个扇区,只要能够定位一个扇区,就能定位任意一个扇区,也就能定位任意多个扇区。
使用指令 fdisk 可以查看磁盘扇区的结构:

从中可以看出,扇区是从磁盘读出和写入信息的最小单位,通常大小为 512 字节。
接下来总结磁盘中涉及的名词:
磁头(head)数:每个盘片⼀般有上下两面,分别对应1个磁头,共2个磁头
磁道(track)数:磁道是从盘片外圈往内圈编号0磁道,1磁道...,靠近主轴的同心圆用于停靠磁头,不存储数据
柱面(cylinder)数:磁道构成柱面,数量上等同于磁道个数
扇区(sector)数:每个磁道都被切分成很多扇形区域,每个磁道的扇区数量相同
圆盘(platter)数:就是盘片的数量
磁盘容量=磁头数× 磁道(柱面)数× 每道扇区数× 每扇区字节数
4.磁盘的逻辑结构
以磁带为切入点理解磁盘的逻辑结构。磁带一卷一卷的,磁带上面可以存储数据。

我们可以把磁带"拉直",形成一个直线,那么我们是否可以将卷起来的磁带想象成一个线性结构?可以。
磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在一起的磁带,那么磁盘的逻辑存储结构类似于线性结构:

一个盘面想象成一个一维数组,数组的元素是扇区,数组的下标就是扇区的编号。

最终就将整个磁盘想象成了一个一维数组。因此我们在逻辑上可以将磁盘理解成一个一维数组,数组的元素就是扇区,数组的下标就是扇区的编号。
怎么在一维数组上定位一个扇区?既然一个磁盘被想象成一个一维数组,这样每个扇区就有了一个线性地址(数组下标),这种地址就做 LBA 地址。因此定位一个扇区可以使用 LBA 地址,数组下标对应的就是扇区的逻辑编号(LBA)。
磁头共用一个转动臂,当转动臂移动时,会引起所有磁头左右移动,不管怎么摆动,所有磁头都是共进退的,因此每个磁头都会访问同一个磁道。
真实情况下,访问磁盘首先需要确定的是柱面,因为磁头在移动。接下来思考一个问题,磁头左右摆动在做什么?磁头摆动的本质:确定访问哪一个磁道或柱面。盘片在旋转是要干什么?盘片旋转的本质:让磁头定位到指定扇区(若磁头错过了盘面,需要等下一圈,因此磁盘的转速是衡量磁盘存储效率的指标)。确定在哪一个柱面,哪一个磁头之后,读取一个扇区,选定哪一个磁头即可。
因此真实情况下,在进行磁盘访问时,先确定访问哪一个柱面!!柱面是一个逻辑上的概念,每一个盘面上的相同半径的磁道逻辑上构成柱面。所以,磁盘物理上分了很多面,但是逻辑上磁盘整体是由"柱面"构成的。
从逻辑上抽象整个磁盘:
某一盘面的某一个磁道展开:

即:一维数组。
整个磁盘所有盘面的同一个磁道,即柱面展开:

柱⾯上的每个磁道,扇区个数是⼀样的,这不就是二维数组吗?
整个磁盘展开:

整个磁盘全部展开后就是多张二维的扇区数组表,这不就是三维数组吗?
由此我们可以知道磁盘的本质就是三维数组!!!那么如何在磁盘中定位一个扇区?先确定哪一个柱面,再确定哪一个磁头(通过确定磁头来确定盘面),最后确定哪一个扇区,这就是CHS。所以CHS 本质上就是三维数组的数组下标。
在我们学习数组时,所有数组都可以看成是一个一维数组,三维数组也不例外。

因此我们可以进一步的理解磁盘,磁盘可以在逻辑上理解为一个一维数组。所以,每⼀个扇区都有⼀个下标,我们叫做LBA(逻辑块)地址,其实就是线性地址。怎么计算得到这个LBA地址呢?
在一个磁盘里,柱面大小是一样的,一个柱面有多少个磁道是一样的,一个磁道有多少个扇区是一样的,一个扇区的大小的是一样的,因此只需要知道磁盘的总容量大小,就可以知道磁盘共有多少个扇区,每个扇区的 LBA 也就可以知道了。
OS 访问磁盘是并不直接使用 CHS 地址,只需要 LBA 地址即可,在系统开机时,获取磁盘总容量的大小,经过计算便就得到了每个扇区的下标。但是知道 LBA 地址后,怎么知道这个地址访问的是磁盘的哪一个位置?我们知道确定一个扇区的位置使用的是 CHS 地址,因此根据 LBA 地址访问磁盘的某个扇区,需要将 LBA 地址转换成 CHS。转换操作是磁盘来执行的,将磁盘当作一个黑盒,不需要关心磁盘是怎么将 LBA 转换为 CHS 地址,只需要知道磁盘总容量的大小,总容量除以扇区的大小,向磁盘导入 LBA 地址,OS 就知道用户要访问哪一个对应的扇区了。
LBA与CHS的相互转换了解即可,我们不需要知道LBA与CHS是如何转换的,只需要知道LBA和CHS地址可以通过简单的乘运算或除模运算进行转化即可:
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
LBA 地址转化为 CHS 地址本质上就是将一维数组的下标转化为三维数组的下标

将标红区域的扇区位置的 LBA 地址转换成 CHS。下标从0开始,红色区域的下标为13,磁头数为3,每磁道扇区数为4。
柱面号:C=13//(3*4) = 1
磁头号:H=(13%(3*4))//4 = 1//4 = 1
扇区号:S=(13%4)+1= 2(扇区的编号从1开始)
因此标红扇区的位置是:第1号柱面的第1个磁头的第2个扇区
就像二维数组转变成一维数组,如char arr[10][10]转化为一维数组arr[100],知道一维数组下标为51的元素,确定该元素在二维数组的位置:51/10 = 5,51%10 = 1,就可以确定该元素在二维数组的第5行第1列。
2.ext2文件系统
本文主要讲的是 ext2 文件系统,目前主流的文件系统用的是 ext3 或 ext4。
1."块"的引入
在现在看来,OS 会将磁盘看成一个线性数组,数组的元素就是一个个扇区。硬盘是典型的"块"设备,数据读写时以扇区为单位。但是 OS 读取扇区时,是不会一个一个的去读,因为这样效率太低了,因为每次读取都会触发一次硬件访问,触发一次IO。一般情况下,OS 访问磁盘会一次性访问多个扇区,即一次读取一个"块 ",最常见的块大小就是4KB ,即连续八个扇区组成一个块 ,"块"是文件存取的最小单位。
可以查看一个块的大小,使用指令:stat 任意文件名。

磁盘就是一个三维数组,我们把它看成一个"一维数组",数组下标就是 LBA,每个元素都是扇区,每个扇区都有 LBA,那么8个扇区一个块,每一个块的地址我们也能算出来。

OS 本来可以直接使用上述的一维数组读写扇区就可以了,但是这样读取效率太低了,因此 OS每次读取一个块(512*8=4096),将数组每8个扇区组成一个块,每个块都有自己的块地址(块号)。之后只需要知道磁盘的总容量大小,知道每个扇区的大小,就可以计算出每个块的地址。若要访问块1,告诉磁盘要访问块1的连续8个扇区,怎么访问?将块1的地址转化为8个LBA地址,怎么转?块号直接乘以8,就是块中第一个扇区的起始地址,之后再向后一直加到7,这样就将块1的地址转化为8个 LBA 地址了,再将这8个 LBA 地址依次交给磁盘,磁盘就可以访问块1了。
LBA地址怎么转变成块号呢?如LBA地址为9,9/8=1,说明 LBA 地址为9的扇区在块1中。如此一来,块<->LBA<->CHS之间可以相互转化。
为什么OS不直接用扇区来访问磁盘?为什么不使用512字节为单位访问磁盘,而是使用4KB为单位访问磁盘呢?1.提高效率(主要因素);2.软硬件解耦。
2.分区的引入
假设一个磁盘的大小为500GB,那么共有500GB/4KB个块,那么以 OS 的视角去看待磁盘就是一个一个块。但是如果OS要管理整个磁盘,500GB的磁盘太大了,管理起来会非常复杂,因此 OS会将磁盘划分成一个一个的分区 。一个分区中有多个块,能够管理好一个分区就可以管理好其它多个分区。如何管理好一个分区?OS 是如何管理好磁盘的呢?不就是将磁盘划分成多个分区吗?那么怎么管理好一个分区呢?将分区划分成多个组不就好了吗?只要将一个组管理好了就可以管理好其它组。将所有组管好之后,一个分区就被管理好了,一个一个的分区管理好了,那么磁盘也就被管理好了,这不就是分治的思想吗?

在OS内部,既有磁盘的概念,又有分区和分组的概念。有时磁盘不只有一个,磁盘多了,分区也就多了,分区多了块组也就多了。OS怎么管理这么多的块组,分区,磁盘?先描述再组织!!因此OS内一定存在一个结构体,结构体中一定包含磁盘的大小,各个分区的起始LBA地址和终止LBA地址(这与之前的虚拟地址空间的划分一样)等等。
接下来只需要知道磁盘是如何管理一个块组,就可以知道OS是如何管理好磁盘的。
文件=文件内容+文件属性,所以在磁盘中要么存的是文件内容,要么存的是文件属性。需要注意:在linux系统中文件内容和文件属性是分开存储的。
一个组中应该包含哪些内容?常规的文件数据,管理文件数据的管理信息(管理者与被管理者)。一个块组中具体包含以下内容:Super Block,GDT,Block Bitmap,inode Bitmap,inode Table,Data Blocks。其中Data Blocks,inode Table是常规文件数据;Super Block,GDT,Block Bitmap,inode Bitmap是管理文件数据的管理信息。
无论一个组中是否存在文件,即Data Blocks是空的,管理文件数据的管理信息都一定要提前写入组中。写入管理信息的过程称之为写入文件系统 。分区完成之后,分组并且向块组中写入管理信息的过程,这在日常生活中叫做格式化。格式化的过程就是写入全新文件系统的过程。
文件=文件内容+文件属性,在一个块组里是怎么存储文件的内容的?
组中大部分空间存储的都是文件数据,而这些空间都是以4KB的数据块组成,假设文件的内容大小为16KB,那么会选取4个数据块来保存文件内容,Data Blocks 中每一个数据块都有唯一编号。数据块的编号转化为扇区的 LBA 地址,LBA地址传给磁盘,定位到磁盘的具体扇区,找到扇区之后就可以将文件内容写入到磁盘里。
一个组块里是怎么存储文件的属性的?
首先需要明白文件的属性是否是数据?当然是数据。是数据就需要保存,那么文件的属性存储在哪?怎么存储的?在回答这些问题之前,需要知道在内存中由一个struct inode结构体 保存文件的所有属性,struct inode 结构体的大小是固定的,一般是128字节。在 OS 中创建一个文件,首先会在内存中定义一个 struct inode 结构体,将新建文件的属性填充到 inode 结构体中,接着将 inode 结构体的二进制数据写入磁盘的 inode Table 中,这样的文件的属性就被保存起来了。那么文件=文件内容+文件属性,就可以理解成文件 = datablock + inode 结构体。
需要注意的是文件名不属于文件的属性 ,即文件名不会保存在 inode 结构体中。块组中不仅仅只有一个文件,文件变多了,就需要对它们进行区分,理论上一个文件一个 inode 编号,该编号由其在 inode Table 中的位置决定,无需在 inode 结构体内部存储。当文件被访问时,内核将磁盘 inode 加载到内存中的 struct inode 结构体,用于运行时管理。
如何确定一个文件存在 inode 编号这种东西呢?使用指令 ls -l -i 指令查看:

文件属性最开头的那个数字 920722 就是 test.c 的 inode 编号。
inode 结构体的源码:
cpp
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
union {
struct {
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag; /* Fragment number */
__u8 m_i_fsize; /* Fragment size */
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
};
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
3.块组内部的构成
inode Table 是什么?inode Table 也是由 4KB 大小的数据块构成,而一个 struct inode 结构体的大小为128字节,inode Table 中一个 inode 存储块的大小为4KB,即4096字节,那么 inode Table 中一个 inode 存储块中就可以保存32个 inode 结构体,即一个 inode 存储块中可以保存32个文件的文件属性。32个文件的 inode 是放在一块的,也就是说一个文件会与其它文件共享 inode 存储块。将 inode Table 想象成一个数组,数组的内容为文件的 inode 结构体。
为什么文件的inode 结构体的大小是在文件系统创建时固定的 ?难道文件的属性是固定的吗?是的,所有文件拥有的属性都是一样的。inode 结构体的大小是固定的便于在 inode Table 中进行高效的索引和管理。
现在看来 inode Table 是一个由固定大小 inode 结构体组成的逻辑数组,OS 在访问时通常以 4KB 块为单位加载。文件的内容存储在由 inode 结构体指向的数据块中,这些数据块本身没有编号,也不构成表结构。每个文件通过唯一的 inode 编号关联其属性和内容。
inode 结构体怎么与文件内容关联在一起?
在 inode 结构体中存在一个属性:__le32 i_block[EXT2_N_BLOCKS]。存储该文件的内容用到了Data Blocks 中的哪些数据块,这些数据块的编号会保存在 __le32 i_block[EXT2_N_BLOCKS]中。因此在一个块组里,只需要找到文件的 inode 编号,就可以找到文件的 inode 结构体,这样文件的属性就找到了,同时也可以找到对应的块地址索引结构(映射表),找到块地址索引结构(映射表)之后,根据块地址索引结构(映射表)中的块编号找到 Data Blocks 中的数据块,也就找到了对应文件的内容。
在 Data Blocks 中存在多个数据块,有的数据块是已经被使用的,有的数据块是没有被使用的,那么如何知道哪些数据块是被使用的,哪些数据块是未被使用的?管理信息Block Bitmap就是用来区分 Data Block 中哪些数据块是被使用的,哪些数据块是未被使用的。Block Bitmap 是一个位图,比特位的位置表示块编号,比特位为0为1表示 Data Block 中的数据块是否被使用(为0未被使用,为1被使用)。如 Block Bitmap 中第 5 位为 0,表示本块组中相对块号为 5 的数据块未被使用。Block Bitmap 就是用来管理 Data Blocks 的,Block Bitmap 非常小。OS 不可以对磁盘进行位操作,必须要以块为单位进行操作,Block Bitmap 中一个存储块就可以管理 Data Blcok 中8*4096=32768个块。如果 Data Blocks 中的一个块由未被使用状态变为已使用状态,那么 Block Bitmap 中对应块编号的比特位的内容会由0变为1。
在 inode Table 存在多个数据块,有的数据块是已经被使用的,有的数据块是没有被使用的,那么如何知道哪些数据块是被使用的,哪些数据块是未被使用的?inode Bitmap 就是用于管理 inode Table 中的每一个 inode结构体是否已被分配(即是否在使用中)。inode Bitmap 是一个位图,比特位的位置表示块编号,1 表示该 inode 已被分配(正在使用),0 表示该 inode 空闲(可分配给新文件)。当创建新文件时,文件系统查找 inode Bitmap 中为 0 的位,将其设为 1,并初始化对应的 inode 结构体。
那么在Linux系统中如何删除一个文件?将 Block Bitmap 中对应比特位上的内容由1变成0,inode Bitmap 中对应比特位上的内容由1变成0,如此一个文件就被删除了。这也是为什么我们拷贝一份文件使用的时间比删除该文件使用的时间长,因为拷贝文件是切实的将数据写入磁盘中,而删除文件只需要做覆盖。
块组整体是多大?块组从哪里开始,从哪里结束?块组中有多少个块,已经使用了多少个块?整个分组的信息仅凭 Block Bitmap,inode Bitmap,Data Blocks,inode Table 是不知道的。Block Bitmap,inode Bitmap,Data Blocks,inode Table管理的都是一个文件,只凭借Block Bitmap,inode Bitmap,Data Blocks,inode Table 来管理块组是不完整的。
GDT(GroupDescriptorTable)块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始是 Data Blocks,空闲的 inode 编号和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
GDT的源码:
cpp
// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_inodes_count; /* Free blocks count */
__le16 bg_free_blocks_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
有了GDT之后,对于块组的管理更丰满了。然而GDT只是描述一个分组的情况的,整个分区的块组使用情况是不清楚的。
Super Block(超级块)存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:block数据块 和 inode 编号的总量,未使用的 block 和 inode 的数量,一个 block 和inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
Super Block的源码:
cpp
struct ext2_super_block {
__le32 s_inodes_count; /* Inodes count */
__le32 s_blocks_count; /* Blocks count */
__le32 s_r_blocks_count; /* Reserved blocks count */
__le32 s_free_blocks_count; /* Free blocks count */
__le32 s_free_inodes_count; /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
__le32 s_log_block_size; /* Block size */
__le32 s_log_frag_size; /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime; /* Mount time */
__le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
__le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
__le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
/*
* These fields are for EXT2_DYNAMIC_REV superblocks only.
*
* Note: the difference between the compatible feature set and
* the incompatible feature set is that if there is a bit set
* in the incompatible feature set that the kernel doesn't
* know about, it should refuse to mount the filesystem.
*
* e2fsck's requirements are more strict; if it doesn't know
* about a feature in either the compatible or incompatible
* feature set, it must abort and not try to meddle with
* things it doesn't understand...
*/
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
__le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for volume */
char s_volume_name[16]; /* volume name */
char s_last_mounted[64]; /* directory where last mounted */
__le32 s_algorithm_usage_bitmap; /* For compression */
/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_COMPAT_PREALLOC flag is on.
*/
__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
__u16 s_padding1;
/*
* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
*/
__u8 s_journal_uuid[16]; /* uuid of journal superblock */
__u32 s_journal_inum; /* inode number of journal file */
__u32 s_journal_dev; /* device number of journal file */
__u32 s_last_orphan; /* start of list of inodes to delete */
__u32 s_hash_seed[4]; /* HTREE hash seed */
__u8 s_def_hash_version; /* Default hash version to use */
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
__le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* First metablock block group */
__u32 s_reserved[190]; /* Padding to the end of the block */
};
每个块组中都会有一个 Super Block 吗?有多少个组就包含多少个 Super Block 吗?不是的,并非每个组都有一个 Super Block。那么一个分区仅需要一个 Super Block 就可以了?也不是,100个组中可能只有 2~3 个 Super Blcok。为什么一个分区不可以只有一个 Super Block?因为这样可能会存在风险,在前面讲述磁盘时,磁盘是一个机械设备,在盘片旋转过程中,磁头可能与盘片发生碰撞/摩擦,如果不小心影响到了分区的 Super Block 区域,若分区中只有一个Super Block,唯一的Super Block被影响了,整个分区也就用不了了,分区中的数据也就没有了,这样风险太大了。将 Super Blcok 放在2~3个块组里,本质上就是对文件系统进行备份,万一某一个块组的Super Block 中的信息与真实情况对不上时,就会将其它块组中的 Super Block 的信息覆盖到这个块组的Super Blcok 中。一个块组组中核心的内容就5个:GDT,Block Bitmap,inode Bitmap,Data Blocks,inode Table;比较特殊的就是 Super Block,它会被多个组共同使用。
为什么 GDT 不备份呢?即便GDT被影响了,影响的也是它所处的组块,影响范围有限。
Super Block 一般被用来表示一个文件系统,GDT,Block Bitmap,inode Bitmap,inode Table,Data Blocks是磁盘内的数据,CPU 是不能直接在磁盘上修改数据的,需要将这些数据加载到内存中再来修改。
4. inode 编号与块号
在linux中如何创建,删除,修改,查看文件?
OS如何创建文件?
在 linux 中真正标识一个文件的唯一性并不直接是文件名。现在目光只聚焦于一个分组,在 linux 中创建一个文件,touch 命令会变成一个进程,进程在内核里调用系统调用来创建文件,每个文件都有对应的 inode 结构体。在 linux 中创建一个文件时,文件系统会在目标块组的 Inode Bitmap中寻找一个空闲位(0),将其置为 1,并据此分配一个 inode 编号;随后在 Inode Table 中初始化对应的 inode 结构体,并在父目录的数据块中添加"文件名 → inode 编号"的映射关系。至此,空文件创建完成。当写入数据时,文件系统再从 Block Bitmap 分配空闲数据块,将内容写入 Data Blocks,并更新 inode 的块指针和文件大小。
OS如何删除文件?(认识不完全,后续会给出确切的答案)
创建一个文件会得到该新建文件的inode编号,删除一个文件的前提得要知道文件对应的 inode 编号。根据文件的 inode 编号,定位其所在的块组,释放该文件占用的所有数据块(在 Block Bitmap 中将对应位清 0),释放其 inode 结构体(在 Inode Bitmap 中将对应位清 0)。
文件的内容和属性仍然存在并没有被删除,因此我们可以知道删除一个文件,文件并没有真正被删除,所以若在linux中误删一个文件是可以恢复的,只要知道被删除文件的 inode 编号,并且编号没有被覆盖,并使用特定的工具,就可以恢复删除的文件。因此若在 linux 中误删了一个重要的文件,此时应该什么都不要做,尤其是创建新文件,然后交给专业人士来恢复。windows 也是如此,删除一个文件,并且将回收站中的文件也删除了,但是文件并没有真正的被删除。
OS如何修改文件?
文件=文件内容+文件属性,在知道文件的 inode 编号前提下,就可以确认文件的合法性,再将对应的 inode 结构从磁盘的 Inode Table 加载到内存,所有修改(属性或内容)均在内存中进行。最后再将修改后的文件属性和内容由内核写入到磁盘中。无论是修改文件属性还是修改文件内容,整个过程都是读改写。
OS如何查看文件?
文件=文件内容+文件属性,查看文件无非就是查看文件内容或文件属性,要看文件属性或文件内容,也必须要知道文件的 inode编号。inode 知道了,根据 inode 编号找到文件的 inode结构体,inode 结构体找到之后文件属性自然也就知道了,再根据 inode 结构体中的映射表,找到文件的内容,如此文件内容和文件属性就找到了。
5.目录与文件名
在讲述怎么删除,修改,查找文件时,都有一个共同点 ------ 得要知道文件的 inode 编号。但是,我们在实际删除,修改,查找文件时并没有使用 inode 文件编号,而是使用文件名的呀?既然我们在对文件进行操作时,使用的都是文件名,理论上讲文件名是文件属性,而文件属性都存储在文件的 inode 结构体中,但是前面特别提到过文件名不保存在inode结构体中,既然如此文件名保存在哪?
要回答这个问题,得要深入了解目录。linux 下一切皆文件,那么目录是文件吗?当然是文件,既然是文件,那么目录当然也有自己的 inode 编号;既然是文件,文件=文件内容+文件属性,目录的文件属性是什么?与普通文件一样(映射表,权限等等)。目录的文件内容是什么?目录的文件内容就是当前目录所包含的文件的inode和对应的文件名的映射关系,即文件名->inode编号。那么文件的文件名和对应的 inode 编号的映射关系是否是数据?当然是!既然是数据,那么能否保存在数据块中?当然可以!
所以当我们使用 ls 指令时,ls 是如何知道要显示哪个文件的信息?ls 向内核请求获取 xxx 的文件属性;内核根据 ls 指令当前工作目录的路径,获取到该目录的 inode 编号,根据目录的编号定位目录的 inode 结构体;内核读取该目录文件的 Data Blocks,在其中查找名为 "xxx" 与对于inode 编号的映射关系;找到后,获得其 inode 编号,并加载对应 inode 结构体的属性;最后将属性返回给 ls,由其显示。
在磁盘和文件系统角度,存储目录和存储普通文件有区别吗?没有任何区别!!唯一差别就是目录与普通文件的文件内容。那么 OS 是如何区分目录和普通文件的?获取到它们的 inode 编号,找到文件属性,文件属性中有标志位记录着文件的类型,据此来区分目录和普通文件。
在讲解文件的权限时,曾提到过目录的权限,若没有 r 权限,无法列出目录中的文件名;若没有 w 权限,无法修改当前目录下的文件;若没有 x 权限,无法进入到一个目录(即无法访问路径中的任何内容)。这是怎么做到的?为什么没有相对应的权限就不能执行某些功能?因为目录的内容保存着它所包含的文件的文件名和对应 inode 编号的映射关系。若没有 w 权限,即便创建了一个文件,文件的文件名和对应的 inode 编号的映射表不能写入到目录的文件内容中,文件名与 inode 编号的映射关系建立不起来,那么创建文件就会失败。每个目录在磁盘上都有一个 inode结构体,其中 i_mode 字段记录了其类型和权限,当内核需要访问该目录下的任何内容时,必须先加载其 inode 结构体,然后检查 i_mode 中的执行位,是否允许当前进程"穿过"该目录。若无 x 权限,内核在路径解析阶段就拒绝操作,根本不会尝试读取目录的 Data Blocks。
怎么证明目录的内容就是它所保存的文件的 inode 编号和对应文件名的映射关系?
演示代码:
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
exit(EXIT_FAILURE);
}
DIR *dir = opendir(argv[1]);
if (!dir)
{
perror("opendir");
exit(EXIT_FAILURE);
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL)
{
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
{ continue; }
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}
演示结果:

访问当前目录中所包含的文件内容,由结果可以得知目录的内容就是它所保存的文件的 inode 编号和对应文件名的映射关系。
此外我们也可以知道在同一个目录中,文件名必须唯一,因为目录的内容是一个以文件名为键的映射表(文件名 → inode 编号),键的重复会导致路径解析歧义,因此文件系统禁止同一目录中存在同名文件。
6.重新认识 inode 编号与块号
通过 inode 编号找到对应的文件的内容和属性,是在一个块组里面的找的,但是一个分区分成了多个块组,如何知道 inode 编号所对应的文件的文件内容和属性在哪个块组里?需要知道:inode编号和存储文件内容的数据块的块号,不是组内唯一,而是整个分区内唯一。例如:块组0里存在一个 inode 编号为600,那么其它组里就不可能存在 inode 编号为600的,inode 编号和组块是整个分区唯一的。因此获取到一个文件的 inode 编号就可以在整个分区中找到对应文件所在的块组。但是需要注意的是 inode 编号和块号是不跨分区的,每个分区的文件系统都是独立的。
在一个分区内部,一个文件系统内部,有多少个 inode 编号,有多少个数据块,都是固定的,都是提前设计好的。例如一个分区的大小为100GB,假设分为10组,每组10GB,那么每个组里有10(GB)*1024(MB)*1024(KB) / 4(KB) = 2,621,440个数据块;假设每100个数据块分配一个 inode 编号,那么就有2,621,440 / 100 = 26,214个 inode 编号;使用 inode Bitmap 位图来表示这些inode,inode Bitmap 中一个存储块大小为4KB,共4*1024*8= 32,768 byte,1个比特位表示一个inode 编号,那么仅需要26,214/32,468 = 1个inode Bitmap存储块就可以表示这些 inode 编号;每个组里一个2,621,440个数据块,那么仅需要2,621,440 / (4*1024*8) = 80个Block Bitmap中的存储即可。
在一个分组里,inode编号被使用完了,但是Data Blocks中的数据块没有被使用完,这种情况存在吗?存在,并且很常见。存储大量小文件,每个文件至少占用 1 个 inode + 1 个数据块(即使只有 1 字节);例如:100 万个 1KB 的文件就需要消耗 100 万 inode + 约 25 万数据块(4KB 块);如果文件系统默认 inode 密度较低(如 1 inode / 16KB),inode 会先耗尽。inode耗尽后,此时无法创建新文件(即使还有很大的空间)。
在一个分组里,inode编号没有使用完,但是Data Blocks中的数据块被使用完了,这种情况存在吗?存在,并且很常见。存储少量大文件,例如:10 个 4GB 的文件会消耗 10 个 inode + 约 100 万个数据块;如果文件系统 inode 密度很高(如 1 inode / 4KB),数据块会先耗尽。数据块耗尽后,此时无法写入新数据。从逻辑上来说,一块磁盘的整体使用率是很难达到100%的。
假设一个分区中共有10个块组,每个块组中有10000个 inode 编号,那么 inode 编号为10240的文件在哪个分组里? inode 编号全局连续:1, 2, 3, ..., 100000;块组 0 管理 inode 1 ~ 10000;块组 1 管理 inode 10001 ~ 20000。因此,inode 10240 属于块组 1,是该组的第 240 个 inode(偏移 239)。在块组 1 的 inode Bitmap 中,第 239 位对应 inode #10240。同理,数据块编号也在整个分区中连续,由 Block Bitmap 按块组管理。
因此知道一个文件的 inode 编号,可以确定其 inode 结构体所在的块组;知道其数据块的块号,可以分别确定每个数据块所在的块组**。**
7.路径解析
当访问一个文件时,内核会从其父目录的 inode 出发,读取该目录的数据块,查找文件名对应的 inode 编号,从而定位到目标文件的属性和内容。
例:访问 lesson26 中的 test.c 文件,test.c文件所处的路径为:/home/zs/Linux/lesson26。
要访问 test.c 文件,就需要打开 lesson26,访问 lesson26 的文件内容;要访问 lesson26 的文件内容,就需要获取到 lesson26 的 inode 编号。但是 lesson26 也是 Linux 目录下的一个文件呀?Linux 目录的文件内容存储的就是 Linux 目录下所包含的文件的文件名与对应 inode 编号的映射关系,所以要获取到 lesson26 文件的 inode 编号,就需要打开 Linux 目录,访问 Linux 目录的文件内容,要访问 Linux 文件的文件内容就需要获取 Linux 文件的 inode 编号;但是 Linux 也是 zs 目录下的一个文件呀?如此递归重复相同的操作。因此要访问 test.c 文件,就需要将沿路上所有的目录都打开,找到它们所包含的文件名与文件的 inode 的映射关系。
真正访问某个文件,不像我们上述分析的那样是倒着找,那只是逻辑推演。回归根本,要访问某个文件,就需要有一个直接能够被打开的目录,能够被我们用户所访问的目录,它就是根目录,而不是还需要找到该目录的inode编号,因此根目录是固定的,是所有绝对路径的起点。内核使用根目录的 inode 作为路径解析起点,就能够访问根目录下的所有文件。若要访问 lesson26 目录下的 test.c 文件,打开根目录后要访问哪个文件?需要要访问的 home 目录,那么在根目录下找到home 目录的文件名与它的inode编号的映射关系,获取到 home 目录的 inode 编号,加载 home 目录的 inode 结构体并读取其数据块,访问 home 文件的文件内容。因此我们要访问任何一个文件,linux 内核都需要为我们做从根目录开始的路径解析 。由此可以得出,访问任意文件都必须要有路径,尽管没有写路径,进程都会在cwd中获取对应的路径,并与文件名合并形成一条完整的路径。
从"访问任何一个文件,linux 内核都需要为我们做从根目录开始的路径解析"这句话来看,难道每次访问一个文件,都需要从根目录开始做路径解析吗?不需要,做路径解析,每次访问文件时都需要访问磁盘,进行重复的磁盘IO,这样对 linux 内核的压力太大了,并且也很慢。对于用户访问过的路径,OS是会做路径缓存的。其中访问一个文件的路径上的路上路径也会被缓存,如访问/home/zs/Linux/lesson26/test.c,/和 /home,/home/zs,/home/zs/Linux/,/home/zs/Linux/lesson26 也会被缓存。若有些路径长时间没有被使用,就会被删除。
缓存的路径有当前正在使用的,经常使用的,长时间未使用要被删除的,缓存的路径这么多,那么是否需要将它们管理起来呢?当然需要!怎么管理?先描述,再组织。描述缓存路径的结构为:struct dentry。
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];
};
在 OS 内核中会构建出一棵多叉树,来管理缓存的路径,逻辑上 OS 对 dentry 结构体的管理结构如下所示:

后续用户打开的文件越来越多,这棵树也会越来越大。这棵多叉树就是在初始 linux 时见到 linux 目录结构树的子集。

若在打开一个文件时,路径上的结点文件并不存在于 dentry 树中,那么会依据该结点文件构建struct dentry结构体,链接到 dentry 树中。之前我们曾学习过一个命令------find,在linux中查找对应的文件,第一次查某个文件时,所需的时间会较长,但是第二次查时,会很快,这是因为第一次查时,路径组件的 dentry 未缓存,需要逐级解析,目录的 Data Blocks 未缓存,需要大量磁盘 I/O 读取文件名列表;第二次查时,dentry 已缓存,而且目录内容(Data Blocks)已在 Data Blocks中的数据块中,无需磁盘 I/O。
这棵 dentry 树会动态变化吗?会。它随文件访问而增长,随内存回收而缩减。dentry 结构体中存在着这样一个属性:struct list_head d_lru,未使用的 dentry 会被 LRU 机制回收,以节省系统资源。
dentry 树表示的是以根目录为起点的全局命名空间,所有缓存的 dentry 结构体都隐含在一个绝对路径上下文中。那么相对路径呢?使用相对路径访问当前路径下的文件,首先需要确定当前路径是否存在缓存树中,如果当前路径存在,就以当前路径为参照点在树中查找;如果当前路径不存在,就需要从根目录开始对当前路径做路径解析。因此无论是绝对路径还是相对路径,都可以通过这棵树查找指定的文件,只不过相对路径要通过它参照的绝对路径来查找。绝对路径是一种特殊的相对路径,它的参照点是根目录。
只有目录才会存在 dentry 结构体吗?不是!普通文件也有 dentry 结构体,只不过是 dentry 树的叶子结点。在之前我们使用进程打开一个文件:fopen("test.txt", "xxx"),在打开 test.txt 文件之前,内核需要对 test.txt 所处路径进行路径解析与缓存,最终找到 test.txt 文件。每一个被访问的文件都要有 dentry 结构体,包括普通文件。
需要注意的是在打开的文件的 strcut files_strcut 结构体中存在着 strcut dentry *f_dentry 属性:
cpp
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct dentry *f_dentry; // 在这里
struct vfsmount *f_vfsmnt;
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;
unsigned long f_version;
void *f_security;
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
};
f_dentry 指向自己在缓存路径树中的结点(dentry结构体),根据自己的 dentry 结构体中的指向 inode 结构体的指针找到inode结构体,这样一来就找到自己的文件内容和文件属性了。如此打开的文件和磁盘上的文件就关联起来了。file → dentry → inode → disk blocks,就是"打开的文件"与"磁盘文件"建立关联的核心机制**。**
访问任何一个文件时,内核会从根目录或当前工作目录开始,逐级进行路径解析:对于路径中的每一级目录,先获取其 inode 编号,再读取其数据块(目录内容),从中查找下一级组件的文件名,获得对应的 inode 编号。最终,通过最后一级目录的目录项(文件名->inode编号),找到目标文件的 inode 编号,并加载其 inode结构体,从而访问文件的属性和内容。
但是平常我们访问一个文件时,使用的都是文件名,路径是谁来提供的?
- 访问文件,都是指令/工具访问,本质就是进程访问,进程有cwd,因此进程会提供路径。那么进程的cwd从哪的?默认都是从它的父进程bash来的。而父进程bash的路径来自当前系统和环境变量。
- open文件,提供了路径
那么最开始的路径从哪来?我们在磁盘文件系统中新建文件,都会在系统指定的目录下新建,这就是天然的路径。因此 Linux 路径结构是系统和用户共同构建的。
8.挂载
文件的存储单位是块,假设一个分区的大小为200GB,分了20个组,每组大小10GB,如果要保存的文件大小为30GB,很明显一个块组是保存不了的,那么该怎么办?我们知道在文件的 inode 结构体中存在着这样的一个成员:__le32 i_block[EXT2_N_BLOCKS],内部存储着与保存当前文件的文件内容所需要的数据块的块号。那么这个数组有多大呢?一共15,表示 15 个块指针。
cpp
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
一个数据块大小4KB,__le32 i_block[EXT2_N_BLOCKS] 可以存储15个块指针,一个快指针指向一个数据块?__le32 i_block[EXT2_N_BLOCKS] 是否只能存储15*4=60KB大小的文件?是否就说明 Linux 中一个文件的大小最大就是60KB?并不是。
对于保存大文件,linux 对此有对应的解决方案:
__le32 i_block[EXT2_N_BLOCKS] 可以存储15个块指针,前 12 个(索引 0 到 11)是 直接块指针,每个直接指向一个数据块。第 13 个(索引 12)是 一级间接块指针,指向一个包含更多数据块指针的块,一级间接块可索引 1024 【一个一级间接块的大小为4KB,一个数据块的块号大小为4byte,4*1024byte / 4byte = 1024】个数据块,共 1024 × 4KB = 4,194,304 字节 = 4MB。第 14 个(索引 13)是 二级间接块指针,指向一个包含一级间接块指针的块,一个二级间接块可索引1024个一级间接块,一级间接块可索引 1024 个数据块,所以一个二级间接块可以存储1024*1024*4KB = 4GB。第 15 个(索引 14)是三级间接块指针,指向一个包含二级间接块指针的块,一个三级间接块可索引1024(4*1024/4)个二级间接块,一个二级间接块可索引1024个一级间接块,一级间接块可索引 1024 个数据块,所以一个三级间接块可以存储1024*1024*1024*4KB = 4TB。

假设一个分区的大小为200GB,分了10个组,每个组的大小为20GB,假设要在某个组内保存的文件大小为50GB,该块组的 Data Block 没有那么大呀?这该如何保存?我们知道块号是整个分区内有效的,块号是跨块组的,__le32 i_block[EXT2_N_BLOCKS] 中保存的块号不仅可以是当前块组的数据块的块号,也可以是该分区其它块组的数据块的块号。当前块组不能保存大数据,可以使用其它块组中 Data Block 中的数据块,直到能够保存。假设要在块组 5 中存储大文件,当前块组中的数据块存不下,此时会将快组5中未被使用的数据块全部使用了,接着按照顺序查找的方式,先查找快组6中的Block Bitmap,看块组6还有哪些未被使用的数据块,接着是块组7,如果循环到块组9,数据块大小还不够,会回到快组0去查找。块号是分区唯一,不是块组唯一;块内不仅仅存储文件自己的数据,也可以存储自己文件中用到的更多块号。
为什么块号和 inode 编号是跨组块的?superblock 存储了整个分区的全局布局信息(块大小、块组结构、总数等),内核才能将全局块号转换为磁盘物理位置,将 inode 编号映射到具体块组,跨块组分配和访问数据,实现大文件的存储。
前面都是聚焦于一个分区,但是磁盘里不只有一个分区呀?怎么知道你要访问的文件在哪个分区里?每个分区都有自己的文件系统,inode 编号和块号不具有跨分区性,分区1中有 inode 编号为100的文件,分区n中也有 inode 编号为100的文件。
写入管理信息的过程称之为写入文件系统。分区完成之后,分组并且向块组中写入管理信息的过程,这在日常生活中叫做格式化 。格式化的过程就是写入全新文件系统的过程。格式化的本质就是在写管理信息,我们将这套管理信息叫做文件系统,全称叫做磁盘级文件系统。格式化是OS执行的,OS的四大功能:进程管理,文件管理,内存管理,驱动管理。分区格式化之后,该文件系统或者分区能被直接使用吗?不能。文件系统/分区怎样才能被使用呢?需要将文件系统或分区挂载在指定目录下。
使用指令:ls /dev/vd*,查看当前云服务器有几个磁盘:

vda 是具体一个磁盘的名称,而 vda1 是磁盘的分区,vda 被分成一个分区。
使用指令:df -h 查看已挂载文件系统的磁盘空间使用情况。

mount on 就是挂载的意思,由上图可知,分区1挂载在根目录下,由于磁盘只有一个分区,所以后续使用的都是vda1分区,新建的文件都是在 vda1 分区下新建的。分区被挂载到指定目录下之后,该分区就可以被使用了,也就可以实现平时我们执行的文件的操作了。
怎么证明分区需要格式化并且需要挂载在指定目录下才能被直接使用?
linux 下存在许多虚拟设备,如/dev/zero:

制作一个大的磁盘,当作一个分区,将 /dev/zero 当作一个分区。使用指令:dd if=/dev/zero of=./disk.img bs=1M count=5。将 /dev/zero 当作一个分区,形成一个大小为5MB的文件,文件名为disk.img。

可以将disk.img理解成一个小分区,现在这个分区不能直接被使用,需要格式化。怎么格式化?向分区中写入文件系统。格式化命令:mkfs.ext4 disk.img(ext2,ext3,ext4的文件系统是一样的)

"1280 inodes":inode个数为1280;"5120 blocks":块数量为5120;"256 blocks (5.00%) reserved for the super user":有5%的块数量来保存super Blcok;"1 block group"分区分为1个组;"8192 blocks per group":每个组8192个数据块;"1280 inodes per group":每组1280个 inode 编号。
命令执行完毕之后,disk.img 分区就格式化完毕了。格式化完毕之后,可以直接使用 disk.img 分区了吗?不能,还需要将 disk.img 分区/文件系统挂载在指定目录下。挂载到指定目录得要有目录呀?现在没有指定目录,我们可以创建一个空目录,让 disk.img 挂载到该空目录下。mkdir /mnt/myvda0,在系统的 mnt 目录下,新建一个空目录 myvda0。

指定目录创建完毕之后,将 disk.img 挂载到该目录下,使用指令:sudo mount -t ext4 ./disk.img /mnt/myvda0/,将分区挂载到指定的目录。
使用df -h 指令查看被挂载的分区:

/dev/loop0 就是挂载的分区。挂载到指定目录下之后,就可以直接使用该分区了,在 myvda0 中创建一个文件 test.c。

怎么卸载分区?使用指令:sudo umount /mnt/myvda0。

接下来回答开头提出的问题:如何知道访问的文件在哪个分区中?文件要被访问是需要有路径的,如 /mnt/myvda0/test.c,test.c 前面的 /mnt/myvda0 不就是 disk.img 分区吗?因此根据要访问的文件的路径前缀(挂载点)来区分文件在哪个分区中。
9.文件的总结
访问磁盘中已有的文件的具体过程:
用户提供一个文件的绝对路径 。内核根据挂载点信息,遍历已挂载的文件系统,找到该路径所属的分区。找到文件所在的分区后,进行路径解析,得到文件的inode编号,通过特定的计算公式,根据inode编号计算文件所在的块组,读取该块组对应的块组描述符(位于块组描述符表中),从中获取 inode 表的起始逻辑块号。根据 inode 编号在块组内的偏移,在 inode Table中定位到具体的 inode 结构体并读取,从而获取到文件的属性,再根据 inode 结构体中的__le32 i_block[EXT2_N_BLOCKS],找到存储文件内容的数据块编号,根据这些编号找到Data Blocks中对应的数据块,从而获取文件的内容。
在磁盘创建一个新的文件具体过程:
用户在某路径创建一个新的文件,根据挂载点信息,确定该路径所属的分区,进行路径解析,解析到当前目录,获取当前目录的inode编号,检查其 i_mode 是否允许写入。读取新建文件所在分区中块组的 inode Bitmap 的信息,找到一个未使用的 inode 位,将该位置为 1(标记已用),这样就为新建的文件得到一个 inode 编号了。OS为新建的文件创建inode结构体,将新建文件的属性初始化inode结构体。根据 inode 编号计算所在块组,读取文件所在块组的块组描述符,获取 inode Table的起始逻辑块号,计算文件的 inode 编号在inode Table中的偏移,从而定位到inode Table中具体的存储块,将新建文件的inode结构体保存在inode Table对应的存储块中。最后读取父目录的数据块(目录内容),保存新建文件的文件名与对应inode编号的映射关系。
向新建文件中写入内容的具体过程:
用户发起 write() 系统调用,传入文件描述符和数据,通过 fd 找到对应的 struct file,进而获取文件的 inode 编号。根据文件的 inode 编号找到文件的inode 结构体,验证文件的 inode 结构体中的 i_mode 是否允许写入。随后,读取文件所在的组块的Block Bitmap信息,找到一个未使用的数据块,将块位图中对应位置为 1。将对应的数据块的编号保存到inode结构体的i_block数组中,再将更新后的 inode 结构体写回 inode Table 中(位置与创建该文件时的位置一样)。
3.软硬链接
1.硬链接
新建文件 test.txt,向文件中写入 "hello linux" 信息。如果要对当前文件建立硬链接,使用指令:ln 源文件 目标链接名。

仔细观察可以发现,test_hard与源文件的属性是完全一样的。

就连文件里的内容也是一样的。看上图显示的文件属性中,权限后面跟着的那个数字"2"就是当前文件的硬链接数。
知道怎么建立硬链接,那么什么是硬链接?我们再来查看 test_hard 和 test.txt 文件的 inode 编号。

可以发现这两个文件的 inode 编号是一样的,而 inode 编号在文件系统中具有唯一性,一个文件一个 inode 编号,既然这两个文件的 inode 编号是一样的,那么可以说明,这两个文件是同一个文件!!因此硬链接和目标文件本质是同一个文件。目录文件内容为目录所包含文件的 inode 编号与文件名的对应关系,有两个文件名对应一个 inode 编号,这不就是C++中所学的别名吗?因此硬链接也可以理解成是文件的别名,建立硬链接的本质其实是在当前目录下新建一个新的字符串(文件名)和目标文件 inode 编号的映射关系。
既然硬链接与目标文件是同一个文件,那么在当前结构下,文件名是否具备唯一性?当然具备。可以理解为 test_hard 与 test.txt 文件名指向同一个文件,硬连接数就表示当前有多少个文件名指向这个文件。硬链接数也是属性,因此也保存在文件的 inode 结构体中。
cpp
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
}
i_link_count 记录的就是当前文件的硬链接数。硬链接数有什么意义?当一个文件的硬链接数大于1时,删除一个文件,并没有真正的删除,必须要将硬链接数减到1再删除,才表示文件真正被删除,先删除文件名与文件的inode 编号的映射关系。
因此 OS 如何删除一个文件?
内核解析路径,最终在目录的数据块中找到文件名与 inode 的映射关系,通过这个映射关系找到inode 编号,再通过 inode 编号找到 inode 结构体,将 inode 结构体缓存到内存中。检查调用者对目录是否有写权限(因为要修改目录内容);检查该 inode 结构体的 硬链接计数(i_link_count) :如果 i_link > 1:仅删除当前文件名,即断开文件名与 inode 编号的映射关系,将文件的 inode 结构体的 i_link 减 1;如果 i_nlink == 1:删除后 inode 将无引用,遍历 i_block[15] 中的所有直接/间接块指针;对每个数据块号:对应块组的 block bitmap 中将该位 置为 0, inode 所在块组的 inode bitmap 中将对应位 置为 0;最后会更新superBlock中的信息。
硬链接只是在当前目录下建立的一个字符串和目标文件inode编号的映射关系,并不是一个文件。为什么要有硬链接?便于对文件进行备份,提供了字符串级别的轻量化的备份方案。建立硬链接,即便源文件被删除了,它依旧存在(除非删除文件是文件的硬链接数为1)。
了解了硬链接数之后,来思考一个问题,为什么新建的普通文件的硬链接数为1,新建的目录的硬链接数为2?

普通文件的硬链接数为1是因为当前文件的文件名与inode编号的对应关系只有一个;对于目录,从表象上来看,当前目录的 inode 编号与文件名的对应关系也只有一个呀?那为什么新建目录的硬链接数为2?这只是从表象上看,在任何目录中都存在隐藏目录:. 和 ..。

.表示当前目录,..表示上级目录。由此可以知道,为什么新建目录的硬链接数为2?因为每个目录默认都会有 . 目录,. 目录与 inode 编号存在一种映射关系。既然如此,就可以理解为:.的本质就是硬链接!!
现在 test 目录的硬连接数为2,打开 test 目录,在目录中新建一个目录,看 test 目录的硬链接数会发生什么变化?

为什么 test 目录的硬链接数变成3了呢?test 目录中新建了一个目录 dir,而每个目录都有.和..目录,..表示上级目录,上级目录不就是 test 目录吗?dir 中的 .. 与 test 目录的 inode 编号存在一种映射关系,所以可以理解为:..的本质也是硬链接。根据对硬链接数性质的概念,我们之后可以仅凭目录的硬链接数来判断当前目录中存在多少个目录?当前目录的硬链接数-2就是当前目录中所包含的目录数。
总结硬链接的意义:1. 进行文件备份;2.构成linux的目录结构。
需要注意的是,我们不可以对目录建立硬链接。

为什么用户不能对目录建立硬链接?这是一个文件的路径:/home/zs/Linux/lesson26,若系统允许用户对目录建立硬链接,那么我在 lesson26 目录下对 Linux 目录建立一个硬链接 Linux_hard,在当前路径下查找文件时,查找到 lesson26,发现 lesson26 中存在一个目录 Linux_hard,打开Linux_hard 目录,访问目录里的内容,Linux_hard 是 Linux 目录的硬链接,打开 Linux_hard ,访问 Linux_hard 里的内容,不就是打开 Linux 目录,访问 Linux 目录中的内容吗?这样一来不就形成一个环状路径 了吗?之后对该路径进行路径解析时,就会出现死循环 。所以为什么用户不能对目录建立硬链接?防止用户造成环形路径,避免死循环。
但是 . 和 .. 不就是对目录建立的硬链接吗?系统可以对目录建立硬链接是客观事实,系统必须这样做。但不是说对目录建立硬链接会形成环形路径,在该路径下访问文件时会造成死循环吗?那么系统是如何解决这些问题的?. 和 .. 是两个特殊的文件名,进行路径解析时遇到 . 和 .. 会直接忽略它们。.和..是一种特殊的硬链接,用来实现绝对路径和相对路径的。此外在我们使用 cd 指令时,不断的cd . ,都是在当前路径,这不就是一种死循环吗?
2.软链接
如何建立软链接?使用指令:ln -s 目标文件名 软链接名。
创建一个 test.c 文件,建立 test.c 的软链接。

查看 test.c 和 test_soft 的 inode 编号:

可以看出 test.c 与软链接的 inode 编号并不一样,软链接有自己的 inode 编号,并且软链接的文件属性l,因此可以知道软链接是一个独立的链接文件。文件=文件内容+文件属性,文件属性与 test.c 的差不多,那么软链接的文件内容是什么?我们直接查看软链接的内容是查看不到,最终看的也只是 test.c 的内容(test.c 和软链接的文件大小都不一样,但是查看的内容是一样的)

软链接文件中保存的是目标文件的路径。比如软链接 test_soft 链接的是 test.c,那么软链接中保存的就是 test.c 的绝对路径。路径也是字符串,也是数据,因此可以存储在软链接的内容中。
为什么要有软链接?在一般写项目时,项目中的文件是很多的。在 lesson26 文件下建立一串目录dir1/dir2/dir3,在 dir3 中建立 test.c 文件,文件中写入打印 "hello linux" 的代码,编译 test.c 形成可执行程序 test。如果我想要在 lesson26 目录下执行可执行程序 test,那么就需要写 test 所处的绝对或相对路径:

这样的话也太麻烦了,为此我们可以对 test 文件建立软链接。当要访问 test 可执行程序时,仅需访问 test 对应的软链接即可。

这样一看,软链接不就类似于 windows 下的快捷方式吗?点击 window 图形化界面上的快捷窗口,就可以直接打开对应的软件。
删除软链接使用指令:unlink 软链接名。

将某个软件或者库安装到系统中,按照之前所学的内容,可以将软件或者库的本身直接拷贝到 linux 系统中。拷贝太消耗空间了,拷贝一份,下载一份,最后再将拷贝删除。如果安装时不允许拷贝,我们可以对软件或库文件建立软链接。
软链接与硬链接的对比
软链接是一个独立的链接文件
硬链接不是独立的文件,只是文件的inode编号与文件名的映射关系
软链接的意义:类似于windows中的快捷方式
硬链接的意义:文件备份;构成linux的目录结构(.和..就是硬链接)
4.文件系统的总结
用户程序调用 open 函数打开文件,当前进程执行 open 函数,进程中的 struct task_struct,struct task_struct 的部分源码如下所示:
cpp
struct task_struct {
struct fs_struct *fs; // 文件系统信息(根、当前目录)
struct files_struct *files; // 打开文件列表
};
struct fs_struct* fs 指向struct fs_struct 结构体,结构体中保存着:root 进程的根目录(/)和 pwd 当前工作目录;struct files_struct* files 指向 struct files_struct 结构体,结构体中保存着fd_array[NR_OPEN_DEFAULT]数组,每个元素是一个 struct file* 类型,指向 struct file 结构体,fd_array 数组的下标就是文件描述符 fd,每个打开的文件都有对应的 fd,通过 fd 找到打开文件的struct file 结构体。struct file 结构体的部分源码为:
cpp
struct file {
struct path f_path; // 包含 dentry 和 mount
const struct file_operations *f_op;
loff_t f_pos; // 偏移量
struct inode *f_inode; // 关联的 inode
};
struct file 结构体中存在 struct path f_path 结构体,该结构体的源码为:
cpp
struct path {
struct vfsmount *mnt; // 挂载点(如 /dev/sda1)
struct dentry *dentry; // 文件或目录的 dentry
};
struct path 结构体中的 struct dentry* dentry 指针指向打开文件的 dentry 结构体,struct vfsmount* mnt 指向打开文件的挂载点。struct dentry 结构体的部分源码如下所示:
cpp
struct dentry {
struct inode *d_inode;
}
struct 结构体中的struct inode* d_inode 指针指向 struct inode 结构体,该结构体是内存级别的,struct inode 结构体的部分源码为:
cpp
struct inode {
const struct inode_operations *i_op;
const struct file_operations *i_fop;
}
struct inode 结构体是 dentry 到磁盘 inode 的桥梁。通过 strcut inode找到磁盘上文件的 struct ext2_inode,exit_inode就结构体中的存在 i_block 数组,数组内存储着存储文件内容的所用到的数据块的块号,通过这些块号找到 Data Blocks 中的数据块。