
✨道路是曲折的,前途是光明的!
📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!
🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

- 一、理解硬件
-
- [1.1 磁盘的物理结构](#1.1 磁盘的物理结构)
- [1.2 磁盘的储存原理](#1.2 磁盘的储存原理)
- [1.3 磁盘的存储结构](#1.3 磁盘的存储结构)
- 思考:如何将数据储存到磁盘?
- [1.4 磁盘的逻辑结构](#1.4 磁盘的逻辑结构)
-
- [1.4.1 理解过程](#1.4.1 理解过程)
- [1.4.2 真实过程](#1.4.2 真实过程)
- [1.5 CHS && LBA地址](#1.5 CHS && LBA地址)
-
- [1.5.1 CHS](#1.5.1 CHS)
- [1.5.2 LBA](#1.5.2 LBA)
- [1.5.3 转换关系](#1.5.3 转换关系)
- [实战演示:LBA = 1000](#实战演示:LBA = 1000)
- 二、重新理解磁盘写入(Write)全过程
-
- [2.1 发起指令(Setup)](#2.1 发起指令(Setup))
- [2.2 地址转换(Translation)](#2.2 地址转换(Translation))
- [2.3 执行写入与阻塞(Execution & Blocking)](#2.3 执行写入与阻塞(Execution & Blocking))
- [2.4 完成与唤醒(Completion & Wakeup)](#2.4 完成与唤醒(Completion & Wakeup))
- 三、文件系统的初探
-
- [3.1 块的概念](#3.1 块的概念)
- [3.2 分区的概念](#3.2 分区的概念)
-
- [3.2.1 分区的本质](#3.2.1 分区的本质)
- [3.2.2 Linux 的视角](#3.2.2 Linux 的视角)
- [3.1.3 视觉化想象](#3.1.3 视觉化想象)
- [3.3 inode的概念](#3.3 inode的概念)
-
-
- [<!-- 这是一张图片,ocr 内容为: -->](#)
-
一、理解硬件
没有打开的文件是储存在磁盘中的。但是如何储存的呢?
1.1 磁盘的物理结构
- 我们小时候都是通过光盘来看电视的,其实数据就储存在这光滑的一面上。

- 其实磁盘也类似于光盘,只不过磁盘的两面都是光滑的,都可以储存数据。
我们先来看看磁盘的物理结构,如下图

- 磁盘是计算机的外设中的一个机械设备,它是永久性存储介质,其优点为储存空间大,价格低;但是缺点是它的存储速度效率比较低下。
注意:盘片的盘面看起来很光滑,其实并不是如此。它的表变有许多密密麻麻的小凸起,这些就是用来存储数据的。
- 一张磁盘通常有三张盘片,就有六个面可以存储数据,每个面都对应着一个磁头;盘主是主轴,控制盘片的运动;磁头借助磁力臂悬浮于盘片上,并且受磁力臂的控制来左右移动。
1.2 磁盘的储存原理
上面我们讲到其实盘片的盘面并不是光滑的,上面其实有很多小凸起是用来储存数据的。那么是如何储存的?
- 这些小凸起其实是磁性物质。
- 我们都知道计算机的底层都是只认识二进制序列的;且磁盘是计算机的外设,当计算机和外设进行IO就意味着外设也必须通过某种方法识别或表征二进制序列。
- 所以磁盘能存数据,是因为盘片上布满了无数微小的磁性颗粒。这些颗粒就像一个个可以被磁化的小磁针。我们可以先把所有颗粒统一方向,比如让它们的北极都朝上,这就代表二进制里的 0。当我们需要存 1 的时候,磁头会产生一个强磁场,把经过的那些颗粒的方向翻转,让它们的南极朝上。这样一来,盘片上就出现了两种状态:北极朝上的是 0,南极朝上的是 1。
- 计算机读取时,只要识别这些颗粒的方向,就能把数据读出来。所以,磁盘存储的本质,就是用磁头改变磁性颗粒的方向,把二进制的 0 和 1 变成物理上的磁极方向。

由于磁盘的价格便宜,储存空间大,所以大公司会用磁盘储存数据,它们则统一在机房,一旦运行终身运行。如果达到年限或者出意外损坏则会进行磁盘销毁而不是出售,因为里面存储的都是数据。

1.3 磁盘的存储结构

- 盘片:由一圈一圈的磁道构成。
- 柱面:同一半径的磁道立体来看形成的柱面
- 扇区:由磁道和圆心辐射线划分成的 "扇形区域",标准大小通常为 512 字节,现代硬盘也常用 4KB(即 4096 字节)。
即扇区是硬盘存储的最小单位。我们就可以把磁盘看做由很多个扇区构成的储存介质。
思考:如何将数据储存到磁盘?
- 根据上面的讲述我们可以总结出:如果想要把数据储存到磁盘,我们必须得找到扇区;找扇区就得去找扇区对应的磁道;找磁道就要找磁道所在的柱面;然后用磁头定位到具体的盘面。
- 所以我们可以将问题转化为如何找到存储的扇区位置?
- 定位盘面 ------ 通过选择磁头来确定
- 定位柱面 ------ 通过移动磁臂来确定磁道
- 定位扇区 ------ 等待盘片旋转到目标位置
这种方法通常称作为CHS寻址方式(C:Cylinder柱面,H:Header磁头,S:Sector扇区)
这样我们对磁盘的机械运动就有了新的理解:在磁盘上读取数据的核心在于定位扇区,机械部件移动的距离越远、次数越多,效率就越低;反之则越高。
正是因为这个物理特性,在软件设计上,我们通常会将相关联的数据尽可能存放在连续的扇区或同一个柱面内。这样在读取时,磁头不需要频繁大幅度移动,就能连续访问所需数据,从而极大地提高了读写效率。
1.4 磁盘的逻辑结构
1.4.1 理解过程
- 大家对如下盒式磁带都不陌生吧,可以用它放听力,放歌等等。
- 其实是盒式录音机就是通过滚动两个滚轴让磁带滚动从而读取数据播放,但是磁带最大的缺点是 "顺序访问"。 如果你想听最后一首歌,或者想重新听一遍刚才的内容,必须先停下来**倒带**,等磁带卷回去才能继续播放,这个过程非常慢。

- 其实,我们可以把磁带从盒子里完全抽离出来,想象成它被**拉直成了一条无限长的直线**。无论磁带是卷着的还是拉直的,数据在上面的排列顺序并没有变。磁头就像是一个在这条直线上移动的 "读数器"。所以,从逻辑上讲,磁带就是**一条线性排列的数据流**。

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

- 这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做LBA (逻辑块地址)。

1.4.2 真实过程
- 其实传动臂上的磁头是共进退的

- 注意:柱面就是一个逻辑上的概念,其实就是在盘面上相同半径的磁道在逻辑上构成的柱面;所以,磁盘物理上分了很多面,但是在我们从逻辑上看,磁盘整体上是由一个个柱面卷起来的。
就如我们吃的山楂卷一样虽然它是一层一层卷起来的,但当你拿在手里看的时候,你看到的是一圈圈的同心圆。我们在逻辑上把硬盘看作是由外到内、像山楂卷一样 "卷" 起来的一层层空心圆柱,而不是一张张平铺的盘片。

- 所以磁盘的真实情况如下:
- 磁道:某一个盘面某一磁道的展开
即是一维数组

- 柱面:所有盘面的同一个磁道,即柱面展开
即是二维数组,且柱面上每个磁道扇区个数一样

整个磁盘:

即整个磁盘在逻辑上可以被看作是一个巨大的三维数组
[柱面][磁头][扇区]。虽然物理上它是由多张盘面堆叠而成的,但我们将所有盘面上相同半径的磁道抽象为一个柱面 。因此,从逻辑视角看,磁盘就像我们吃的山楂卷一样,是由一层层柱面"卷"起来的。
寻址时,我们先确定柱面(数组第一维),再确定磁头(数组第二维),最后定位扇区(数组第三维),这就是 CHS 寻址。
而 LBA 则是将这个三维数组在逻辑上"拍平",变成了 C/C++ 中我们熟悉的一维数组。

所以,每⼀个扇区都有⼀个下标,我们叫做LBA(Logical Block Address)地址,其实就是线性地址。所以怎么计算得到这个LBA地址呢?

操作系统只需使用LBA地址(如LBA=1000)即可完成磁盘寻址,LBA与CHS地址的互相转换,完全由磁盘自身的固件(硬件电路、伺服系统)负责执行。
1.5 CHS && LBA地址
上面我们说到LBA与CHS地址的互相转换,完全由磁盘自身的固件(硬件电路、伺服系统)负责执行,我们接下来我们通过一个形象的比喻和底层的逻辑推导,彻底把这两个概念讲透。
1.5.1 CHS
基于上面的"山楂卷"结构,早期的硬盘寻址采用了 CHS 模式。
- C (Cylinder) 柱面:决定你在山楂卷的哪一圈(最外层的维度)。
- H (Head) 磁头:决定你在这一圈的哪一层"皮"上(中间维度)。
- S (Sector) 扇区:决定你在这一层皮上的哪一小块(最内层维度)。
CHS 的本质:
它就像是一个 三维数组 disk[C][H][S]。
要找到一个数据,你需要三个坐标:先找柱面,再找磁头,最后找扇区。
1.5.2 LBA
随着硬盘容量越来越大,三维的 CHS 寻址变得越来越麻烦(比如不同磁道的扇区数可能不同)。于是,LBA (Logical Block Addressing) 诞生了。
核心思想:把"山楂卷"拍平,LBA 的想法非常简单粗暴: 忽略物理结构,把所有的扇区从 0 开始,一直往后数。
- LBA 0, LBA 1, LBA 2, ..., LBA N。
- 无论这个扇区在哪个柱面、哪个磁头,在 LBA 眼里,它就是一个线性的数字。
LBA 的本质:
- 它就像是一个 一维数组
disk_lba[N]。- 要找到一个数据,你只需要一个下标(Index)。
LBA的优点:
- 简单:操作系统(OS)不需要关心硬盘有多少个磁头、多少个柱面,它只需要告诉硬盘"给我读第 1000 号扇区"。
- 统一:无论是机械硬盘(HDD)还是固态硬盘(SSD),OS 都可以用同样的方式访问。
1.5.3 转换关系
既然 CHS 是三维的,LBA 是一维的,它们之间必然存在数学上的换算关系。
假设硬盘参数:
- H_total:磁头总数(Heads)
- S_per_track:每磁道扇区数(Sectors per Track)
- Per_Cylinder = H_total × S_per_track (单个柱面包含的总扇区数)
- CHS 转 LBA(三维坐标转一维下标)
逻辑:先算出前面所有柱面的扇区数 + 前面所有磁头的扇区数 + 当前扇区的偏移(注意扇区从1开始,需减1)。

- LBA 转 CHS(一维下标转三维坐标)
逻辑:通过整除和取余来"拆解"地址。
//表示整除(取商)%表示取模(取余数)
- 求柱面 C:看 LBA 里包含多少个完整的柱面。

- 求余数:算出在当前柱面内的偏移位置。

- 求磁头 H:看偏移位置里包含多少个完整的磁道。

- 求扇区 S:剩下的就是当前磁道里的扇区(注意结果要加1)。

实战演示:LBA = 1000
假设某硬盘参数:
- 磁头数 (H_total) = 16
- 每磁道扇区数 (S_per_track) = 63
- 单个柱面扇区数 (Per_Cylinder) = 16 × 63 = 1008
问题:LBA = 1000 对应的 CHS 是多少?
- 算柱面 C:
1000 // 1008 = 0 (说明还在第 0 个柱面里)
- 算偏移:
1000 % 1008 = 1000
- 算磁头 H:
1000 // 63 = 15 (说明在第 15 个磁头的位置)
- 算扇区 S:
(1000 % 63) + 1 = 55 + 1 = 56
结果:
LBA 1000 对应的 CHS 地址是 (0, 15, 56)。
结论:
CHS 与 LBA 的互相转换,完全由硬盘内部的固件(硬件电路)自动完成。对操作系统来说,磁盘就是一个巨大的一维数组,下标就是 LBA 地址。
二、重新理解磁盘写入(Write)全过程

当 CPU/操作系统需要向磁盘写入数据时,整个交互流程如下:
2.1 发起指令(Setup)
- 控制寄存器 :CPU 向磁盘控制器的控制寄存器 写入指令(如
WRITE命令),告知磁盘要执行写操作。 - 地址寄存器 :CPU 将目标位置的 LBA 地址 (逻辑扇区地址)写入地址寄存器。
- 数据寄存器 :CPU 将内存中待写入数据的内存地址 (或直接传输数据,取决于总线架构)放入数据寄存器,磁盘控制器会从该内存地址读取数据。
2.2 地址转换(Translation)
- LBA -> CHS :磁盘控制器(固件/Firmware)接收到 LBA 地址后,利用内部算法自动将其转换为 CHS 地址(柱面、磁头、扇区)。
- 物理定位:根据 CHS 地址,磁盘的硬件电路(伺服系统)控制磁臂移动到指定柱面,并驱动相应的磁头寻找指定扇区。
2.3 执行写入与阻塞(Execution & Blocking)
- 状态变更 :一旦开始写入,磁盘将状态寄存器的标志位设为**"忙/未就绪"**(Busy/Not Ready)。
- 进程阻塞 :操作系统不断轮询(Polling)或通过中断机制检测状态寄存器。发现状态为"未就绪"时,操作系统会将当前发起 I/O 请求的进程挂起 ,使其进入阻塞状态(Blocked),并调度其他就绪进程运行(这是多任务系统的关键)。
2.4 完成与唤醒(Completion & Wakeup)
- 写入完成:磁盘成功将数据写入指定扇区。
- 状态更新 :磁盘控制器将状态寄存器 更新为**"就绪"(Ready),并产生一个中断信号(Interrupt)**通知 CPU。
- 进程唤醒 :CPU 响应中断,操作系统得知 I/O 完成,将对应的进程从阻塞队列 移回就绪队列。
- 恢复运行:当进程再次获得 CPU 时间片时,它就从之前的断点继续运行,感知到数据已成功写入。
三、文件系统的初探
3.1 块的概念
- 其实硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个"块"(block)。
- 硬盘的每个分区是被划分为⼀个个的"块"。⼀个"块"的⼤⼩是由格式化的时候确定的,并且不可以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 "块"。
- "块"是⽂件存取的最⼩单位。

:::
注意:
- 磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇区
- 每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
- 知道LBA:块号 = LBA/8
- 知道块号:LAB=块号*8 + n (n是块内第几个扇区)
完整示例(1 块 = 8 扇区)
- 已知 LBA=15 → 块号 = 15//8=1,块内序号 = 15%8=7;
- 已知块号 = 2,要找块内第 3 个扇区 → LBA=2×8+3=19;
- 块号 0 包含的扇区 LBA:0~7;块号 1 包含 LBA:8~15;块号 2 包含 LBA:16~23(以此类推)
:::info

:::
3.2 分区的概念
3.2.1 分区的本质
正如你所说,分区的本质就是切山楂卷
- 物理视角:磁盘是由一个个柱面(Cylinder)堆叠而成的(山楂卷)。
- 分区逻辑 :我们通过设定"起始柱面号"和"结束柱面号" ,把这一大卷山楂卷切成好几段。
- 第一段(柱面 0~1000)给 C 盘。
- 第二段(柱面 1001~2000)给 D 盘。
- 关键点 :因为柱面是磁盘寻址中移动成本最低的单位(不需要移动磁臂),所以柱面是分区的最小单位。
3.2.2 Linux 的视角
Windows 给我们展示的是 C、D、E 这种带有盘符的逻辑驱动器,而 Linux 秉承 "一切皆文件"的哲学,它不使用盘符,而是把磁盘和分区都映射成文件。
- 磁盘本身 :被看作一个大文件。
- 例如:
/dev/sda(表示第一块 SCSI/SATA 硬盘)。
- 例如:
- 分区 :被看作是这个大文件里的"子文件"或"切片"。
- 例如:
/dev/sda1(表示第一块硬盘的第 1 个分区)。 - 例如:
/dev/sda2(表示第一块硬盘的第 2 个分区)。
- 例如:
注意 :这些文件(如 /dev/sda1)并不是普通的文本文件,它们是设备文件 。当你向 /dev/sda1 写入数据时,Linux 内核会把它翻译成磁盘 I/O 指令,最终写入到对应的柱面区间里。
3.1.3 视觉化想象
想象我们把那个 3D 的"山楂卷"(柱面堆叠)拿出来,然后把它像卷纸一样展开,拉直。
- 结果 :你会得到一条长长的一维纸带。
- 纸带上的刻度 :就是柱面号(0, 1, 2, 3 ...)。
- 分区就是在纸带上画线 :
- 在刻度 0 到 1000 之间画个框,写上
sda1。 - 在刻度 1001 到 2000 之间画个框,写上
sda2。
- 在刻度 0 到 1000 之间画个框,写上
这样一来,无论是 Windows 的 C/D/E 盘,还是 Linux 的 /dev/sda1/sda2,本质上都是在这条"平铺的纸带"上划定了不同的区间。

📌 注意:柱面大小一致,扇区个位一致,所以我们只要知道每个分区起始和结束的柱面号。知道每一个柱面多少个扇区,那么该区分多大,解释LBA是多少也就清楚了。

3.3 inode的概念
我们之前说过:文件 = 内容 + 属性,我们使用ls -l的时候除了能够看到文件名,还能够看到文件的内容和属性。

ls -l 每一行的 7 列依次是:
模式(权限) 、硬链接数 、所有者 、所属组 、大小 、最后修改时间 、文件名
到这我们要思考一个问题,文件数据都储存在"块"中 ,那么很显然,我们还必须找到一个地方储存文件的元信息(属性信息),比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。

每一个文件都有一个对应的inode
- Linux下文件的存储是属性和内容分离存储的。
- Linux下,保存文件属性的集合叫做inode,⼀个⽂件,⼀个inode,inode内有⼀个唯⼀的标识符,叫做inode号
我们来看看inode文件长什么样子!
plain
/*
* 磁盘上索引节点(inode)的结构定义
*/
struct ext2_inode {
__le16 i_mode; /* 文件模式(类型+权限) */
__le16 i_uid; /* 文件所有者UID的低16位 */
__le32 i_size; /* 文件大小(字节) */
__le32 i_atime; /* 最后访问时间 */
__le32 i_ctime; /* 节点创建/属性修改时间 */
__le32 i_mtime; /* 文件内容最后修改时间 */
__le32 i_dtime; /* 文件删除时间 */
__le16 i_gid; /* 文件所属组GID的低16位 */
__le16 i_links_count; /* 硬链接数 */
__le32 i_blocks; /* 文件占用的块数 */
__le32 i_flags; /* 文件标志 */
union {
struct {
__le32 l_i_reserved1; /* 保留字段 */
} linux1;
struct {
__le32 h_i_translator; /* 翻译器(Hurd系统使用) */
} hurd1;
struct {
__le32 m_i_reserved1; /* 保留字段(Masix系统使用) */
} masix1;
} osd1; /* 操作系统相关字段1 */
__le32 i_block[EXT2_N_BLOCKS]; /* 数据块指针数组 */
__le32 i_generation; /* 文件版本号(用于NFS) */
__le32 i_file_acl; /* 文件访问控制列表(ACL) */
__le32 i_dir_acl; /* 目录访问控制列表(ACL) */
__le32 i_faddr; /* 碎片地址 */
union {
struct {
__u8 l_i_frag; /* 碎片编号 */
__u8 l_i_fsize; /* 碎片大小 */
__u16 i_pad1; /* 填充字段 */
__le16 l_i_uid_high; /* 以下2个字段 */
__le16 l_i_gid_high; /* 原为reserved2[0](高16位UID/GID) */
__u32 l_i_reserved2; /* 保留字段 */
} linux2;
struct {
__u8 h_i_frag; /* 碎片编号(Hurd系统使用) */
__u8 h_i_fsize; /* 碎片大小(Hurd系统使用) */
__le16 h_i_mode_high; /* 模式高16位(Hurd系统使用) */
__le16 h_i_uid_high; /* UID高16位(Hurd系统使用) */
__le16 h_i_gid_high; /* GID高16位(Hurd系统使用) */
__le32 h_i_author; /* 作者ID(Hurd系统使用) */
} hurd2;
struct {
__u8 m_i_frag; /* 碎片编号(Masix系统使用) */
__u8 m_i_fsize; /* 碎片大小(Masix系统使用) */
__u16 m_pad1; /* 填充字段(Masix系统使用) */
__u32 m_i_reserved2[2]; /* 保留字段(Masix系统使用) */
} masix2;
} osd2; /* 操作系统相关字段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) // 块指针总数(12+1+1+1=15)
// 备注:EXT2_N_BLOCKS = 15
注意:
- 文件名属性并未纳入到inode数据结构内部
- inode的大小⼀般是128字节或者256,我们后面统⼀128字节
- 任何文件的内容大小可以不同,但是属性大小⼀定是相同的
到目前为止,相信大家还有两个问题:
- 我们已经知道硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,读取的基本单位是"块"。"块"又是硬盘的每个分区下的结构,难道"块"是随意的在分区上排布的吗?那要怎么找到"块"呢?
- 还有就是上面提到的存储文件属性的inode,又是如何放置的呢?
其实文件系统就是为了组织管理这些的!

以及此处的EXT2是什么?
✍️ 坚持用 清晰易懂的图解 + 可落地的代码,让每个知识点都 简单直观!
💡 座右铭 :"道路是曲折的,前途是光明的!"
