目录
[2.HDD 机械硬盘硬件结构](#2.HDD 机械硬盘硬件结构)
[(4)块组------Block Group](#(4)块组——Block Group)
[(5)超级块------Super Block](#(5)超级块——Super Block)
[(7)块位图------Block Bitmap](#(7)块位图——Block Bitmap)
[(8)inode位图------Inode Bitmap](#(8)inode位图——Inode Bitmap)
[(9)inode数组------Inode Table](#(9)inode数组——Inode Table)
[(10)数据块------Data Block](#(10)数据块——Data Block)
前言
上篇我们已经了解到在数据交互时操作系统是如何将各种资源包装成文件,进行统一操作。但是操作之前,系统肯定要知道文件的存储地址。所以我们来了解一下文件是被如何存储的。
首先,我们应该知道文件数据存储应该由两部分组成------硬件基础存储加物理寻址功能和软件寻址优化,所以本文将从硬件基础开始往软件寻址架构顺序来讲解。
一.硬件知识------了解磁盘
1.存储设备分类
磁盘有:HDD 机械硬盘(传统盘),SSD 固态硬盘(闪存盘),U 盘 / 移动硬盘,虚拟磁盘等等
不管机械 HDD、NVMe SSD、U 盘、移动硬盘等等,出厂硬件就都对外规定了扇区这个最小存储单元,为了适配操作系统的寻址系统。这些规则也是因为操作系统是为适应传统盘------HDD 机械硬盘而形成的,而其它种类的磁盘只能依靠主控模拟逻辑扇区,适配已有的操作系统体系
接下来将以HDD 机械硬盘为例来讲解磁盘硬件基础存储和寻址功能。
2.HDD 机械硬盘硬件结构
2.1.物理结构

- 主轴(马达)
带动圆形磁盘高速旋转
- 磁盘(盘片)
表面涂有磁性材料,相当于"纸",所有数据永久存储在盘片上。(其实有三片)
- 磁头
相当于"笔",悬浮在盘片上方极近距离,负责读取、写入磁信号;
- 磁头臂
承载磁头,能够径向移动,把磁头移动到不同磁道位置。
- 永磁铁
与音圈电机配合,驱动磁头臂快速摆动寻道。
数据是如何被存储的?
在硬件中,数据存储形式是0和1,刚好一块磁铁是分为南北两极可以代表0和1。
而磁盘上有很多突起点,相当于一个个磁针,磁头有不同磁场就可以改变其方向,所以来存储数据

2.2.存储结构
磁盘以什么规律来存储数据(划分最小存储单元),以什么方式来寻址?

扇区:最小存储单元,传统物理扇区 512 Byte,现代物理扇区 4096 Byte,不过很多 4K 硬盘依然对外上报逻辑扇区 512B(可以为了系统进行兼容),我们以传统512 Byte为准。
#注意区分两个概念:
逻辑扇区:对外,和操作系统接触(LBA 视角,OS 打交道)
物理扇区:磁盘硬件真实读写大小
那么怎么寻址呢?即精准定位到该扇区
通过对磁盘的结构可知,我们只需要三个坐标即可
柱面号,磁头号,扇区号------定位哪个柱面(Cylinder),哪个盘面(Head),哪个扇区(Sector)也就是CHS寻址法
3.操作系统LBA编码
注意:CHS 仅仅绑定 HDD 机械结构,其它种类的存储设备没有相应的物理结构,是不使用的。
那么操作系统是怎么对所有种类的存储设备进行兼容的呢?
**LBA编码------**操作系统只需要提供这一个数字,就能指定要读写哪一块扇区。
那么LBA编码是什么呢?
我们已经知道在存储设备中,都会对外形成逻辑扇区给操作系统使用。那么操作系统就可以对其进行编码,方便系统寻址。这个编码就是LBA------把磁盘上所有可用的逻辑扇区,从 0 开始依次编成一维连续序号
其核心思想:扁平化地址,屏蔽硬件内部结构,将其转换成一维数组
拿HDD 机械硬盘来举例,看如何将CHS转换成LBA:
先展开成一维数组的流程:
1.将磁盘空间按不同柱面展开,那么原本的三维空间就会变成几块二维数组

2.选择磁头将柱面分割成一个个盘面------一维数组

3.将几个一维数组拼接成一块大的一维数组。
数据上转换:
CHS转换成LBA:
LBA = 柱面号 * 每柱面扇区数 + 磁头号 * 每磁道扇区数 + 扇区号 - 1
LBA转换成CHS:
柱面号 = LBA / 单个柱面扇区数;
磁头号 = LBA % (柱面号 * 每柱面扇区数) / 单个磁道扇区数;
扇区号 = LBA % 单个磁道扇区数 + 1。
**总结:**各种类型的存储设备拿到LBA后,会将其转换成自己寻址机制的形式来寻址。
二.系统软件寻址------文件系统
我们已经了解了硬件是如何寻址,并且已经有第一次向下兼容的举措------LBA,那么操作系统会如何高效地管理这些空间来存储文件呢?我们继续了解软件方面------文件系统的架构。
1.文件系统架构

概念解释:
(1)分块------Block
磁盘以扇区为单位读写,但对于操作系统来说空间太小,使用起来要频繁申请空间,效率太低,所以操作系统是以块为单位来使用,多个扇区组成一个块。常见一个块=八个扇区,即4KB。
那么LBA怎么配合呢?
块号=LBA/8;LBA=块号*8+n

(2)分区------Partition
**定义:**分区就是把一块物理磁盘,在逻辑上切割成多个独立区域,也就是C,D,E盘。
特性:
每个分区都有一套独立的文件系统
每个分区是独立:一个分区损坏,一般不会破坏同磁盘其他分区的数据。
本质:
磁盘本身是一整条连续的 LBA 扇区序列,分区表负责把这条长序列切分成若干段独立区间,每一段就是一个分区

(3)inode
我们知道文件=内容+属性,inode就是位于磁盘上,专门用来存储文件的属性的。(我们现在讨论的是磁盘上持久存储的 inode)
内容:
struct ext4_inode {
__le16 i_mode; // 文件类型、权限
__le16 i_uid; // uid低16位
__le32 i_size_lo; // 文件大小低32位
__le32 i_atime; // 访问时间
__le32 i_ctime; // 属性变更时间
__le32 i_mtime; // 修改时间
__le32 i_dtime; // 删除时间
__le16 i_gid; // gid低16位
__le16 i_links_count; // 硬链接计数
// ★最重要:数据块指针数组
__le32 i_block[EXT4_N_BLOCKS];
__le32 i_flags;
// ... 还有扩展属性、高位uid/gid、i_size高位等字段
};
注意:inode 不保存文件名,那么文件名在哪,为什么?后面再讲
特点:
1.不同分区(不同文件系统)的磁盘 inode,可以出现相同 inode 编号,互不冲突。
2.大小固定,因为文件属性大小固定
(4)块组------Block Group
把一整个巨大的分区,切割成若干独立管理片区(块组),用来解决超大分区空间管理效率低下的问题,同时优化机械硬盘性能。
意义:
1.分配资源时,只需扫描当前块组内的元数据,检索范围大幅缩小,查找空闲块更快。
2.inode 与文件内容集中在磁盘一段连续区域,机械硬盘磁头不需要来回远距离移动,减少寻道时间,提升读写速度。
相应的规则:
文件 inode 分配在某一个块组;
文件的数据块尽量在inode 所在同一块组分配
inode不够就换块组,数据块不够可以使用其它块组
(5)超级块------Super Block
分布在每个块组上,拥有多个备份。
**作用:**记录整个分区文件系统的全局信息,提供给内核使用
内容:
文件系统魔数(识别 ext4)
文件系统块大小
分区总块数量、空闲块数量
块组总数
inode 总量、空闲 inode 数量
卷标、UUID、日志相关信息等等
struct ext4_super_block {
__le32 s_inodes_count; // 总inode数量
__le32 s_blocks_count_lo; // 总块数量(低32位)
__le32 s_r_blocks_count_lo;
__le32 s_free_blocks_count_lo; // 空闲块低32位
__le32 s_free_inodes_count; // 空闲inode数量
__le32 s_first_data_block;
__le32 s_log_block_size; // 块大小:1<<log_block_size
__le32 s_blocks_per_group; // ★每个块组最多多少块(32768)
__le32 s_inodes_per_group; // 每个块组inode数量
__le16 s_magic; // ★魔数 EXT4_SUPER_MAGIC = 0xEF53
// 挂载状态、时间、UUID、卷名、日志信息、64位扩展字段等
uuid_t s_uuid;
char s_volume_name[16];
// ...大量扩展字段
};
(6)块组描述符------GDT
存在于块组内
用于描述块组内部信息
struct ext4_group_desc {
__le32 bg_block_bitmap_lo; // 本块组【块位图】所在块号
__le32 bg_inode_bitmap_lo; // 本块组【inode位图】所在块号
__le32 bg_inode_table_lo; // 本块组【inode表】起始块号
__le16 bg_free_blocks_count; // 本组空闲块数量
__le16 bg_free_inodes_count; // 本组空闲inode数量
__le16 bg_used_dirs_count; // 本组目录数量
// 还有64bit扩展字段、校验和等
};
(7)块位图------Block Bitmap
Block Bitmap(块位图)描述内的数据块,用二进制比特位,标记本组内Data Blocks是否有空闲
(8)inode位图------Inode Bitmap
标记本组内每一个 inode 是否空闲。
(9)inode数组------Inode Table
每个块组拥有独立的 inode 表,只存放属于本块组的磁盘 inode,但是按分区内编号,每个分区不同。和 inode 位图一一对应。存放着一个个inode结构体,只是一段连续的 block 数组。
(10)数据块------Data Block
用来存放文件真实内容的块,按分区编号,不可跨区。
不同的文件类型存放内容不同:
普通文件:存放用户真正的文件二进制内容(代码、文本、图片等);
目录文件:存放多条 目录项------「文件名 → inode 号」映射;
2.inode索引数据
2.1文件名和inode的映射
映射关系保存在【目录文件的数据块】里,存储单元叫做目录项。
其结构:
struct ext4_dir_entry {
__le32 inode; // inode编号
__le16 rec_len; // 当前目录项总长度
__le16 name_len; // 文件名长度
__u8 file_type; // 文件类型(普通文件/目录/链接等)
char name[]; // 文件名字符串
};
映射存在的意义:让人类使用文件名,让内核使用 inode,来寻找相应的数据(下面的目录文件,普通文件)。
2.2基于映射和inode来对文件的增删查改
1.查找文件
(1)借助多级目录项映射,将字符串文件名转换为内核唯一识别的 inode 编号;
(2)依靠 inode 内部 i_block\[\] 可以进一步寻址文件数据块
2.新建文件
(1)分配磁盘 inode;
(2)在父目录中建立文件名与 inode 的映射关系;
(3)更新块位图和inode位图
(4)分配数据块,写入内容。

默认块大小 4KB,每个块指针占 4 字节,一块索引表可存 4096/4=1024 个块号。
小文件,inode使用直接块指针索引一块普通数据块
一级间接块指针:数据块数量:4096/4=1024,一级间接单独容量:1024 × 4KB = 4096 KB = 4 MB。
二级间接块指针:数据块总数:1024 × 1024 = 1048576,二级间接单独容量:1048576 × 4KB = 4194304 KB = 4096 MB = 4 GB
二级间接块指针:数据块总数:1024 × 1024 × 1024 = 1073741824
三级间接单独容量:1073741824 × 4KB = 4294967296 KB = 4096 GB = 4 TB
3.修改文件
修改文件内容:
(1)借助多级目录项映射,将字符串文件名转换为内核唯一识别的 inode 编号;
(2)根据写入内容分配数据块(不够就扩)
修改文件属性:改该inode信息即可
4.删除文件
(1)路径解析找到父目录,删除父目录内对应的目录项,断开文件名→inode 映射;
(2)inode 成员 i_nlink 硬链接计数减一;
若 i_nlink > 0:操作结束,inode 和数据块保留(存在其他硬链接);
若 i_nlink == 0:
回收 inode i_block\[\]指向的所有数据块、间接块,块位图对应 bit 清零;
(3)inode 位图对应 bit 清零,inode 标记为空闲。
总结:
先定位------得到inode
对块组成员进行相应的调整
调整目录内容
2.3路径解析
**定义:**一串由/分割的名字序列,作用是逐层通过目录项映射,把字符串名称翻译成目标 inode
我们新建文件时就在创建路径
路径如何寻找文件:
例子:/home/user/test.txt
前提:文件系统挂载时,超级块记录根 inode,对应根目录
1.起始:拿到根 inode(/)读取根目录的数据块,遍历目录项,匹配名称home,得到home目录的 inode 号;
2.进入home目录 inode
读取该 inode 对应的数据块,检索目录项user,拿到user目录 inode;
3.进入user目录 inode
读取目录数据块,检索目录项test.txt,得到目标普通文件 inode;
拿到最终 inode 后,依靠 inode 内 12 直接 + 多级间接指针,访问文件真实数据块。
2.4路径缓存
我们已经知道访问文件过程是从根目录开始,这样的话速度就会太慢了
解决:路径缓存
定义:
内存中形成一串 dentry 链表:
每个 dentry 内部持有指针,关联对应的 struct inode,利用链表快速搜寻。
struct dentry {
// 目录项名称
const unsigned char *d_name;
// 父目录dentry(构建路径树形结构)
struct dentry *d_parent;
// ★关键:关联对应的VFS inode
struct inode *d_inode;
// 用于哈希链表,快速查找dentry缓存
struct hlist_bl_node d_hash;
// 子dentry链表
struct list_head d_subdirs;
// 引用计数、状态标记(有效/负缓存等)
atomic_t d_count;
unsigned int d_flags;
};
内容:
• 每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构;
• 整个树形节点也同时会隶属于LRU (Least Recently Used,最近最少使用) 结构中,进行节点淘汰;
• 整个树形节点也同时会隶属于Hash,方便快速查找;
• 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这 棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径。
内核使用图:

2.5硬链接和软链接
1.硬链接
**本质:**不创建新 inode,只是新增一条目录项,多个文件名映射到同一个 inode。
命令:
ln sr.txt log.txt
ls -i sr.txt log.txt

2.软链接
创建全新 inode,inode内部存放目标路径字符串,不直接指向 inode,短路径(≤60 字节)不分配数据块,路径直接存在 inode 空余区域;超长路径才占用数据块存储路径文本。
作用:读写软链接文件时,内核自动跳转去访问目标文件,可以跨文件系统引用文件;
ln -s log.txt
ls -i

