☰
【Linux笔记】Linux磁盘文件系统
2026/10/1 2:43:06 网站建设 项目流程

一、磁盘

1.1 磁盘的认识

磁盘是计算机中用于长期存储数据的外部存储设备,相比内存(RAM),磁盘具有断电不丢失数据的特性,但读写速度远低于内存。

主要分类: - 机械硬盘(HDD, Hard Disk Drive):基于磁性存储,依靠机械臂在高速旋转的盘片上读写数据。容量大、价格低、寿命长,但抗震性差、速度相对较慢。 - 固态硬盘(SSD, Solid State Drive):基于闪存芯片(NAND Flash),无机械结构,读写速度快、抗震、功耗低,但价格相对高、写入寿命有限。 - 混合硬盘(SSHD):结合 HDD 大容量与 SSD 缓存的方案。

1.2 磁盘的物理结构

磁盘结构示意图:

部件作用
盘片(Platter)存储数据的圆形金属片(通常涂有磁性介质),一个硬盘有 1~N 张
主轴(Spindle)带动盘片高速旋转(常见 5400 / 7200 / 10000 RPM)
磁头(Head)读取/写入盘片数据,每个盘面需要一个磁头
磁头臂(Actuator Arm)承载磁头在盘片径向方向移动
音圈马达(VCM)驱动磁头臂摆动
控制电路处理 I/O 指令、缓存数据

1.3 磁盘的存储结构

盘片结构示意图

核心结构:盘片的讲解

盘片结构(自顶向下看): - 磁道(Track):盘片上的同心圆 - 扇区(Sector):每个磁道被划分为若干弧段,是磁盘的最小读写单位(通常 512 字节,传统标准;现代多为 4K 字节) - 柱面(Cylinder):多个盘片上相同半径的磁道组成柱面

1.4 磁盘的逻辑结构

操作系统为磁盘提供了一套"逻辑视图",使得用户和软件无需关心物理细节即可寻址。早期使用 CHS 寻址,现代使用 LBA 寻址。

1.4.1 CHS 地址

CHS 是最早期的磁盘寻址方式,直接对应磁盘的物理结构,用三个维度来定位一个扇区:

C (Cylinder) ── 柱面号:定位到哪个半径位置 H (Head) ── 磁头号:定位到哪个盘面 S (Sector) ── 扇区号:定位到磁道上的哪一段 备注:其中的磁头号可以对应磁盘上的盘面号

CHS地址定位过程分析:

定位过程: 1. 通过 Head 选择盘面(哪一张盘片的哪一面) 2. 通过 Cylinder 控制磁头臂移动到对应的同心圆 3. 等待盘片旋转,使目标扇区转到磁头下方 4. 读/写该扇区

CHS 的编号起始值:

C(柱面) :起始值为0 H(磁头) :起始值为0 S(扇区) : 起始值为 1 (历史遗留,从1开始,因为0号保留作"无效"标记)

磁盘 数据填充顺序:

第一优先:扇区号 S 递增(盘片旋转,最快) 第二优先:磁头号 H 递增(电子切换磁头,很快) 第三优先:柱面号 C 递增(机械移动磁头臂,最慢)
假设一个硬盘有: 2 个柱面(C=0, C=1) 2 个磁头(H=0, H=1) 每磁道 3 个扇区(S=1, 2, 3) 填充顺序如下所示: 柱面0, 磁头0(盘面0): [S=1] [S=2] [S=3] ← 先填满一条磁道的扇区 柱面0, 磁头1(盘面1): [S=1] [S=2] [S=3] ← 再切换磁头(同柱面内) 柱面1, 磁头0(盘面0): [S=1] [S=2] [S=3] ← 最后切换柱面(移动磁头臂) 柱面1, 磁头1(盘面1): [S=1] [S=2] [S=3]

1.4.2 LBA 地址

LBA 是 (逻辑块,Logical Block Addressing )的缩写,是现代磁盘统一采用的线性寻址方式。

基本思想:将磁盘上的柱面进行展开,其中所有扇区从 0 开始顺序编号,形成一个连续的线性地址空间。

假设一个硬盘有: ​ A. 2 个柱面(C=0, C=1) ​ B. 4 个磁头(H=0,H=1,H=2,H=3) ​ C. 每磁道 4 个扇区(S=1, 2, 3,4) ​ ​ 柱面0, 磁头0 (盘面0): CHS(0,0,1) → LBA 0 CHS(0,0,2) → LBA 1 CHS(0,0,3) → LBA 2 CHS(0,0,4) → LBA 3 柱面0, 磁头1 (盘面1): CHS(0,1,1) → LBA 4 CHS(0,1,2) → LBA 5 CHS(0,1,3) → LBA 6 CHS(0,1,4) → LBA 7 柱面0, 磁头2 (盘面2): CHS(0,2,1) → LBA 8 CHS(0,2,2) → LBA 9 CHS(0,2,3) → LBA 10 CHS(0,2,4) → LBA 11 柱面0, 磁头3 (盘面3): CHS(0,3,1) → LBA 12 CHS(0,3,2) → LBA 13 CHS(0,3,3) → LBA 14 CHS(0,3,4) → LBA 15 ​ ​ 柱面1, 磁头0 (盘面0): CHS(1,0,1) → LBA 16 CHS(1,0,2) → LBA 17 CHS(1,0,3) → LBA 18 CHS(1,0,4) → LBA 19 柱面1, 磁头1 (盘面1): CHS(1,1,1) → LBA 20 CHS(1,1,2) → LBA 21 CHS(1,1,3) → LBA 22 CHS(1,1,4) → LBA 23 柱面1, 磁头2 (盘面2): CHS(1,2,1) → LBA 24 CHS(1,2,2) → LBA 25 CHS(1,2,3) → LBA 26 CHS(1,2,4) → LBA 27 柱面1, 磁头3 (盘面3): CHS(1,3,1) → LBA 28 CHS(1,3,2) → LBA 29 CHS(1,3,3) → LBA 30 CHS(1,3,4) → LBA 31

1.4.3 LBA 与 CHS 的转换

参数符号说明:

C : 柱面号 H : 磁头号 S : 扇区号
A. LBA地址 → CHS地址

已知 LBA 地址 转换为 CHS 地址

1. 柱面号C = LBA / (磁头总数 * 每磁道的扇区数) 2. 磁头号H = LBA % (磁头总数 * 每磁道的扇区数) / 每磁道的扇区数 3. 扇区号S = LBA % 每磁道的扇区数 + 1
B. CHS地址 → LBA地址

已知 CHS地址 转换为 LBA地址

LBA 地址 = C * (磁头总数 * 每磁道的扇区数) + H * 每磁道的扇区数 + S - 1

二、磁盘文件系统

2.1 磁盘文件系统的认识

操作系统的一个核心职责就是管理外存(主要是磁盘),而文件系统正是操作系统中负责管理磁盘数据的子系统。

用户通过文件系统来创建、读写、删除文件,而不需要关心数据在磁盘上的物理存放位置。

若要深入理解文件系统的运作机制,需要逐步引入三个关键概念:块、分区 、inode。

2.1.1 "块''的引入

A. 为什么需要块?

磁盘是一种机械(或固态)设备,其读写操作具有以下特点:

  • 寻道时间(磁头移动到目标磁道)和旋转延迟(盘片旋转到目标扇区)占据了访问磁盘的大部分时间。

  • 磁盘硬件的最小读写单位是扇区(sector),通常为512 字节(新型磁盘为 4KB)。

事实上操作系统不会以扇区为单位来读写磁盘,而是将若干个连续的扇区组合成一个更大的逻辑单位——块(block),作为磁盘 I/O 的基本单位。

B. 块的定义

块(Block):操作系统对磁盘进行读写操作的基本逻辑单位,通常由若干个连续的扇区组成。

常见的块大小为4KB(即 8 个 512 字节的扇区,或 1 个 4KB 的扇区),其中块大小可以在格式化文件系统时指定。

C. 块带来的好处
- 减少 I/O 次数:一次读写一个块(4KB),比逐字节操作高效得多。 - 简化管理:操作系统只需追踪哪些块被使用、哪些空闲,而不必追踪每个扇区。 - 对齐硬件:块的大小通常为扇区大小的整数倍,便于与硬件协作。

2.1.2 ''分区''的引入

A. 为什么需要分区?

一块物理磁盘可以被整个当作一个大的存储空间来使用,但在实际使用中,我们通常将磁盘划分为多个独立的逻辑区域,原因如下:

  • 多操作系统共存:一台计算机可能需要同时安装 Windows 和 Linux,不同操作系统的文件系统格式不同,需要各自的独立空间。

  • 数据隔离与安全:将系统文件和用户数据放在不同分区,重装系统时不会丢失用户数据。

  • 管理便利:不同分区可以设置不同的权限、配额和备份策略。

  • 性能优化:将频繁访问的数据放在磁盘外圈(传输速率更高)的分区中。

B. 分区的定义

分区(Partition):将一块物理磁盘在逻辑上划分为若干个独立的区域,每个分区在操作系统看来就像一块独立的磁盘,可以各自格式化为不同的文件系统。

C. 分区的典型结构
/dev/sda ← 整块物理磁盘 ├── /dev/sda1 ← 分区1:/boot(引导分区,约 512MB) ├── /dev/sda2 ← 分区2:/(根分区,系统文件) ├── /dev/sda3 ← 分区3:/home(用户数据) └── /dev/sda4 ← 分区4:swap(交换空间)

2.1.3 ''inode''的引入

A. 为什么需要inode?
在文件系统中,文件 = 内容 + 属性 所以对于一个文件而言有两部分信息需要管理: - 文件的元数据(属性):文件大小、权限、所有者、创建时间、修改时间、数据所在块的位置等。 - 文件的实际数据(内容):文件的内容本身,存放在数据块中。
  • 如果将元数据(属性)和数据(内容)混在一起存储,管理会非常混乱。

  • Linux 文件系统采用了一种精妙的设计:将文件的元数据与文件数据分开存储。

  • 元数据(属性)存放在一个专门的数据结构中,这就是inode(index node,索引节点)。

B.inode的定义

inode(索引节点):文件系统中存储文件(或目录)元数据的固定大小的数据结构,每个文件或目录对应一个唯一的inode。

C.inode中存储的信息

inode中重要的存储信息如下所示:

信息说明
文件类型普通文件、目录、符号链接、设备文件等
文件权限读/写/执行权限(rwx)
所有者信息UID(用户ID)、GID(组ID)
文件大小以字节为单位
时间戳atime(访问时间)、mtime(修改时间)、ctime(状态变更时间)
硬链接计数指向该inode的目录项数量
数据块指针指向存放文件实际数据的磁盘块编号

注意:

  • inode中 不存储文件名。

  • 文件名存储在目录的数据块中,通过目录项(dentry)将文件名映射到inode编号。

磁盘上的索引节点结构体如下:

/* * 磁盘上索引节点(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; /* 组ID的低16位 */ __le16 i_links_count; /* 链接计数 */ __le32 i_blocks; /* 块计数 */ __le32 i_flags; /* 文件标志 */ .../*其他字段*/ };

三、ext2 磁盘文件系统

3.1 宏观认识 "ext2" 文件系统

操作系统将磁盘划分为多个分区,不同对应的分区文件系统不同,磁盘级别的文件系统主要引用的是ext系列文件系统。

以ext2文件系统为例,理解磁盘级的文件系统。

ext2文件系统将整个磁盘分区划分为若干个大小相同的块组(Block Group)。

ext2文件系统将每个块组划分为:

+-------------+---------+---------------+--------------+-------------+-------------+ | Super Block | GDT | Block Bitmap | Inode Bitmap | Inode Table | Data Blocks | +-------------+---------+---------------+--------------+-------------+-------------+ ↓ ↓ ↓ ↓ ↓ ↓ 超级块 组描述符 块位图 索引节点位图 索引节点表 数据块

备注:数据块(Data Blocks)和 索引节点 (Inode) 是可以跨块组(Block Group)。

3.2 块组内部构成

3.2.1Data Blocks--数据块

数据块: 存放文件的实际内容,也就是文本、二进制数据。

不同文件类型的数据块存储内容不同:

- 普通文件:数据块中存储文件的实际内容(文本、二进制数据等)。 - 目录文件:数据块中存储,目录项(directory entry)。 每个目录项包含: - 文件名(字符串) - 对应的 inode 编号 - 这样就把"文件名"和"inode"关联了起来。

3.2.2Inode Table--索引节点表

Inode Table:存放当前块组中所有inode的具体内容,其中每个inode记录了一个文件的 全部元数据(属性信息)。

Inode Table核心特征:

  1. 预先分配:格式化时就确定了大小,之后不能动态扩展

  2. 连续存储:在磁盘上占据一段连续的扇区

  3. 定长记录:每个inode占用固定字节数(ext2 默认 128 字节)

  4. 编号寻址:通过inode号(数组下标)直接定位,O(1) 时间复杂度

Inode Table表如下所示: +---------------------------------------------------+ | Inode Table | +---------------------------------------------------+ | 索引 | 内容 | +---------------------------------------------------+ | struct ext2_inode 0 | (通常是保留的,不使用) | +---------------------------------------------------+ | struct ext2_inode 0 | (通常是保留的,不使用) | +---------------------------------------------------+ | struct ext2_inode 1 | (通常也是保留的) | +---------------------------------------------------+ | struct ext2_inode 2 | (根目录 inode) | +---------------------------------------------------+ | struct ext2_inode 3 | (lost+found 目录) | +---------------------------------------------------+ | struct ext2_inode 4 | (用户文件/目录) | +---------------------------------------------------+ | struct ext2_inode 5 | (用户文件/目录) | +---------------------------------------------------+ | ... | | +---------------------------------------------------+ | struct ext2_inode N | (最后一个 inode) | +---------------------------------------------------+

3.2.3Inode Bitmap--索引节点位图

Inode Bitmap:它是一个比特数组(bit array),每个bit位严格对应索引节点表中的一个inode,用来记录Inode Tbale中已用/空闲状态总览表。

假设某个块组有 8192 个 inode: Inode Bitmap 大小 = 8192 bits = 1024 bytes = 1 KB 位编号: 0 1 2 3 4 5 6 7 ... 8191 比特值: 1 1 1 0 1 0 0 1 ... 0 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 含义: 已用 已用 已用 空闲 已用 空闲 空闲 已用 空闲 对应关系: bit[0] = 1 → inode 1 已被占用 bit[3] = 0 → inode 3 空闲可用 bit[5] = 0 → inode 5 空闲可用

3.2.4Block Bitmap--数据块位图

Block Bitmap: 是一个纯粹的比特数组(bit array),但它的每一位代表一个数据块(Data Block) 的使用状态。

假设块大小(Block Size)为 4KB,某个块组有 32768 个数据块: Block Bitmap 大小 = 32768 bits = 4096 bytes = 4 KB(正好占满一个数据块) 位编号: 0 1 2 3 4 5 6 7 ... 32767 比特值: 1 1 0 0 1 1 1 0 ... 1 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 含义: 已用 已用 空闲 空闲 已用 已用 已用 空闲 已用 对应关系: bit[0] = 1 → 第 0 号数据块已被占用(可能存着某个文件的内容) bit[2] = 0 → 第 2 号数据块空闲,可以分配给新写入的数据

3.2.5GDT--组描述符

GDT:整体描述一整个块组(Block group)的信息。

struct ext2_group_desc { __u32 bg_block_bitmap; // 本块组"块位图"所在块号 __u32 bg_inode_bitmap; // 本块组"inode 位图"所在块号 __u32 bg_inode_table; // 本块组"inode 表"起始块号 __u16 bg_free_blocks_count; // 本块组空闲块数 __u16 bg_free_inodes_count; // 本块组空闲 inode 数 __u16 bg_used_dirs_count; // 本块组目录数 __u16 bg_pad; // 填充对齐 __u32 bg_reserved[3]; // 保留 // ... 后续版本扩展字段 };

3.2.6Super Block--超级块

Super Block:整体描述一整个分区(Partion)中的信息。

struct ext2_super_block { __u32 s_inodes_count; // 总 inode 数 __u32 s_blocks_count; // 总块数 __u32 s_r_blocks_count; // 保留块数(保留给 root) __u32 s_free_blocks_count; // 空闲块数 __u32 s_free_inodes_count; // 空闲 inode 数 __u32 s_first_data_block; // 第一个数据块号(通常 1) __u32 s_log_block_size; // 块大小 = 1024 << s_log_block_size __u32 s_log_frag_size; // 碎片大小 __u32 s_blocks_per_group; // 每块组块数 __u32 s_frags_per_group; // 每块组碎片数 __u32 s_inodes_per_group; // 每块组 inode 数 __u32 s_mtime; // 最后挂载时间 __u32 s_wtime; // 最后写入时间 __u16 s_mnt_count; // 挂载次数 __u16 s_max_mnt_count; // 强制检查前的最大挂载次数 __u16 s_magic; // 魔数 0xEF53(识别 ext2 的标识) __u16 s_state; // 文件系统状态 __u16 s_errors; // 错误处理方式 __u16 s_minor_rev_level; // 次版本号 __u32 s_lastcheck; // 最后检查时间 __u32 s_checkinterval; // 检查间隔 __u32 s_creator_os; // 创建者操作系统 __u32 s_rev_level; // 主版本号 __u16 s_def_resuid; // 保留块默认 UID __u16 s_def_resgid; // 保留块默认 GID // ... 后续版本扩展字段 };

注意:每个块组(Block Group)都冗余存储超级块(Super Block),让超级块所在的块组损坏时,可以用其他块组的副本恢复,这是ext系列文件系统的抗损机制。

3.3Inode管理机制

3.3.1 判断空闲的Inode

文件系统在内存中维护着每个块组(Block Group)的 块组描述符表(GDT),其中明确记录了该组当前空闲的Inode数量。

判断时,内核无需读取磁盘位图,直接遍历内存中的 GDT 表,检查各组的空闲计数是否大于零即可。

3.3.2 分配Inode的完整流程

步骤 1:选择目标块组

分配策略注重数据局部性: - 普通文件: 优先分配在父目录所在的块组(Block Group),使文件与目录在物理位置上接近,减少后续操作的寻道时间。 - 目录文件: 会寻找空闲 "Inode" 和 "空闲数据块" 均较充裕的块组,为其预留扩展空间。

步骤 2:位图占位

选中指定的块组(Block Group)后: 1. 将该组的 `Inode` 位图从磁盘读入内存 2. 扫描位图,找到第一个空闲位(值为 0) 3. 将该位置标记为 1(表示已占用) 4. 更新内存 GDT 中的空闲 `Inode` 计数(减 1) 5. 将修改标记为“脏页”,等待后台线程异步写回磁盘

步骤 3:初始化Inode结构

计算出该位对应的全局Inode编号后,在Inode Table中找到对应槽位,填入文件的基本属性(权限、时间戳、所有者等)。

3.3.3 释放Inode的流程

删除文件时(释放Inode): 1. 根据 `Inode` 编号计算其所属块组(`Block Group`)及位图(`Inode Bitmap`)中的偏移位置 2. 读取该组的位图( `Inode Bitmap`)到内存 3. 将对应bit位进行清零(标记为空闲) 4. 更新 GDT 中的空闲 `Inode` 计数(加 1) 5. 标记为脏页,异步写回到磁盘中。

3.4Data Blocks管理机制

3.4.1 空闲数据块的判断方式

文件系统在内存中维护着每个块组(Block Group)的 GDT(组描述符),其中明确记录了该组当前空闲数据块的数量。

判断时,内核无需读取磁盘上的块位图,直接遍历内存中的 GDT 表,检查各组的空闲数据块计数是否大于零即可。

3.4.2 分配数据块的完整流程

当文件需要写入数据时(例如向file.txt追加内容),内核会调用ext2_new_blocks()函数分配新的数据块。

步骤 1:选择目标块组(分配策略)

Ext2 采用局部性优先的分配策略: - 优先选择与 `Inode` 所在块组相同的块组,使文件的元数据(`Inode`)和数据(`Data Blocks`)在物理位置上接近,减少后续读写操作的寻道时间。 - 将逻辑上相邻的文件数据分配到磁盘上物理相邻的块中。 - 将碎片分配给尽量少的文件,从全局上减少磁盘碎片。

步骤 2:在块位图中“占坑”

选中目标块组(Block Group)后: 1. 将该组的块位图(Block Bitmap)从磁盘读入内存 2. 在块位图(Block Bitmap)中扫描,寻找第一个bit位为 0 的空闲位 3. 将该bit位置为 1(标记为已占用) 4. 同时更新内存 GDT 中的空闲数据块计数(减 1) 5. 将修改标记为“脏页”,等待后台线程异步写回磁盘

步骤 3:关联到Inode

1. Date Blocks分配成功后,内核将分配到的块号填入 Inode 的 i_block[] 数组中(更新当前文件所占据数据块的块号位置) 2. 更新 Inode 中的 i_blocks 字段(更新当前文件所占据的数据块数)。

3..4.3 释放数据块的流程

当文件被删除或截断时,内核会调用ext2_free_blocks()函数释放不再使用的数据块。

1. 根据要释放的数据块号,计算其所属的块组(Block Group) 2. 读取该组的块位图(Block Bitmap)到内存 3. 将对应的"bit位"置为0(标记为空闲) 4. 更新 GDT 中的空闲数据块计数(加 1) 5. 同时更新 Inode 中的 `i_blocks` 字段(减少相应数量) 6. 标记为脏页,异步写回到磁盘中

特别注意:释放数据块时,数据块中原有的文件内容不会被清除。位图标记为空闲后,旧数据依然保留在磁盘上,直到下次被新文件覆盖。这也是数据恢复的基本原理。

3.5 目录与文件名

3.5.1 目录是一种文件

在 ext2 文件系统中,目录本质上是一种特殊的文件。

它与普通文件的区别仅在于: - 普通文件的数据块(data block)中存储的是用户数据; - 目录文件的数据块中存储的是目录项(directory entry),即一组 "文件名 -> inode 号" 的映射关系。

3.5.2 目录的数据块内容 ---dentry

假设有一个目录/home/user/,它的目录的数据块里存储的内容大致是这样的:

这张表叫作 目录项(Directory Entry,简称 ) +-------------------+------------------+ | 文件名 (name) | inode 编号 (inode) | +-------------------+------------------+ | "." | 1048577 | <-- 当前目录自身 | ".." | 1048576 | <-- 父目录 | "test.txt" | 20000 | | "photo.jpg" | 20001 | | "project" | 20002 | <-- 这是一个子目录 | "notes.md" | 20003 | +-------------------+------------------+

3.5.3inode与 文件名的分离设计

inode(索引节点)中不存储文件名。

inode存储的重要信息 1. 文件类型(普通文件、目录、符号链接等) 2. 文件权限(rwx) 3. 文件所有者(uid/gid) 4. 文件大小 5. 时间戳(atime/mtime/ctime) 6. 指向数据块的指针(12个直接指针 + 1个间接 + 1个双重间接 + 1个三重间接) 7. 链接计数(link count)

文件名只存在于它所在目录的数据块中的目录项里。

1. 同一个 inode 可以被多个目录项引用(这就是硬链接的本质)。 2.通过路径 /home/user/file.txt 访问文件时,系统需要逐级解析路径,在每一级目录的数据块中查找文件名+inode号映射, 最后再通过 inode 号读取 inode 信息。

3.5.4 为什么文件名与inode分离?

A. 硬链接

因为inode里不存文件名,所以 多个文件名可以指向同一个inode:

目录 /home/user/ 的数据块: +------------------+---------+ | 文件名 | inode | +------------------+---------+ | "report.txt" | 20000 | --+ | "report_backup" | 20000 | --+ --> 两个名字指向同一个 inode +------------------+---------+ | v Inode 20000 (文件大小、权限、数据块指针……)
B. 文件名长度不受inode限制
1. Inode 的大小是固定的(通常 128 / 256 字节),里面要塞大量元数据(属性数据)。 2. 如果把文件名也放进去,文件名长度就会受到严格限制,把文件名放在目录的数据块中,文件名长度的限制就取决于目录数据块的格式。
C. 一个文件可以有不同名字出现在不同目录中
目录 /etc/ 的数据块: 目录 /tmp/ 的数据块: +------------------+------+ +------------------+------+ | "config.conf" | 5000 | | "temp_conf" | 5000 | +------------------+------+ +------------------+------+ | | 同一个 inode v Inode 5000

3.6 目录与分区挂载

3.6.1 为什么需要挂载

Linux 和 Windows 在目录结构上有根本区别:

系统结构特点
Windows多根目录树每个分区有独立盘符(C:\ D:\ E:\)
Linux单根目录树全系统只有一棵树,根是/,所有分区必须融入这棵树

问题:Linux 只有一棵树,但系统可能有多个分区(系统盘、数据盘、U盘)。这些独立的分区如何接入这棵唯一的树?

答案:挂载(Mount)。

注意:Linux系统中的根/ 是内存级别的,在系统启动时就已经存在。

3.6.2 挂载的本质

挂载 = 把一个分区的文件系统 "接入" 到 Linux 统一目录树的某个目录节点上。

mount /dev/sda2 /data 命令解释: 1. 把 /dev/sda2 分区的分区根目录,接入到统一目录树的 /data 节点上。 2. 从此访问 /data 就等于访问 /dev/sda2 的根目录。

其中被选中的那个目录/data叫作挂载点(Mount Point)。

挂载前: Linux 统一目录树(/dev/sda1) /dev/sda2(独立,无法访问) / ┌──────────────┐ ├── bin/ │ 分区根目录 │ ├── etc/ │ ├── photos/ │ ├── home/ │ ├── videos/ │ ├── data/ <-- 普通空目录 │ └── docs/ │ └── var/ └──────────────┘ 挂载后(mount /dev/sda2 /data): / ├── bin/ (来自 /dev/sda1) ├── etc/ (来自 /dev/sda1) ├── data/ =========================> 现在指向 /dev/sda2 的分区根目录 │ ├── photos/ (来自 /dev/sda2) │ ├── videos/ (来自 /dev/sda2) │ └── docs/ (来自 /dev/sda2) └── var/ (来自 /dev/sda1)

3.6.3 磁盘分区只能挂载到根目录吗?

任何目录都可以作为挂载点。 /dev/sda1 挂载到 / <-- 根挂载(系统启动时,必须且唯一) /dev/sda2 挂载到 /home <-- 用户数据 /dev/sda3 挂载到 /var <-- 日志数据 /dev/sdb1 挂载到 /data <-- 数据盘 /dev/sdc1 挂载到 /media/usb <-- U盘 /dev/sdd1 挂载到 /data/photos <-- 甚至可以嵌套挂载

唯一的特殊之处:

  1. 根目录/是第一个被挂载的,也是必须被挂载的。

  2. 因为 Linux 的目录树必须有一个起点,系统启动时内核做的第一件事就是把根分区挂载到/,这棵树才算"长出来"。

3.6.4 分区与根目录的关系

"分区的根目录"和"Linux 系统的根目录",这两个"根目录"不是一回事。

概念位置数量本质
Linux 统一目录树的根/内存中(VFS 层)全系统只有一个内核启动时创建的空节点
分区的根目录磁盘上每个分区各有一个ext2 中是inode2 对应的目录
解释:分区的根目录 ext2 文件系统在设计时,人为规定了前几个 inode 编号的用途: inode 0:未使用(保留) inode 1:坏块 inode <==== 记录磁盘上的坏扇区列表 inode 2:根目录(.) <==== 这就是分区的根目录 inode 3:ACL inode <==== 访问控制列表,某些版本使用 inode 4:保留 inode 5:保留 ... inode 11:第一个可用的普通 inode(用户创建的文件从这里开始分配)

挂载就是"对接两个根"

挂载前: 内存中(内核创建的): 硬盘上(/dev/sda1): VFS 的 /节点(内存中,空) /dev/sda1 的分区根目录(硬盘上,inode 2) | │ NULL ├── bin/ ├── etc/ ├── lib/ └── ... 挂载后(mount /dev/sda1): VFS 的 / 节点 ──── 指针连接 ────> /dev/sda1 的分区根目录(inode 2)

"访问 / 就等于访问 /dev/sda1 的根目录"

内存中(内核创建的): 硬盘上(/dev/sda1): VFS 的 /节点(内存中,空) /dev/sda1 的分区根目录(硬盘上,inode 2) | │ NULL ├── bin/ ├── etc/ ├── lib/ └── ... 挂载前两者没有任何关系,一个存在于内存,另一个存在于磁盘,互不相通。 挂载前: / 节点.数据源 = NULL 挂载后: / 节点.数据源 = /dev/sda1 的分区根目录

3.6.5 系统启动时的根分区挂载

1. BIOS/UEFI 启动 -> 加载 GRUB 2. GRUB 从硬盘读取内核(vmlinuz)到内存 -> 交给内核 3. 内核在内存中初始化 - 创建空的目录树,根节点是 / - 此时 / 存在但是空的 4. 挂载根分区(关键步骤) - mount /dev/sda1 / - / 不再为空,可以看到 bin/、etc/、lib/ 等 5. 启动 init 进程 - 执行 /sbin/init(现在可以访问了) - 读取 /etc/fstab,知道还需要挂载哪些分区 6. 挂载其他分区 - mount /dev/sda2 /home - mount /dev/sdb1 /data - ... 7. 启动系统服务 -> 显示登录界面

3.6.6 挂载的覆盖效应

核心理解:挂载不会删除原目录的内容,只是将其遮盖,将其卸载后原内容恢复。 挂载前:ls /data -> old_file1.txt old_file2.txt 执行:mount /dev/sda2 /data 挂载后:ls /data -> photos/ videos/ documents/ (old_file1.txt 被遮盖了,但还在 /dev/sda1 上) 执行:umount /data 卸载后:ls /data -> old_file1.txt old_file2.txt (原内容恢复)

3.6.7 访问路径时与挂载分区的关系

假设挂载状态: /dev/sda1 挂载到 / /dev/sda2 挂载到 /data /dev/sdb1 挂载到 /data/backup 访问 /data/backup/photo.jpg 时: 第 1 步:从 / 开始(/dev/sda1) 第 2 步:查找 "data" -> 发现是挂载点 -> 切换到 /dev/sda2 第 3 步:在 /dev/sda2 中查找 "backup" -> 发现是挂载点 -> 切换到 /dev/sdb1 第 4 步:在 /dev/sdb1 中查找 "photo.jpg" -> 找到 一个路径,三次分区切换,对用户完全透明。

3.7 路径解析

当用户在Xshell中输入cat /home/user/test.txt时,完整的底层流程如下:

第 1 级:从根目录 "/" 开始 - 根目录的 inode 编号固定为 2(硬编码在内核中) - 读取 inode 2,找到根目录的数据块 - 在根目录的数据块中查找 "home",找到它的 inode 编号(假设是 1000) 第 2 级:进入 /home/ - 读取 inode 1000,找到 /home 目录的数据块 - 在其中查找 "user",找到它的 inode 编号(假设是 2000) 第 3 级:进入 /home/user/ - 读取 inode 2000,找到 /home/user/ 目录的数据块 - 在其中查找 "test.txt",找到它的 inode 编号(假设是 25000) 第 4 级:读取文件 - 读取 inode 25000,获取文件属性和数据块指针 - 根据指针读取数据块,获得文件内容 - 输出到终端

流程图示如下所示:

用户输入: cat /home/user/test.txt | | v inode 2 (根目录 /) | | 查找目录项 "home" v inode 1000 (/home/) | | 查找目录项 "user" v inode 2000 (/home/user/) | | 查找目录项 "test.txt" v inode 25000 (test.txt 的元数据) | | 读取数据块指针 v Data Block 70000, 70001, ... | | 读取实际内容 v 输出到终端

3.8 路径缓存 ---Dentry Cache

3.8.1 路径缓存是什么

路径缓存缓存的不是文件内容,而是目录项(dentry)。

路径缓存的核心思想:把"文件名 ->inode编号"的映射关系缓存在内存中,下次直接查内存,不再读磁盘。

目录项就是一条映射记录,存储在Data Blocks 数据块中。 目录项 = "在哪个父目录下" + "叫什么名字" + "对应哪个 inode" 例如:/home/user/test.txt 目录项 1:父目录 = /(inode 2),名字 = "home",对应 inode = 1000 目录项 2:父目录 = /home(inode 1000),名字 = "user",对应 inode = 2000 目录项 3:父目录 = /home/user(inode 2000),名字 = "test.txt",对应 inode = 25000

3.8.2 为什么需要路径缓存

假设没有路径缓存时,每次访问/home/user/test.txt都要逐级读磁盘,导致效率极其低下需要多次IO操作。

第 1 步:读取根目录(inode 2)的数据块 -> 查找 "home" -> 得到 inode 1000 第 2 步:读取 inode 1000 的数据块 -> 查找 "user" -> 得到 inode 2000 第 3 步:读取 inode 2000 的数据块 -> 查找 "test.txt" -> 得到 inode 25000 第 4 步:读取 inode 25000 的数据块 -> 得到文件内容 读磁盘 → 根目录数据块 → 找 "home" → 读磁盘 → 找 "user" → 读磁盘 → 找 "test.txt"
  1. 每一步都要读磁盘,对于一个三级路径而言,光"找到文件"就要读 3 次磁盘(还没算读文件内容本身)。

  2. 如果每次cat /home/user/test.txt都要重复这个过程,效率极低。

3.8.3 如何实现路径缓存

Linux内核通过描述 + 组织 的方式进行实现路径缓存,通过在内核上维护dentry结构体 +哈希表 + 树 进行实现

A. 描述dentry
struct dentry { struct inode *d_inode; // 指向对应的 inode(核心!) struct dentry *d_parent; // 指向父目录的 dentry struct qstr d_name; // 文件名(如 "home"、"test.txt") struct list_head d_child; // 在父目录的子项链表中 struct list_head d_subdirs; // 自己的子目录项链表 unsigned int d_count; // 引用计数(有多少进程正在使用) unsigned int d_flags; // 标志位 struct hlist_node d_hash; // 挂在哈希表上的节点 ... };
B. 组织方式:哈希表 + 树

结构 1:哈希表(用于快速查找)

哈希表(dentry_hashtable): 桶 0: -> dentry_A -> dentry_X -> ... 桶 1: -> dentry_B -> ... 桶 2: -> dentry_C -> dentry_Y -> dentry_Z -> ... 桶 3: -> ... ... 哈希键 = hash(父目录的 inode 编号 + 文件名) 例如: hash(inode_2 + "home") -> 桶 5 hash(inode_1000 + "user") -> 桶 12 hash(inode_2000 + "test.txt") -> 桶 3

查找流程如下所示:

要查找 "/home/user/test.txt" 中的 "test.txt": 已知:父目录是 /home/user(inode 2000),文件名是 "test.txt" 计算:hash(2000, "test.txt") = 3 去桶 3 中遍历链表,逐个比较: dentry_Z: d_name = "other.txt" -> 不匹配 dentry_Y: d_name = "test.txt" -> 匹配!返回这个 dentry -> 从 dentry->d_inode 得到 inode 25000 时间复杂度:O(1)(平均)

结构 2:树形结构(用于遍历和回收)

dentry 树: /(dentry,d_inode = inode 2) ├── bin(dentry,d_inode = inode 500) ├── etc(dentry,d_inode = inode 600) ├── home(dentry,d_inode = inode 1000) │ ├── user(dentry,d_inode = inode 2000) │ │ ├── test.txt(dentry,d_inode = inode 25000) │ │ └── .bashrc(dentry,d_inode = inode 25001) │ └── admin(dentry,d_inode = inode 3000) └── var(dentry,d_inode = inode 700)

3.8.4 工作流程

访问 /home/user/test.txt 第 1 步:解析 "home" - 计算 hash(inode_2, "home") - 查哈希表 - 命中? 是 -> 直接得到 dentry,d_inode = inode 1000(不读磁盘) 否 -> 读磁盘,在根目录数据块中查找 "home" -> 得到 inode 1000 -> 创建新的 dentry,插入哈希表和树中(缓存起来) 第 2 步:解析 "user" - 计算 hash(inode_1000, "user") - 查哈希表 - 命中? 是 -> 直接得到 dentry,d_inode = inode 2000(不读磁盘) 否 -> 读磁盘...(同上) 第 3 步:解析 "test.txt" - 计算 hash(inode_2000, "test.txt") - 查哈希表 - 命中? 是 -> 直接得到 dentry,d_inode = inode 25000(不读磁盘) 否 -> 读磁盘...(同上) 第 4 步:读取文件内容 - 通过 inode 25000 找到数据块 - 数据块可能也在 Page Cache 中(文件内容缓存)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询