再谈文件-ext2文件系统
文件系统
文章目录
- 文件系统
- 1. 理解硬件
- 1.1 磁盘
- 1.2 服务器
- 1.3 重识磁盘结构
- 1.3.1 磁盘的物理结构
- 1.3.2 磁盘的逻辑结构
- 1.3.2.1 真实过程
- 1.3.2.2 CHS && LBA
- 2. 文件系统
- 2.1 块(Block)
- 2.2 分区
- 2.3 inode
- 3 ext2 文件系统
- 3.1 宏观认识
- 3.2 Block Group
- 3.3 Block Group 结构
- 3.3.1 Super Block(超级块)
- 3.3.2 Group Descriptors(块组描述符)
- 3.3.3 Data Block Bitmap(数据块位图)
- 3.3.4 Inode Bitmap(inode 位图)
- 3.3.5 Inode Table(inode 表)
- 3.3.6 Data Blocks(数据块)
- 3.4 目录文件
- 3.5 inode 和 datablock 映射
- 3-6 目录与文件名
- 3-7 路径解析
- 3-8 路径缓存
- 3.9 挂载分区
- 3.9.1 挂载概念
- 3.9.2 挂载的核心组件
- a) 设备标识
- b) 挂载点
- c) 文件系统类型
- 3.9.3 挂载操作
- 基础挂载命令:
- 持久化挂载:/etc/fstab 文件
- 3.9.4 挂载故障处理
- 3.9.5 挂载过程
- 3.9.6 总结
- 3.10 总结
- 4. 软硬链接
- 4.1 硬链接
- 4.1 软链接
- 软链接 VS 硬链接
1. 理解硬件
1.1 磁盘
磁盘是计算机中用于长期存储数据的主要设备,主要分为机械硬盘(HDD
)和固态硬盘(SSD
)两类
1. HDD
(机械硬盘)
结构:由高速旋转的磁性盘片、磁头、马达和控制器组成
原理:磁头在旋转的盘片上移动,通过磁化颗粒记录数据,依赖物理机械运动
特点:容量大、成本低,但读写速度较慢,怕震动,适合大容量存储(如备份、存档)
2. SSD
(固态硬盘)
结构:由闪存芯片和主控芯片构成,无机械部件
原理:通过电子信号在闪存单元中存储数据,无物理运动
特点:读写速度快、抗震、能耗低,但价格较高,适合系统盘或高频读写场景
3. 用途
存储操作系统、软件及用户文件,HDD
常用于大容量需求(如数据中心),SSD
多用于提升性能(如个人电脑启动盘)
本篇内容讨论的是HDD
(机械硬盘)
1.2 服务器
服务器是为其它设备(通常叫做客户端)提供资源或服务的专用计算机,一般7$\times$24小时运行
服务器作为 IT
系统的核心,最初承担计算、存储、网络的综合职能。但随着数据量增长和业务复杂度提升,存储逐渐成为瓶颈 —— 本地磁盘的容量、速度和共享能力难以满足需求
此时,存储功能开始从通用服务器中剥离,形成专注于块级数据交付的专用设备,即磁盘服务器。它通过高速网络协议(如iSCSI
)、冗余磁盘阵列和优化固件,为数据库、虚拟化等场景提供‘存储即服务’的能力,成为现代数据中心的关键组件
1.3 重识磁盘结构
1.3.1 磁盘的物理结构
除去外壳之后的磁盘,内部结构如下图:
磁头 (Head):
- 定义与作用: 安装在传动臂上的电磁转换装置,负责在盘片表面读取和写入数据
- 数量: 每个盘片(
Platter
)通常有两个记录面(上表面和下表面),每个记录面对应一个独立的磁头。因此,磁头数 = 盘片数 ×2
磁盘的存储结构
放大磁盘,多个磁盘的结构如下:
-
磁道:
- 是什么: 在盘片表面划分出的一个个同心圆环
- 作用: 定位数据在盘片“半径方向”上的位置。读写磁头通过移动到不同的磁道半径位置来选择在哪个“圈”上进行操作
-
扇区:
- 是什么: 将单个磁道等分成的若干弧段
- 作用: 存储数据的最小物理单位,也是操作系统读写数据的最小单位(通常为
512
字节或4K
字节)。其定位了数据在磁道“圆周方向”上的具体位置
-
柱面 (
Cylinder
):- 是什么: 所有盘片上相同半径位置的磁道的集合。想象一个贯穿所有盘片的圆柱体的表面,该表面经过的所有磁道就构成了一个柱面
- 数量: 柱面数在数值上等于单个盘片上的磁道数
- 关键作用: 由于所有磁头固定在同一个传动臂上同步移动(共进退),它们在任何时刻都位于各自盘片的同一个磁道编号(即同一个柱面) 上。因此,柱面是数据定位中最重要的维度之一,切换柱面需要移动磁头(寻道),这是磁盘操作中最耗时的部分。在同一个柱面内切换磁头(选择不同盘面)和等待扇区旋转到磁头下(旋转延迟)则快得多
磁盘的读写时,通过传动臂带动磁头进行读写
进一步放大细节:
盘片 (Platter
):
- 是什么: 磁盘中实际存储数据的圆形盘片,通常由铝合金或玻璃基板覆盖磁性材料制成
- 数量: 一个硬盘驱动器包含一个或多个盘片叠放在主轴上,由主轴电机驱动同步高速旋转(如
5400 RPM
,7200 RPM
,10000 RPM
,15000 RPM
)
如何定位一个扇区呢?
- 数据存储在扇区中
- 要访问一个特定的扇区,硬盘需要:
- 首先将读写磁头移动到正确的磁道(径向定位)
- 然后等待盘片旋转,直到目标扇区移动到磁头下方(圆周定位)
- 因此,磁道地址(柱面号
Cylinder
) + 磁头号Head
(盘面) + 扇区号Sector
共同构成了硬盘上每个物理数据块的唯一地址(CHS
寻址或LBA
的逻辑基础)
CHS 寻址 (Cylinder
-Head-Sector Addressing
):
- 原理: 通过指定 柱面号 (
Cylinder
)、磁头号 (Head
)、扇区号 (Sector
) 这三个坐标来唯一确定硬盘上的一个物理扇区 - 定位过程:
- 寻道 (
Seek
): 移动传动臂,使所有磁头移动到目标柱面(最慢的操作) - 选择磁头 (
Head Select
): 激活目标记录面对应的磁头(电子切换,很快) - 等待旋转 (
Rotational Latency
): 等待盘片旋转,直到目标扇区移动到激活的磁头下方(速度取决于转速)
- 寻道 (
- 历史作用: 是早期硬盘(尤其是
IDE
/ATA
硬盘)与BIOS
及操作系统交互的主要物理寻址方式 - 容量限制的根源:
- 寄存器位数限制: 早期
BIOS
和IDE
/ATA
接口规范为CHS
参数分配的寄存器位数有限:- 柱面 (
Cylinder
):10
bits
(最大值1024
,0-1023
) - 磁头 (
Head
):8
bits
(最大值256
,0-255
) - 扇区 (
Sector
):6
bits
(最大值63
,1-63
,扇区号通常从1
开始)
- 柱面 (
- 扇区大小: 固定为
512
字节
- 寄存器位数限制: 早期
- 最大容量计算:
最大扇区数 = 柱面数 × 磁头数 × 扇区数/磁道 = 1024 × 256 × 63
最大容量 (字节) = 1024 × 256 × 63 × 512
- 按二进制单位 (1MB = 1048576 Bytes):
= (1024 × 256 × 63 × 512) / 1048576 MB ≈ (8455716864) / 1048576 MB ≈ 8064 MB ≈ 7.875 GB
- 按制造商十进制单位 (1MB = 1000000 Bytes):
= (1024 × 256 × 63 × 512) / 1000000 MB ≈ (8455716864) / 1000000 MB ≈ 8455.7 MB ≈ 8.4557 GB
(通常表述为 ~8.4 GB
)
- 按二进制单位 (1MB = 1048576 Bytes):
- 突破限制与淘汰:
- 瓶颈: 随着硬盘物理容量迅速增长,
CHS
寻址的 ~8.4GB
容量限制很快成为瓶颈 - 解决方案:
- 扩展
CHS
(ECHS
/LBA Assist
):BIOS
通过逻辑变换(如将柱面数除以某个因子,磁头数乘以该因子)欺骗操作系统,突破寄存器限制。但这依赖于BIOS
和驱动程序的特定实现,容易混乱 - 逻辑块寻址 (
LBA
-Logical Block Addressing
): 现代硬盘的标准寻址方式。操作系统和硬盘控制器将整个硬盘的扇区从0开始线性连续编号(LBA 0
,LBA 1
,LBA 2
, …)。硬盘内部的控制器负责将LBA地址透明地转换为其内部的物理地址(可能仍是类似CHS
的结构,或更复杂的映射)。LBA
使用28位(最大128GB
)、48位(最大128PB
)甚至更多位来表示扇区地址,彻底突破了CHS
的容量限制,且更简单、通用
- 扩展
- 现状: 现代操作系统(
Windows NT
系列及之后,Linux
等)和硬盘接口(SATA
,SAS
,NVMe
)普遍使用LBA寻址。CHS
仅在古老的系统或极低级的操作(如某些引导过程)中可能被提及,对于用户和现代操作系统已是透明且被取代的概念
- 瓶颈: 随着硬盘物理容量迅速增长,
1.3.2 磁盘的逻辑结构
磁带上面可以存储数据,我们可以把磁带拉直,形成线性结构
那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成卷在一起的磁带,那么磁盘的逻辑存储结构也可以类似于
这样每一个扇区,就有了一个线性地址(其实是数据下标),这种地址叫做LBA(Logical Block Address)
1.3.2.1 真实过程
传动臂上的磁头是共进退的
柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道逻辑上构成柱面
所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由“柱面”卷起来的
某一盘面的磁道展开:
也就是一维数组
柱面展开:
也就是二维数组
整个磁盘
👇
整个磁盘就是多张二维的扇区数组表(三维数组)
所以,寻址一个扇区:先找到柱面(Cylinder
),再确定柱面内哪个磁道(其实就是磁头(Header
)的位置),再确定扇区(Sector
),所以就有了CHS
因此,LBA
(Logical Block Address
)地址,其实就是线性地址,怎么计算得到这个 LBA
地址呢?🤔
操作系统使用 LBA
,LBA
转换为 CHS
,CHS
转换为 LBA
,由磁盘自己来做(硬件实现)
1.3.2.2 CHS && LBA
磁盘抽象化
- 通过
LBA
寻址,磁盘被抽象为一维扇区数组,数组下标即LBA
地址(从0
开始连续编号) - 操作系统只需指定
LBA
地址即可读写扇区,无需关心物理结构(柱面/磁头/磁道)
转换前提
- 需已知磁盘几何参数(开机时从磁盘控制器获取):
H_per_cylinder
:磁头总数(= 盘片数 ×2
)S_per_track
:每个磁道的扇区数C_total
:总柱面数(数值等于单个盘片的磁道数)
CHS → LBA 转换公式
关键说明
-
参数范围
- 柱面号
C
:0
到C_total-1
(从外圈向内编号) - 磁头号
H
:0
到H_per_cylinder-1
- 扇区号
S
:1
到S_per_track
(从1开始计数)
- 柱面号
-
偏移调整
S-1
:因LBA
从0
开始,而CHS
扇区号从1
开始,需减1
对齐eg
:CHS
(0,0,1)
→LBA
0``;CHS
(0,0,2)
→LBA
1
-
物理意义
- 柱面贡献:
C × (H_per_cylinder × S_per_track)
每个柱面包含H_per_cylinder
个磁道,每磁道S_per_track
个扇区 - 磁头贡献:
H × S_per_track
在当前柱面内,磁头号H
表示已跳过H
条完整磁道
- 柱面贡献:
LBA → CHS 转换公式
关键说明
- 计算步骤
- 步骤1:计算柱面号
C
确定LBA
地址跨越的完整柱面数(整除单个柱面的扇区总数
) - 步骤2:计算磁头号
H
用余数temp = LBA % (H_per_cylinder × S_per_track)
确定当前柱面内的位置,
再整除S_per_track
得到磁头偏移量 - 步骤3:计算扇区号
S
取余temp % S_per_track
得到当前磁道内的扇区偏移,加1(扇区号从1开始)
- 步骤1:计算柱面号
现代磁盘的实际情况
-
LBA
主导地位- 操作系统仅使用
LBA
访问磁盘,CHS
转换由磁盘控制器在硬件层完成 - 控制器内部维护映射表(考虑
ZBR
区域位记录等物理优化)
- 操作系统仅使用
-
寻址限制突破
LBA
采用 48位地址(最大支持128PB
),彻底解决CHS
的8.4GB
容量限制
2. 文件系统
2.1 块(Block)
硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实不会一个一个扇区的读取,这样效率很低,而是一次性连续读取多个扇区,即一次性读取一个块(Block
)
硬盘的每个分区是被划分为一个个的块,一个块的大小是由格式化的时候确定的,并且不可以更改,最常见的是 4KB
,即连续八个扇区组成一个块。块是文件存取的最小单位
注意:
- 磁盘看作为一个三维数组,数组下标就是
LBA
,每个元素都是扇区 - 每个扇区都有
LBA
,那么8
个扇区一个块,每一个块的地址都可以算出来 - 知道
LBA
:块号 =LBA
/8
- 知道块号:
LBA
= 块号 ×8
+n
(n
表示块内第几个扇区)
2.2 分区
磁盘可以被分为多个分区,在 Windows
下,分为 C
、D
、E
盘等,这就是分区。分区是对硬盘的一种格式化,那么在 Linux
下如何分区的呢?
柱面是分区的最小单位,我们可以利用参考柱面号的方式来进行分区,也就是设置每个区的起始柱面和结束柱面号。这时我们可以将硬盘上的柱面(分区)进行线性平铺,想象成一个大的平面,如下
注意
- 柱面大小一致,扇区大小一致,那么只需要知道每个分区的起始和结束柱面号,知道一个柱面多少个扇区,那么该扇区多大就很清楚了
2.3 inode
之前提到过,文件 = 数据 + 属性,在 ls -l
时,看到的不仅有文件名,还有文件的属性
$ ls -l
total 8
-rw-rw-r-- 1 wyf wyf 252 Jul 15 21:31 main.cpp
-rw-rw-r-- 1 wyf wyf 884 Jul 15 21:31 task.hpp
$
每行包括7列
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l
读取存储在磁盘上的文件信息,然后显示出来
还可以通过 stat
命令显示文件或文件系统的详细状态信息
$ stat main.cpp File: main.cppSize: 252 Blocks: 8 IO Block: 4096 regular file
Device: 253,1 Inode: 811287 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ wyf) Gid: ( 1000/ wyf)
Access: 2025-07-15 21:31:00.489344033 +0800
Modify: 2025-06-16 19:45:12.873442408 +0800
Change: 2025-06-16 19:45:12.873442408 +0800Birth: 2025-06-10 16:05:23.622826134 +0800
$
文件都存储在块中,那么很显然,需要一个存储文件的元信息(属性信息)的空间,比如说文件的创建者、文件的大小等,这种存储文件元信息的区域就叫做inode
(index node
),直译过来为索引节点
通过 ls -li
可以看到文件的 inode
号
$ ls -li
total 20
811287 -rw-rw-r-- 1 wyf wyf 252 Jun 16 19:45 main.cpp
811284 -rw-rw-r-- 1 wyf wyf 84 Jun 10 16:05 makefile
811288 -rw-rw-r-- 1 wyf wyf 5705 Jun 16 19:25 ProcessPool.hpp
801353 -rw-rw-r-- 1 wyf wyf 884 Jun 15 14:17 task.hpp
$
注意
- Linux下文件的存储是属性和内容分离存储的
- Linux下,保存文件属性的集合叫做 inode,一个文件,一个inode,inode内有一个唯一的标识符,叫做inode号
在内核中,inode
如下
struct ext2_inode {unsigned short i_mode; /* File mode */unsigned short i_uid; /* Owner Uid */unsigned long i_size; /* Size in bytes */unsigned long i_atime; /* Access time */unsigned long i_ctime; /* Creation time */unsigned long i_mtime; /* Modification time */unsigned long i_dtime; /* Deletion Time */unsigned short i_gid; /* Group Id */unsigned short i_links_count; /* Links count */unsigned long i_blocks; /* Blocks count */unsigned long i_flags; /* File flags */unsigned long i_reserved1;unsigned long i_block[EXT2_N_BLOCKS];/* Pointers to blocks */unsigned long i_version; /* File version (for NFS) */unsigned long i_file_acl; /* File ACL */unsigned long i_dir_acl; /* Directory ACL */unsigned long i_faddr; /* Fragment address */unsigned char i_frag; /* Fragment number */unsigned char i_fsize; /* Fragment size */unsigned short i_pad1;unsigned long i_reserved2[2];
};
注意
- 文件名属性并未纳入inode数据结构内部
- inode的大小一般是128字节或512字节
- 任何文件的内容大小可以不同,但是属性大小一定是相同的
到现在,我们已经知道硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,读取的基本单位是块。块又是硬盘的每个分区下的结构,那么如何找到"块"呢?谁来管理这些块呢?inode
又是如何放置的呢?
这些工作都是由文件系统来完成的
3 ext2 文件系统
3.1 宏观认识
在硬盘上存储文件,就需要先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的是组织和管理硬盘中的文件。在Linux下,最常见的 ext2
系列的文件系统,其最早期的版本是 ext2
,后来发展出 ext3
和 ext4
,ext3
和 ext4
虽然对 ext2
进行了增强,但其核心设计并没有发生变化
ext2
文件系统将整个分区划分为若干个同样大小的块组(Block Group
),如下图所示,只要能管理一个分区,就能管理所有的分区,也就能管理所有磁盘文件
图中的启动块(Boot Block
/Sector
)的大小是确定的,为 1KB
,由 PC
标准规定,用来存储磁盘分区信息和启动信息,任何文件都不能修改启动块。启动块之后才是 ext2
文件系统的开始
3.2 Block Group
在 ext2
文件系统中,整个磁盘分区在逻辑上被划分为一系列连续的、大小相等的单元,称为 Block Group(块组)。这是 ext2
文件系统组织磁盘空间、管理元数据(描述文件系统自身结构的数据)和用户数据(文件内容)的基本管理单元。这种设计是 ext2
性能、可靠性和可扩展性的关键
为什么需要 Block Group?
想象一下如果整个巨大的分区只有一个超级块和一个 inode
表会怎样?查找数据会非常慢(需要在整个分区寻址),元数据损坏的风险极高(一旦关键结构损坏,整个分区数据可能丢失)。Block Group
的设计解决了这些问题:
- 提高性能(局部性原理): 将相关的元数据(如
inode
表、位图)和它管理的数据块放在同一个物理区域(或附近)的块组内。这大大减少了磁头长距离寻道的时间(对机械硬盘尤其重要),提高了文件读写效率 - 提高可靠性(元数据冗余): 关键元数据(如超级块和块组描述符表)在多个块组中进行备份。如果一个块组的这些结构损坏了,可以使用其他块组中的备份进行恢复,降低了单点故障导致整个文件系统崩溃的风险
- 支持大分区: 通过将大分区划分为多个独立的块组来管理,避免了早期文件系统设计中对分区大小的严格限制
- 碎片控制(相对): 虽然
ext2
本身没有现代日志文件系统那么强的防碎片能力,但块组结构有助于将新文件的数据块分配限制在同一个或相邻的块组内,在一定程度上减少了文件碎片化
3.3 Block Group 结构
每个 Block Group
包含以下关键组成部分(按它们在磁盘上的典型顺序排列):
3.3.1 Super Block(超级块)
- 作用: 描述整个文件系统的全局信息,是文件系统的“总蓝图”。每个块组理论上都可以包含一个超级块的副本。
Super Block
的信息被破坏,可以说整个文件系统结构就被破坏了 - 内容: 文件系统大小、块大小、块总数、空闲块数、空闲
inode
数、第一个inode
号(通常是根目录/
的inode
)、挂载时间、最后一次写入时间、魔数(标识为ext2
)、文件系统状态(干净/脏)等关键全局参数 - 冗余: 为了可靠性,并非所有块组都存储完整的超级块。通常只在块组
0
、1
以及3
、5
、7
的幂次方(如3
,5
,7
,9
,25
,49
,81
, …)的块组中存储完整的超级块副本。其他块组中的超级块位置可能为空或用0
填充。dumpe2fs
命令可以查看哪些块组有超级块备份 - 原型
struct ext2_super_block {unsigned long s_inodes_count; /* Inodes count */unsigned long s_blocks_count; /* Blocks count */unsigned long s_r_blocks_count;/* Reserved blocks count */unsigned long s_free_blocks_count;/* Free blocks count */unsigned long s_free_inodes_count;/* Free inodes count */unsigned long s_first_data_block;/* First Data Block */unsigned long s_log_block_size;/* Block size */long s_log_frag_size; /* Fragment size */unsigned long s_blocks_per_group;/* # Blocks per group */unsigned long s_frags_per_group;/* # Fragments per group */unsigned long s_inodes_per_group;/* # Inodes per group */unsigned long s_mtime; /* Mount time */unsigned long s_wtime; /* Write time */unsigned short s_mnt_count; /* Mount count */short s_max_mnt_count; /* Maximal mount count */unsigned short s_magic; /* Magic signature */unsigned short s_state; /* File system state */unsigned short s_errors; /* Behaviour when detecting errors */unsigned short s_pad;unsigned long s_lastcheck; /* time of last check */unsigned long s_checkinterval; /* max. time between checks */unsigned long s_reserved[238]; /* Padding to the end of the block */};
3.3.2 Group Descriptors(块组描述符)
- 作用: 描述该特定块组的详细信息。所有块组的描述符集合在一起形成了
Group Descriptor Table
(块组描述符表 -GDT
) - 内容(每个块组描述符包含):
- 该块组的数据块位图所在的块号
- 该块组的
inode
位图所在的块号 - 该块组的
inode
表起始块号 - 该块组中空闲块数
- 该块组中空闲
inode
数 - 该块组中目录数(用于目录分配策略)
- 冗余: 块组描述符表(
GDT
)通常紧随在超级块(们)之后。为了可靠性,GDT
本身也会像超级块一样在多个块组中进行备份(通常和超级块备份在相同的块组中) - 原型
struct ext2_group_desc {unsigned long bg_block_bitmap; /* Blocks bitmap block */unsigned long bg_inode_bitmap; /* Inodes bitmap block */unsigned long bg_inode_table; /* Inodes table block */unsigned short bg_free_blocks_count; /* Free blocks count */unsigned short bg_free_inodes_count; /* Free inodes count */unsigned short bg_used_dirs_count; /* Directories count */unsigned short bg_pad;unsigned long bg_reserved[3]; };
3.3.3 Data Block Bitmap(数据块位图)
- 作用: 一个简单的位图(
bitmap
),用于跟踪该块组内所有数据块的使用状态(空闲/已分配)。每一位(bit
)对应块组中的一个数据块(0
表示空闲,1
表示已用) - 大小: 一个块(
Block
)。因此,一个块组能容纳的数据块总数受限于一个块大小能表示的位数。例如,4KB
块大小的位图可以管理4KB
*8
bits
/byte
=32
,768
个数据块
3.3.4 Inode Bitmap(inode 位图)
- 作用: 一个位图,用于跟踪该块组内所有
inode
的使用状态(空闲/已分配)。每一位对应inode
表中的一个inode
- 大小: 通常也是一个块
3.3.5 Inode Table(inode 表)
- 作用: 存储该块组内所有
inode
的数组。每个文件或目录在创建时,会从其所在块组的inode
表中分配一个唯一的inode
- 内容(每个 inode 包含): 文件类型(普通文件、目录、符号链接、设备文件等)、访问权限、所有者
UID
/GID
、大小、时间戳(访问、修改、inode
变更)、链接计数、文件数据块在磁盘上的位置(直接指针、间接指针、双重间接指针、三重间接指针) - 大小: 占用多个连续的块。块组内
inode
总数在文件系统创建时确定(mkfs.ext2 -i
可调整比例) - inode 编号以分区为单位,整体划分,不可跨分区
3.3.6 Data Blocks(数据块)
- 作用: 存储实际的文件内容或目录条目列表
- 分配: 当文件需要存储数据时,文件系统会优先从该文件
inode
所属的同一个块组中分配空闲数据块(体现局部性原理)。如果当前块组满了,则尝试相邻块组 - Block 编号以分区为单位,不可跨分区
3.4 目录文件
所有文件(包括目录和普通文件)的文件名都只存在于其父目录的数据块中
核心机制
-
所有文件(含目录)的“文件名”
→ 存储在父目录的数据块中(作为文件名 + inode号
的条目)
目录自身没有“自我命名权”,其名字由父目录管理 -
目录的本质
→ 是一种特殊文件,其数据块内容不是普通数据,而是一张[子项文件名 : inode号]
的映射表
→ 操作系统通过目录inode
中的文件类型标记识别它是目录 -
父目录的管理方式
→ 通过其数据块中的ext2_dir_entry
结构管理子项struct ext2_dir_entry {__u32 inode; // 子项的 inode 号__u16 rec_len; // 条目总长度__u8 name_len; // 文件名长度__u8 file_type; // 文件类型(普通文件/目录等)char name[EXT2_NAME_LEN]; // 可变长度文件名 };
特殊目录的自我指涉
❓:根目录 /
或当前目录 .
的父目录是谁?
- 根目录
/
:其inode
号固定为2
(ext2
约定),在格式化时写入超级块 .
和..
:- 每个目录的数据块中自动包含两个特殊条目:
. ——> 指向自己的 `inode`(当前目录) .. ——> 指向父目录的 `inode`
- 这就是
cd ..
能返回上级目录的实现基础
- 每个目录的数据块中自动包含两个特殊条目:
3.5 inode 和 datablock 映射
inode
内部存在unsigned long i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
,就是用来进行inode
和block
之间的映射的
💡小结
- 分区之后的格式化操作,就是对分区进行分组,在每个分组中写入
SB
、GDT
、Block
、Bitmap
、Inode
、Bitmap
等管理信息,这些管理信息给文件系统使用 - 只要知道文件的
inode
号,就能在指定分区中确定是哪一个分组,进而在哪一个分组中确定是哪一个inode
- 获取到
inode
,文件属性和内容就都全有了
接下来以创建一个新文件来看一下整个过程
$ touch test
$ ls -li test
791104 test
$
创建一个新文件主要有以下4个操作:
-
分配
inode
并存储属性- 扫描 inode 位图,找到空闲
inode
(791104
) - 初始化
inode
元数据(权限、时间戳等),暂不填充i_block
- 更新
inode
位图,标记该inode
为“已用”
- 扫描 inode 位图,找到空闲
-
分配数据块并存储内容
- 扫描块位图,找到空闲块(
300
、500
、800
) - 将文件数据复制到这些块
- 更新块位图,标记这些块为“已用”
- 扫描块位图,找到空闲块(
-
记录块映射到
inode
- 将块指针按逻辑顺序写入
inode
的i_block
:- 小文件(≤12块):直接填入
i_block[0..N-1]
(如i_block[0]=300
,i_block[1]=500
,i_block[2]=800
) - 大文件:使用间接索引块存储指针列表
- 小文件(≤12块):直接填入
- 将块指针按逻辑顺序写入
-
将文件名链接到目录
- 在父目录的数据块中添加新条目(
test
→inode 791104
)。 - 更新父目录的
inode
(如大小、修改时间)
- 在父目录的数据块中添加新条目(
3-6 目录与文件名
- 我们在访问文件时,使用的都是文件名,没有使用过
inode
号🤔 - 磁盘上会区分目录吗?
前面已经提到过目录就是一种特殊文件,存储的内容特殊而已
下面用一个示例代码来说明问题
#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("File name is: %s,inode is: %lu\n", entry->d_name, (unsigned long)entry->d_ino);}closedir(dir);return 0;
}
运行结果
$ ./readdir /
File name is: cdrom,inode is: 131073
File name is: proc,inode is: 393220
File name is: dev,inode is: 131074
File name is: lib,inode is: 13
File name is: run,inode is: 8193
File name is: CloudrResetPwdAgent,inode is: 262145
File name is: media,inode is: 131075
File name is: etc,inode is: 393218
File name is: bin,inode is: 12
File name is: home,inode is: 524290
File name is: mnt,inode is: 524291
File name is: root,inode is: 524292
File name is: snap,inode is: 16385
File name is: sbin.usr-is-merged,inode is: 8194
File name is: lib64,inode is: 14
File name is: usr,inode is: 32769
File name is: sbin,inode is: 15
File name is: boot,inode is: 524289
File name is: bin.usr-is-merged,inode is: 393217
File name is: lib.usr-is-merged,inode is: 393219
File name is: tmp,inode is: 24578
File name is: var,inode is: 32770
File name is: lost+found,inode is: 11
File name is: opt,inode is: 131076
File name is: srv,inode is: 16386
File name is: sys,inode is: 24577
$
总结
访问任何文件都必须通过其所在目录文件获取 inode
编号。对于相对路径访问,必须能打开当前工作目录文件并查询其内容
一切皆文件,包括目录本身。也就是说,访问文件,必须知道当前工作目录,本质是必须能打开工作目录文件,查看目录文件的内容
3-7 路径解析
打开当前工作目录文件,查看当前工作目录文件的内容,当前工作目录也是文件,那么我们访问当前工作目录也只是只知道当前工作路径的文件名,要访问当前工作目录,不是也需要知道它的 inode
号吗?🤔
所以说也要打开:当前工作目录的上级目录,但是如此递归下去,上级目录也是目录,问题还是没有得到解决❓
最终递归的出口是"根目录",整个过程需要将路径中的所有目录全部解析,出口就是"根目录
比如说下面的文件
/home/code/test.c
从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下的指定目录,直到访问到 test.c
。整个过程叫做 Linux
路径解析
注意
- 到此,我们清楚了为什么访问文件必须要有 目录+文件名 的原因
- 根目录是固定的,inode号无需查找,系统开机之后就必须知道
路径的提供者🚶♂️➡️
-
内核层:
- 提供根目录
inode
基准 - 维护进程
CWD
元数据 - 实现路径解析算法
- 提供根目录
-
系统层:
/etc/passwd
定义用户家目录- 初始化脚本创建系统目录结构
- 挂载点管理跨文件系统路径
-
用户层:
Shell
通过$PWD
维护当前路径- 应用程序使用相对路径访问资源
- 用户通过命令创建新路径
关键点 🔑
-
根目录的特殊性:
- 根目录的
inode
是硬编码的(通常是inode 2
) - 在文件系统挂载时,内核直接获取根目录
inode
- 这是递归解析的唯一出口,避免了无限递归问题
- 根目录的
-
当前工作目录(CWD)的实现:
- 每个进程维护一个
cwd
指针指向当前目录的inode
- 进程启动时继承父进程的
CWD
(通常是shell
的工作目录) - 通过
chdir()
系统调用更新CWD
指针
- 每个进程维护一个
-
路径解析本质:
- 从根目录(已知
inode
)开始的链式查询过程 - 通过目录文件内容实现"路径名→
inode
"转换 - 当前工作目录作为相对路径的解析起点
- 从根目录(已知
最开始的路径来自何方❓
-
Linux
下的根目录,缺省目录,家目录等,都是提前在磁盘文件系统中新建目录文件。而我们自己新建的目录文件,都在我们自己或者系统指定的目录下新建,路径就这样诞生了 -
系统 + 用户共同构建
Linux
路径结构 -
路径的诞生与演化
- 根目录:文件系统创建时硬编码生成
- 系统目录:操作系统初始化时预创建
- 用户路径:通过
mkdir
/touch
等命令创建 - 进程工作目录:由
shell
初始化并传递给子进程
总结:
文件系统的路径结构是自举(bootstrapping) 的完美体现:
- 从已知的根inode出发
- 通过目录文件内容扩展出整个路径空间
- 最终形成易于理解的树状结构
3-8 路径缓存
- 在
Linux
中,存在真正的目录吗?
不存在,只有文件。只保存 文件属性 + 文件内容 - 访问任何文件,都要从
/
目录开始路径解析吗?
原则上是这样,但是这样太慢,所以Linux
会缓存历史路径结构 Linux
目录的概念,从何而来?
打开的文件是目录的时候,OS
内部会自己在内存中维护
Linux
中,在内核中维护树路径结构的内核结构体叫做:struct dentry
在内核中如下
struct dentry {/* RCU lookup touched fields */unsigned int d_flags; /* protected by d_lock */seqcount_spinlock_t d_seq; /* per dentry seqlock */struct hlist_bl_node d_hash; /* lookup hash list */struct dentry *d_parent; /* parent directory */struct qstr d_name;struct inode *d_inode; /* Where the name belongs to - NULL is* negative */union shortname_store d_shortname;/* --- cacheline 1 boundary (64 bytes) was 32 bytes ago --- *//* Ref lookup also touches following */const struct dentry_operations *d_op;struct super_block *d_sb; /* The root of the dentry tree */unsigned long d_time; /* used by d_revalidate */void *d_fsdata; /* fs-specific data *//* --- cacheline 2 boundary (128 bytes) --- */struct lockref d_lockref; /* per-dentry lock and refcount* keep separate from RCU lookup area if* possible!*/union {struct list_head d_lru; /* LRU list */wait_queue_head_t *d_wait; /* in-lookup ones only */};struct hlist_node d_sib; /* child of parent list */struct hlist_head d_children; /* our children *//** d_alias and d_rcu can share memory*/union {struct hlist_node d_alias; /* inode alias list */struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */struct rcu_head d_rcu;} d_u;
};
注意
- 每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成树状结构
- 整个树形节点也同时隶属于 LRU(Least Rencently Used),最少使用结构中,进行节点淘汰
- 整个树形节点也会同时隶属于Hash,方便快速查找
- 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加到dentry结构,缓存新路径
总结:
dentry
这样的逻辑结构在物理存储(inode
)和逻辑路径之间建立抽象层- 实现"一切皆文件"的统一视图
- 分离路径管理和文件存储
3.9 挂载分区
现在我们已经能够根据 inode
号在指定分区找文件了,也已经能根据目录文件内容,找指定的 inode
了,在指定的分区内,我们几乎可以说是为所欲为了。但是新的问题产生了:
inode
不是不能跨分区吗?Linux
肯定有多个分区,我怎么知道我在哪一个分区呢?
3.9.1 挂载概念
在 Linux
中,挂载(Mounting) 是将存储设备(分区、磁盘、网络存储等)的文件系统连接到目录树指定位置的过程。这个连接点称为挂载点(Mount Point)
- 文件系统隔离:每个挂载的文件系统有独立的
inode
空间、权限和特性 - 命名空间整合:通过挂载点将不同文件系统整合到统一路径视图
- 访问透明性:用户无需关心文件实际存储位置
3.9.2 挂载的核心组件
a) 设备标识
就是 Linux
识别存储设备的逻辑标签,用于是建立物理存储与文件系统之间的映射关系
标识类型 | 示例 | 说明 |
---|---|---|
设备文件 | /dev/sda1 | SATA 第一分区 |
UUID | UUID=3e6b... | 全局唯一标识符 |
文件系统标签 | LABEL=DataDisk | 用户友好名称 |
网络路径 | server:/share | NFS 共享 |
b) 挂载点
- 必须是已存在的空目录(最佳实践)
- 标准挂载点位置:
/mnt
:临时挂载/media
:可移动设备/opt
:附加软件- 自定义目录
c) 文件系统类型
类型 | 说明 | 典型场景 |
---|---|---|
ext4 | Linux 标准日志文件系统 | 系统根分区 |
xfs | 高性能文件系统 | 企业级存储 |
btrfs | 写时复制文件系统 | 高级存储方案 |
ntfs/vfat | Windows 兼容文件系统 | 跨平台共享 |
nfs | 网络文件系统 | 分布式存储 |
tmpfs | 内存文件系统 | 临时高速存储 |
3.9.3 挂载操作
基础挂载命令:
mount -t <文件系统类型> <设备标识> <挂载点>
eg
:
# 挂载 ext4 分区
sudo mount -t ext4 /dev/sdb1 /mnt/data# 挂载 NTFS 分区(需 ntfs-3g)
sudo mount -t ntfs-3g /dev/sdc1 /media/windows
常用选项:
选项 | 作用 |
---|---|
ro /rw | 只读/读写(默认 ·) |
noexec | 禁止执行二进制文件 |
nosuid | 忽略 SUID /SGID 权限位 |
remount | 重新挂载已挂载的文件系统 |
defaults | 默认选项(rw , suid , dev , exec ) |
示例:
# 以只读方式挂载
sudo mount -o ro /dev/sdd1 /backup# 重新挂载为读写模式
sudo mount -o remount,rw /backup
持久化挂载:/etc/fstab 文件
系统启动时自动挂载的配置文件:
<设备> <挂载点> <文件系统> <选项> <dump备份> <fsck检查顺序>
示例配置:
# 设备 挂载点 类型 选项 备份 检查
UUID=a1b2c3d4 / ext4 defaults,noatime 0 1
/dev/sdb1 /data xfs defaults 0 2
server:/nfs /mnt/share nfs rw,hard,intr,timeo=300 0 0
关键字段:
- dump 备份:
0
=不备份,1
=每日备份 - fsck 顺序:
0
=不检查,1
=根优先检查,2+
=其他
3.9.4 挂载故障处理
常见错误及解决:
错误信息 | 原因 | 解决方案 |
---|---|---|
mount: /dev/sdx: already mounted | 重复挂载 | umount 后重试 |
mount: unknown filesystem type | 缺少驱动 | 安装对应包(如 ntfs-3g ) |
mount: wrong fs type | 文件系统损坏 | fsck 检查修复 |
mount: special device does not exist | 设备不存在 | 检查设备路径 |
3.9.5 挂载过程
- 通过设备文件找到文件系统超级块(
super_block
) - 创建
vfsmount
结构关联挂载点和超级块 - 将
vfsmount
插入全局挂载链表 - 在
dentry
树中标记挂载点
3.9.6 总结
- 分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用
- 因此,就可以根据访问目标文件的"路径前缀"准确判断在哪一个分区
3.10 总结
一下面三张图做一个总结
-
进程视角
-
文件视角
-
内核视角
4. 软硬链接
4.1 硬链接
真正找到磁盘上文件的并不是文件名,而是 inode
。其实在 Linux
中,可以让多个文件名对应于同一个 inode
,比如说下面创建硬链接
$ touch test
$ ln test link
$ ls -li test link
655488 -rw-rw-r-- 2 wyf wyf 0 Jul 16 15:12 link
655488 -rw-rw-r-- 2 wyf wyf 0 Jul 16 15:12 test
$
test
和link
的链接状态相同,他们被称为指向文件的硬链接。内核记录了这个链接数,inode
655488
的硬链接数为2
- 在删除文件时,发生两件事:
- 在目录中将对应的记录删除
- 将硬链接数
-1
,如果为0
,则将对应的磁盘释放
4.1 软链接
硬链接是通过 inode
引用另外一个文件,软链接则是通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的 inode
是不同的,应用场景上可以把其看作一个快捷方式,在 shell
中创建软连接
$ touch test
$ ln -s test slink
$ ls -li test slink
655494 lrwxrwxrwx 1 wyf wyf 4 Jul 16 15:22 slink -> test
655493 -rw-rw-r-- 1 wyf wyf 0 Jul 16 15:22 test
$
软链接 VS 硬链接
特性 | 硬链接 (Hard Link) | 软链接 (Symbolic Link) |
---|---|---|
本质 | 目录项(dentry )指向相同 inode | 独立文件存储目标路径 |
存储位置 | 同一文件系统内 | 可跨文件系统 |
inode | 共享源文件 inode | 拥有独立 inode |
删除影响 | 源文件删除仍可访问 | 源文件删除链接失效 |
文件类型 | 普通文件 | 特殊链接文件 (l类型) |
大小 | 与源文件相同 | 等于路径字符串长度 |
更新机制 | 自动同步内容 | 路径重定向 |
限制 | 不能链接目录/跨设备 | 无限制 |
- 硬链接:相同
inode
号(12345
) - 软链接:
l
文件类型 + 箭头指示目标
当需要保持数据同一性时用硬链接,当需要保持路径灵活性时用软链接
wyf@hcss-ecs-0be3:~/code$ pwd
/home/wyf/code
wyf@hcss-ecs-0be3:~/code$ ls -alin
total 16
655383 drwxrwxr-x 3 1000 1000 4096 Jul 16 15:22 .
655361 drwxr-x--- 14 1000 1000 4096 Jul 16 11:47 ..
655492 drwxrwxr-x 2 1000 1000 4096 Jul 16 15:22 tmp
wyf@hcss-ecs-0be3:~/code$ cd tmp
wyf@hcss-ecs-0be3:~/code/tmp$ ls -alin
total 8
655492 drwxrwxr-x 2 1000 1000 4096 Jul 16 15:22 .
655383 drwxrwxr-x 3 1000 1000 4096 Jul 16 15:22 ..
wyf@hcss-ecs-0be3:~/code/tmp$
可以看到其实子目录下的 ..
就是父目录的 .
,任何目录下的 .
就是目录自己本身。所以不难得出,两个其实都是硬链接