回顾基础 IO
前面的章节主要围绕已打开文件的操作与理解展开,重点在于说明文件与进程之间的关系。我们知道,文件大体可以分为两类:和。
系统介绍了 Linux 文件系统与底层硬件的关联。内容涵盖机械磁盘的物理结构(磁头、磁道、柱面、扇区)及逻辑寻址(CHS 与 LBA 转换)。详细解析了 ext2 文件系统的组成,包括超级块、块组描述符、块位图、inode 表及数据块的管理机制。文章还探讨了目录与文件名的映射关系、路径解析与缓存(dentry)、以及分区挂载的原理,帮助读者建立从硬件到软件的文件存储完整认知。

前面的章节主要围绕已打开文件的操作与理解展开,重点在于说明文件与进程之间的关系。我们知道,文件大体可以分为两类:和。
对于未打开的文件,它们并不驻留在内存中,而是被保存在磁盘等存储设备上。这就引出了一个新的话题——数据保存的方案。接下来,我们将带领大家深入了解未打开文件的特性和存储方式,并逐步展开相关内容的介绍。
机械磁盘是计算机中唯一的机械设备,优点是:容量大,价格便宜。缺点:慢。而在像腾讯、阿里这样的公司里面,就有自己的机房,机房里面存在上万个机械磁盘,用来存储数据,而多出的机械磁盘则可以外租到外面。
那么,这种磁盘长啥样呢?物理结构又是如何?




柱面其实就是磁道,相同半径的磁道看做一个整体。
一面一个磁头,而磁头之间是共进退的!!!
既然扇区是磁盘存储数据的基本单位。那么如何定位一个扇区呢?
从磁盘存储结构上我们可以知晓:确定磁头,即选择哪一面;确定哪个磁道或者柱面;确定是哪一个扇区,从而定位到一个扇区。

对磁盘特定位置进行寻址:
左右摆动,本质是在定位哪一个磁道 (柱面);
盘片旋转的本质:是确定了那一个磁道 (柱面),定位该磁道 (柱面上) 的哪一个扇区。
文件 = 内容 + 属性 都是数据,无非就是占据那几个扇区的问题!能定位一个扇区了,能不能定位多个扇区呢?

磁头(head),柱面(cylinder),扇区(sector),显然可以定位数据了,这就是数据定位 (寻址) 方式之一,CHS 寻址方式。
CHS 寻址:
对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱面上的第几扇区就可以读到数据了。
但是 CHS 模式支持的硬盘容量有限,因为系统用 8bit 来存储磁头地址,用 10bit 来存储柱面地址,用 6bit 来存储扇区地址,而一个扇区共有 512Byte,这样使用 CHS 寻址一块硬盘最大容量
为 256 * 1024 * 63 * 512B = 8064 MB(1MB = 1048576B)(若按 1MB=1000000B 来算就是
8.4GB)
上面我们都是从磁盘的物理结构进行看待的。这里从零开始,构建对磁盘 OS 级别的理解抽象。

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

如何定位一个扇区呢?

仔细一瞧,这不就是一维数组 sector_array[N] 吗,每个下标都存储着一个扇区。这样每一个扇区,就有了一个线性地址 (其实就是数组下标),这种地址叫做 LBA ( Logic Block Address).
某一盘面的某一个磁道展开:

我们把一个磁面拉开抽象成一个线性结构,一个磁面就具有无数个扇区。而每个磁面的扇区总量是相等的,扇区总数是 s 一个扇区假设有 n 个磁道,这样每个磁道的扇区总个数都是 s/n.

那如何通过 LBA 确定在物理结构的哪一个扇区呢?
假设一个磁面的扇区总数是 100,每个磁道的扇区总数是 10.此时 LBA:124,那么 124/100 就能确定是哪一个磁头,即确定哪一个磁面。124%100=24 即能确定该磁面有多少个扇区。
此时 24 / 10 = 2 就能确定是哪一个磁道 (柱面), 24%10=4 确定是该磁道的第 4 个扇区.
前面我们理解展开的时候,是以一面为单位进行展开的!但是,磁盘真实的展开,并不是以物理盘面展开的。是以"柱面"为单位进行展开的!
整个磁盘所有盘面的同一个磁道,即柱面展开:

而一个磁盘具有多个柱面,这不就意味着存在多个二维数组吗?不就是三维数组吗?
所以,寻址某个扇区:先找到哪一个柱面 (Cylinder) ,在确定柱面内哪一个磁面 (其实就是磁头位置,Head),在确定扇区(Sector),所以就有了 CHS 寻址。

我们之前学过 C/C++的数组,在我看来,磁盘,就是一个以 sector 为单位的一维数组!!!

所以,每一个扇区都有一个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址。那么怎么计算得到这个 LBA 地址呢?
OS 只需要使用 LBA 就可以了!!LBA 地址转成 CHS 地址 (本质是:一维数组的下标,转换成为三个数字!!!),CHS 如何转换成为 LBA 地址。谁做啊??磁盘自己做!固件 (硬件电路,伺服系统)
CHS 转成 LBA:
LBA 转成 CHS:
所以:从此往后,在磁盘使用者看来,根本就不关心 CHS 地址,而是直接使用 LBA 地址,磁盘内部自己转换。
所以:从现在开始,磁盘就是一个 元素为扇区 的一维数组,数组的下标就是每一个扇区的 LBA 地址。OS 使用磁盘,就可以用一个数字访问磁盘扇区了。
其实硬盘是典型的'块'设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个'块'(block)。
硬盘的每个分区是被划分为一个个的'块'。一个'块'的大小是由格式化的时候确定的,并且不可以更改,最常见的是 4KB,就是 4096 字节,一个扇区占 512 个字节,那么就是有 4096/512=8 个扇区。即连续八个扇区组成一个'块'。'块'是文件存取的最小单位。

注意:

至此,我们完成了对磁盘的完整建模的过程!!! 磁盘:本质就是一个 block array[N],块设备!!!
其实磁盘是可以被分成多个分区(partition)的,以 Windows 观点来看,你可能会有⼀块磁盘并且将它分区成 C,D,E 盘。那个 C,D,E 就是分区。分区从实质上说就是对硬盘的一种格式化。但是 Linux 的设备都是以文件形式存在,那是怎么分区的呢?
柱面是分区的最小单位,我们可以利用参考柱面号码的方式来进⾏分区,其本质就是设置每个区的起始柱面和结束柱面号码。此时我们可以将硬盘上的柱面(分区)进行平铺,将其想象成一个大的平面,如下图所示:

柱面大小一致,扇区个数一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面多少个扇区,那么该分区多大,其实和解释 LBA 是多少也就清楚了.
为了能解释清楚 inode,在此之前,我们需要深入了解一下文件系统。
我们想要在硬盘上存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在 Linux 系统中,最常见的是 ext2 系列的文件系统。(其早期版本为 ext2,后来又发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进行了增强,但是其核心设计并没有发生变化,我们仍是以较老的 ext2 作为演示对象。)
ext2 文件系统将整个分区划分成若干个同样大小的块组 (Block Group),实际上就是一个块组对多个块和关键管理结构的组织形式。如下图所示。只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。

存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:block 和 inode 的总量,未使用的 block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了.
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是 inode Table,从哪里开始是 Data Blocks,空闲的 inode 和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
// 磁盘级 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_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};

那么该如何管理好一个小组 group 呢?

在 ext2 文件系统中就有一个 Block Group,它里面含有 Super Block 超级块,Block 块和块的位图,inode 结点和 inode 位图以及 inode Table,最后就是 Data Blocks 数据块,里面就是存储着我们写的数据。接下来小编带着大家理解它们之中的含义以及联系。
我们说过文件 = 文件内容 + 文件属性 -->都是数据 -->既然都是数据,那么都要存储
数据区:存放文件内容,也就是一个一个的 Block。根据不同的文件类型有以下几种情况:

既然每一个数据块,都有唯一的编号 -->一个组可能会存在多个文件,那么该如何确定该数据块是否被其他文件所占用了呢?
Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
还是因为文件 = 文件内容 + 文件属性。既然是数据,那么文件属性也需要存储。
那么 Linux 中,该如何表达文件属性呢?
struct inode{} 结构体!!!
struct inode 表示文件的属性 --> 其大小是固定的,一般是 128 或 256 字节,因为是一样的成员变量。换句话讲,任何文件,它们的 inode 结构体中包含多少种属性,是一样的,但是属性的内容不同!
扩展:OS 读取文件时,会读取 inode,并且一次是 4KB 读取。总共能读取多少个文件的 inode 呢?
41024 个字节,而一个 inode 大小是固定的,假设是 128.那么 41024/128=32 个文件的 inode
一般而言,一个文件一个 inode 一个文件,可能对应 0 或者多个 data block.那该如何标示这个文件的唯一性呢?
inode 结构体里有 inode number
并且在 Linux 中文件名不能也不在 inode 保存!!!(看似不符合我们的期望,放在后面讲解)

每个 bit 表示一个 inode 是否空闲可用.

如果一个文件的内容不是很大,占的数据块不是很多。那么就用 12 个直接块指针,前 12 个映射的是内容,一个数据块占 4KB,那么就可以存 124KB=48KB 的数据。当文件内容过大时,就需要有一级、二级、三级间接块索引表指针:一级间接块索引表指针:inode 里有一个一级间接块指针,它指向一个'索引块'.这个索引块里存放的是更多的数据块指针,每个指针再指向实际的数据块。一个数据块能存 4KB,而每个块指针占 4B 字节,那么一个"索引块"就能存下 4KB/4B=1024 个指针,就能存下 10244KB=4MB 的数据。二级间接块索引表指针:inode 里还有一个二级间接块指针,它指向一个'二级索引块'.二级索引块里存放的是一级索引块的指针,每个一级索引块再指向实际数据块。一个二级"索引块"同样也是 4KB,存放着一级索引块的指针,就是 4KB/4B=1024 个一级索引块指针,那么就可以存放 102410244KB=4GB 的数据。三级间接块索引表指针:inode 里还有一个三级间接块指针,它指向一个'三级索引块'。三级索引块里存放的是二级索引块的指针,二级索引块再指向一级索引块,一级索引块再指向实际数据块。同理就可以存放 102410241024*4KB=4TB 的数据 (使得文件系统可以支持极大的单个文件,比如几十 GB 甚至更大)
总结:有一级、二级、三级间接指针,是为了让文件系统既能高效管理小文件,又能支持超大文件。
数据块的分配是全局有效的虽然磁盘被分成了多个'块组'(block group),每个组有自己的 inode table 和 data block 区域。但数据块编号(block number)是全局唯一的,不局限于某个组.
inode table 和 data block 的映射关系 inode 结构体中的数据块指针,指向的是全局编号的数据块。只要知道块号,就能定位到磁盘上的具体位置,无论它属于哪个组。
文件的数据块可以跨组分配当一个文件很大,或者本组的数据块用完时,文件的数据块可以分配到其他组的空闲块。也就是说,一个文件的数据块可以分布在磁盘的任意位置,不一定都在同一个块组里。
既然有 group 就会存在两种情况:
1.inode 用完,block 没用完这种情况下,你无法再在这个 group 里创建新文件或目录,因为没有多余的 inode 了。但这个 group 里的数据块还可以被其他 group 的文件使用(比如大文件跨组分配数据块)。常见于大量小文件的场景(比如邮件服务器、图片存储等)。
2.inode 没用完,block 用完无法再在这个 group 里为文件分配新的数据块,但还可以创建新文件(只要 inode 还有)。新文件的数据块可能会被分配到其他 group。常见于少量大文件的场景(比如视频、数据库等)。
总结:
经过前面学习,将来我们就可以先拿到文件的 inode 号,那么如何找到该文件的内容呢?只要通过 inode 编号,就能找到文件对应的 inode 属性,内部具有和数据块的对应关系,就能进一步找到文件的内容了!!
问题 1:Super Block 既然是描述一个分区的所有分组的整体情况,sb 为什么会在一个块组中???
超级块 (Super Block) 在每个块组的开头都有一份拷贝(第一个块组必须有,后面的块组可以没有)。为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的 super block 信息在这种情况下也能正常访问。为了实现上述目的,super block 不仅仅在一个组里,可能会同时存在在多个 block group 里!(不一定所有组都有,但是几乎多个组会同时存在同样的 super block).万一 super block 出了错误,多个组里面存多个 super block。即使搞坏了一个,可以用其他组的 super block 进行修复,是一种数据容灾的备份处理.
问题 2:新建一个分区,Super Block 和 GDT 一定是有效数据,为什么呢?
这是因为给特定分区写入管理信息,即写入文件系统和分区分组相关的的管理数据,对于文件数据可以暂时不用。(这不就相当于格式化吗!)
因此要使用一块硬盘:分区格式化 (给当前分区,写入文件系统)
问题 3:访问一个文件,在分区内,标识该文件的唯一性:inode 编号!!!新建一个文件:
在块组里查找 inode bitmap,找到一个为 0 的比特位,由 0 置 1,即申请 inode。就可以在 inode table 里申请一个 128 字节的空间。
通过 touch 一个新文件来看看如何工作。
创建一个新文件主要有以下 4 个操作:
存储属性
内核先找到一个空闲的 i 节点(这里是 263466)。内核把文件信息记录到其中。存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块数据复制到 300,下一块复制到 500,以此类推。记录分配情况
文件内容按顺序 300,500,800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。添加文件名到目录
新的文件名 abc。linux 如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和 inode 之间的对应关系将文件名和文件的内容及属性连接起来。删除一个文件:
拿着 inode 编号,在 Inode bitmap 找,确认是否有效,再通过 inode 编号读取 inode 结构,获取该文件的属性,判断文件的大小。如果不为 0,通过 inode 结构体内的数据块指针数组找到具体的数据块编号,计算在 Block Bitmap 的位置,由 1 置为 0,那么数据块就被释放了。
所以删除文件只改位图!!!→ 计算机删除数据,只要设置数据无效就行了 ->所以如果误删数据怎么办?啥都不做。修改文件:
修改文件无疑就是修改文件的内容或者属性。根据文件的 inode 编号找到 inode 结构体,接着检查文件的权限,如果不通过则返回权限错误。若通过,用 inode 结构体的数据块指针数组读取数据块,确认是否需要新的数据块,如果需要,则在 Block Bitmap 中查找为 0 的位置,申请 Data Block 数据块,更新 inode 数据块指针。再将数据写入到数据块中,更新 inode 元数据,最后同步元数据到磁盘中。查文件:
查文件和修改文件的逻辑是类似的,这里就不过多赘述。
问题 4:关于 inode 编号和 Data Block 编号 inode 是全分区统一分配的,不是只在分组内有效。inode 不能跨区域,一个分区,一个文件系统,互相独立!--> 即 inode 和 block 编号是在整个区有效
那么我们是如何通过 inode 确定 inode bitmap 在哪个位置的呢?
inode:1234 在 sb 中找到 block_per_group:1000,1234/1000 说明在块组 1 中;
找到 inode_per_group:1000,1234%1000 说明在 inode bitmap 中 234 位置中
问题 5: 我们打开、删除、查找等操作都是通过文件名啊,那 inode 在哪存着呢?
文件名不在 inode 结构体内部,应该在哪里呢?该如何看待目录呢?
目录也是文件,目录 = 目录内容 + 目录属性。
而目录自己的 inode 就在属性中存着。
那么目录的文件名又在哪呢?
实际上是在上一级目录的内容中。
那么文件内容存储着文件名有啥用呢?甚至我们打开、删除、查找等操作都是通过文件名啊,是如何找到 inode 编号的?
目录的文件内容存的是文件名和 inode 编号的映射关系啊,只要有了文件名就能确定其 inode 编号。
结论:文件系统中,从存储方式角度,存储普通文件和目录,有区别吗?没有
下面这段代码验证:目录是否确实是存的文件名与 inode 编号的映射关系
所以,访问文件,必须打开当前目录,根据文件名,获得对应的 inode 号,然后进行文件访问
所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!
由问题 5 我们就可以解决之前存在的未解决的问题:
为什么同一个目录下,文件名不能重复呢?
结论一:因为文件名和 inode 编号是具有映射关系的,重复的文件名确有多个 inode 编号,如何进行映射就是存在问题的,因此在同一目录下文件名不能重复。
结论二:在指定目录下,新建文件的本质:文件名->inode,写入到当前目录的 data block 里,这就是为什么在当前目录下新建文件,需要该目录具有'w'权限.
结论三:读取一个文件的属性的话,需要当前目录具有'r'权限,是因为如果不可读文件内容,就找不到文件名和 inode 编号的映射,,就不能确定 inode 编号,最终无法读取一个文件的属性。
总结来说结论二、三是对目录设置 rw 本质是约束用户,访问目录的 datablock
结论四:x 权限比较特殊:允许我们是否打开该目录!
问题 6:为什么 Linux 访问文件,都必须带路径啊?但是我 pwd 指令又是如何得到路径的?
这就是为什么我们访问文件,本质是进程访问,进程具有 cwd 的根本原因!
这就是为什么,以前 open 文件的时候,必须要有路径的原因! 这就是为什么 Linux 下访问任意文件,都需要路径访问!
这就是为什么 linux 下,文件路径可以定位文件的根本原因!Linux 下访问文件,都必须带路径 (无论是显示的,还是隐式的!)
我们知道了:访问文件必须要有目录 + 文件名=路径的原因 (根目录固定文件名,inode 号,无需查找,系统开机之后就必须知道)
但这样也会出现一个问题,每次访问一个文件,都需要做路径解析,这样不是很慢吗? 实际上在 linux 系统里有一个目录树:
Linux 系统中,当用户访问指定路径下的文件 (包括路上目录,最终的目标文件在内)
Linux 会在进行路径解析的过程中,在内核中形成目录树和路径缓存! --> 目录结构是内存级的
路径缓存:在做路径解析的时候,只有第一次是慢的,第 2..n 次的时候,路径解析会优先从 dentry 树结构中进行解析!
Linux 中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
注意:每个文件其实都要有对应的 dentry 结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构整个树形节点也同时会隶属于 LRU(Least Recently Used,最近最少使用) 结构中,进行节点淘汰整个树形节点也同时会隶属于 Hash,方便快速查找更重要的是,这个树形结构,整体构成了 Linux 的路径缓存结构,打开访问任何文件,都在先在这棵树下根据路径进行查找,找到就返回属性 inode 和内容,没找到就从磁盘加载路径,添加 dentry 结构,缓存新路径
一个磁盘,必须分区格式化,才能具有使用的前提;
一个分区,要真正的使用,必须挂在到指定的目录才可以.
那该如何把一个分区挂载到指定的目录呢?
挂载到 / 目录下
dd 命令 : 二进制写入,制作一个大的文件,就当做一个分区.
格式化写入文件系统
建立空目录
查看可以使用的分区
将分区挂载到指定的目录
卸载分区
我们已经能够根据 inode 号在指定分区找文件了,也已经能根据目录文件内容,找指定的 inode 了,在指定的分区内,我们可以为所欲为了。可是:
问题:inode 不是不能跨分区吗?Linux 不是可以有多个分区吗?我怎么知道我在哪一个分区???
分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。
所以,可以根据访问目标文件的"路径前缀"准确判断我在哪一个分区。
那么 OS 是如何将数据写到磁盘的呢?
实际上不只 CPU 具有寄存器,外设同样也可以有寄存器。OS 对外设的控制,是通过对设备内部的寄存器进行数据写入的!




本文从硬件到软件系统性地介绍了 Linux 下磁盘存储与文件系统的核心概念。首先讲解了机械磁盘的物理结构(磁头、磁道、柱面、扇区)和逻辑寻址方式(CHS 与 LBA 转换),指出操作系统通过 LBA 线性地址访问磁盘。其次详细阐述了文件系统的关键组件:超级块记录文件系统元信息,块位图管理数据块分配,inode 结构存储文件属性并通过多级索引映射数据块,目录则保存文件名与 inode 的映射关系。最后解释了文件访问路径解析机制(dentry 缓存)和分区挂载原理,揭示了 Linux 通过路径前缀确定分区的设计思想。全文由浅入深地构建了对磁盘存储体系的完整认知框架。


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online