【Linux】从磁盘到文件系统:深入理解Ext2文件系统

文章目录

    • 从磁盘到文件系统:深入理解Ext2文件系统
    • 一、理解磁盘硬件
      • [1.1 磁盘在计算机中的位置](#1.1 磁盘在计算机中的位置)
      • [1.2 磁盘物理结构](#1.2 磁盘物理结构)
      • [1.3 磁盘的存储结构](#1.3 磁盘的存储结构)
        • [1.3.1 磁道(Track)](#1.3.1 磁道(Track))
        • [1.3.2 扇区(Sector)](#1.3.2 扇区(Sector))
        • [1.3.3 柱面(Cylinder)](#1.3.3 柱面(Cylinder))
        • [1.3.4 磁盘容量计算](#1.3.4 磁盘容量计算)
    • 二、CHS与LBA寻址
      • [2.1 CHS寻址方式](#2.1 CHS寻址方式)
      • [2.2 磁盘的逻辑结构](#2.2 磁盘的逻辑结构)
        • [2.2.1 磁带的启示](#2.2.1 磁带的启示)
        • [2.2.2 把磁盘"拉直"](#2.2.2 把磁盘"拉直")
      • [2.3 LBA寻址](#2.3 LBA寻址)
      • [2.4 CHS与LBA转换](#2.4 CHS与LBA转换)
        • [2.4.1 CHS转LBA](#2.4.1 CHS转LBA)
        • [2.4.2 LBA转CHS](#2.4.2 LBA转CHS)
        • [2.4.3 谁负责转换?](#2.4.3 谁负责转换?)
    • 三、从硬件到文件系统
      • [3.1 引入"块"概念](#3.1 引入"块"概念)
      • [3.2 引入"分区"概念](#3.2 引入"分区"概念)
        • [3.2.1 为什么要分区?](#3.2.1 为什么要分区?)
        • [3.2.2 分区的本质](#3.2.2 分区的本质)
        • [3.2.3 Linux下的分区](#3.2.3 Linux下的分区)
      • [3.3 引入"inode"概念](#3.3 引入"inode"概念)
        • [3.3.1 文件 = 内容 + 属性](#3.3.1 文件 = 内容 + 属性)
        • [3.3.2 stat命令查看更多信息](#3.3.2 stat命令查看更多信息)
        • [3.3.3 inode的定义](#3.3.3 inode的定义)
        • [3.3.4 ext2_inode结构体](#3.3.4 ext2_inode结构体)
    • 四、Ext2文件系统详解
      • [4.1 宏观认识](#4.1 宏观认识)
        • [4.1.1 整体布局](#4.1.1 整体布局)
    • [4.2 Block Group 内部构成](#4.2 Block Group 内部构成)
    • [4.3 Super Block(超级块)](#4.3 Super Block(超级块))
      • [4.3.1 作用](#4.3.1 作用)
      • [4.3.2 ext2_super_block 结构体(简化)](#4.3.2 ext2_super_block 结构体(简化))
      • [4.3.3 为什么多个块组中会出现超级块?](#4.3.3 为什么多个块组中会出现超级块?)
      • [4.4 GDT(Group Descriptor Table,块组描述符表)](#4.4 GDT(Group Descriptor Table,块组描述符表))
        • [4.4.1 ext2_group_desc结构体](#4.4.1 ext2_group_desc结构体)
        • [4.4.2 作用](#4.4.2 作用)
      • [4.5 Block Bitmap(块位图)](#4.5 Block Bitmap(块位图))
      • [4.6 Inode Bitmap(inode位图)](#4.6 Inode Bitmap(inode位图))
      • [4.7 Inode Table(inode表)](#4.7 Inode Table(inode表))
      • [4.8 Data Blocks(数据块区)](#4.8 Data Blocks(数据块区))
      • [4.9 inode与datablock映射](#4.9 inode与datablock映射)
        • [4.9.1 直接块指针(0-11)](#4.9.1 直接块指针(0-11))
        • [4.9.2 一级间接块指针(12)](#4.9.2 一级间接块指针(12))
        • [4.9.3 二级间接块指针(13)](#4.9.3 二级间接块指针(13))
        • [4.9.4 三级间接块指针(14)](#4.9.4 三级间接块指针(14))
        • [4.9.5 最大文件大小](#4.9.5 最大文件大小)
    • 五、创建文件的过程
    • 六、总结

从磁盘到文件系统:深入理解Ext2文件系统

💬 欢迎讨论:这是Linux系统编程系列的第九篇文章。在前八篇中,我们学习了进程管理、文件描述符、重定向和缓冲区机制。我们知道文件存储在磁盘上,但磁盘是如何组织管理文件的?文件的属性和内容究竟存储在哪里?Linux如何通过文件名找到文件?本篇将深入磁盘物理结构,理解Ext2文件系统的设计原理。

👍 点赞、收藏与分享:这篇文章包含了磁盘硬件原理、寻址方式和完整的文件系统实现,内容硬核,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:建议先学习前八篇文章,特别是第七、八篇的文件I/O知识,这样理解本篇会更轻松。


一、理解磁盘硬件

1.1 磁盘在计算机中的位置

在学习文件系统之前,我们必须先理解磁盘这个硬件设备。

磁盘的特点:

  1. 唯一的机械设备

    • 计算机中唯一还在使用机械结构的部件
    • CPU、内存、主板都是电子器件
    • 磁盘有旋转的盘片、移动的磁头
  2. 外设

    • 不属于主机核心部件
    • 既是输入设备也是输出设备
    • 通过总线与CPU、内存通信
    • CPU访问寄存器:< 1ns
    • CPU访问内存:~100ns
    • CPU访问磁盘:~10ms(慢100,000倍!)
  3. 容量大、价格便宜

    • 内存:32GB ~ 128GB,价格昂贵
    • 磁盘:1TB ~ 10TB,价格便宜
    • 这就是为什么我们需要磁盘

题外话:机房与磁盘

让我们了解一下真实的计算环境:

bash 复制代码
机房(Data Center)
↓
机柜(Rack)
↓
服务器(Server)
↓
磁盘(Disk)

一个典型的互联网公司的机房:

  • 成百上千台服务器
  • 每台服务器多块磁盘
  • RAID阵列提供冗余
  • 数据中心级别的存储系统

关于磁铁

有趣的事实:磁盘利用磁性存储数据,所以强磁场会损坏磁盘数据!早期的数据销毁方法之一就是用强力磁铁消磁。

1.2 磁盘物理结构

让我们深入磁盘内部,看看它的物理结构:

核心部件:

  1. 盘片(Platter)

    • 圆形金属片
    • 表面涂有磁性材料
    • 高速旋转(5400/7200/10000 RPM)
  2. 磁头(Head)

    • 读写数据的关键部件
    • 每个盘面对应一个磁头
    • 所有磁头固定在传动臂上
  3. 主轴(Spindle)

    • 驱动盘片旋转
    • 保持恒定转速
  4. 传动臂(Actuator Arm)

    • 控制磁头移动
    • 关键:所有磁头共进退!

1.3 磁盘的存储结构

磁盘表面是如何组织数据的?

1.3.1 磁道(Track)

磁道特点:

  • 同心圆结构
  • 从外向内编号:0, 1, 2, ...
  • 最内圈用于停靠磁头,不存储数据
1.3.2 扇区(Sector)

每个磁道被切分成多个扇形区域:

扇区特点:

  • 磁盘存储数据的基本单位
  • 传统大小:512字节
  • 现代磁盘:4096字节(4KB扇区)
  • 每个磁道的扇区数相同
1.3.3 柱面(Cylinder)

重要概念:所有盘面上相同位置的磁道构成柱面!

为什么需要柱面概念?

因为所有磁头共进退!当磁头移动到某个位置时,所有盘面上相同位置的磁道都可以访问,这就是柱面。

1.3.4 磁盘容量计算

假设一个磁盘参数:

  • 盘片数:2
  • 磁头数:4(每个盘片2个面)
  • 柱面数(磁道数):1000
  • 每磁道扇区数:63
  • 每扇区字节数:512

磁盘容量 = 磁头数 × 柱面数 × 每道扇区数 × 每扇区字节数

bash 复制代码
容量 = 4 × 1000 × 63 × 512
= 129,024,000 字节
≈ 123 MB

二、CHS与LBA寻址

2.1 CHS寻址方式

知道了磁盘的物理结构,如何定位一个扇区呢?

CHS:Cylinder-Head-Sector

bash 复制代码
定位一个扇区的三个步骤:

1. 确定柱面号(Cylinder)  ─→ 磁头移动到指定位置
2. 确定磁头号(Head)        ─→ 选择哪个盘面
3. 确定扇区号(Sector)      ─→ 等待盘片旋转到指定扇区

示例:读取 CHS(5, 2, 10)

bash 复制代码
1. 传动臂移动,使所有磁头到达柱面5
2. 激活磁头2(盘片1的下表面)
3. 等待盘片旋转,直到扇区10经过磁头
4. 读取数据

CHS寻址的限制:

早期BIOS使用CHS寻址,受到位数限制:

  • 柱面:10 bit → 最多1024个柱面
  • 磁头:8 bit → 最多256个磁头
  • 扇区:6 bit → 最多63个扇区

最大容量 = 1024 × 256 × 63 × 512 = 8064 MB ≈ 8 GB

这就是为什么早期硬盘不能超过8GB!

2.2 磁盘的逻辑结构

CHS寻址虽然符合物理结构,但使用不便。能否把磁盘看成一个线性数组

2.2.1 磁带的启示
bash 复制代码
磁带:线性存储介质
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│ 9│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

磁带可以"拉直",形成一维数组。

2.2.2 把磁盘"拉直"

虽然磁盘是旋转的,但我们可以逻辑上把它看成线性结构:

bash 复制代码
物理结构(CHS):
盘片0上表面 → 柱面0 → 扇区0,1,2,...,62
柱面1 → 扇区0,1,2,...,62
...
盘片0下表面 → 柱面0 → 扇区0,1,2,...,62
...

逻辑结构(线性):
┌────┬────┬────┬────┬────┬────┬────┐
│ 0  │ 1  │ 2  │ 3  │ 4  │ 5  │ ...│
└────┴────┴────┴────┴────┴────┴────┘
扇区编号(LBA地址)

2.3 LBA寻址

LBA:Logical Block Address(逻辑块地址)

核心思想:给每个扇区一个唯一的线性地址(就像数组下标)

优势:

  1. 简单:用一个整数就能定位扇区
  2. 不受CHS位数限制
  3. 支持更大容量硬盘
  4. 现代操作系统都使用LBA

2.4 CHS与LBA转换

虽然操作系统使用LBA,但磁盘硬件仍然需要CHS来物理定位。转换由磁盘固件完成。

2.4.1 CHS转LBA

公式:

bash 复制代码
LBA = C × (磁头数 × 每道扇区数) + H × 每道扇区数 + (S - 1)

说明:

  • C:柱面号(从0开始)
  • H:磁头号(从0开始)
  • S:扇区号(从1开始)

示例:

假设磁盘参数:

  • 磁头数:4
  • 每道扇区数:63

计算 CHS(5, 2, 10) 对应的LBA:

bash 复制代码
LBA = 5 × (4 × 63) + 2 × 63 + (10 - 1)
= 5 × 252 + 126 + 9
= 1260 + 126 + 9
= 1395
2.4.2 LBA转CHS

公式:

bash 复制代码
每柱面扇区数 = 磁头数 × 每道扇区数

C = LBA ÷ 每柱面扇区数
H = (LBA % 每柱面扇区数) ÷ 每道扇区数
S = (LBA % 每道扇区数) + 1

示例:

将 LBA = 1395 转换为CHS:

bash 复制代码
每柱面扇区数 = 4 × 63 = 252

C = 1395 ÷ 252 = 5(余135)
H = 135 ÷ 63 = 2(余9)
S = 9 + 1 = 10

结果:CHS(5, 2, 10) ✓
2.4.3 谁负责转换?
bash 复制代码
操作系统
↓
使用LBA
↓
发送给磁盘控制器
↓
磁盘固件(Firmware)
↓
转换为CHS
↓
伺服系统(Servo System)
↓
物理定位扇区

结论:从现在开始,我们可以认为磁盘就是一个"扇区数组",下标就是LBA地址!


三、从硬件到文件系统

3.1 引入"块"概念

虽然扇区是磁盘的物理单位,但操作系统并不直接以扇区为单位操作。

为什么?

  1. 扇区太小:512字节,读写效率低
  2. 系统调用开销大:每次读512字节,开销不划算

解决方案:引入"块"(Block)

bash 复制代码
块 = 连续的多个扇区

常见配置:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│扇区0│扇区1│扇区2│扇区3│扇区4│扇区5│扇区6│扇区7│
└────┴────┴────┴────┴────┴────┴────┴────┘
_************块0************/ ___****块1****
4KB(8个扇区)

块的特点:

  • 大小固定:通常是4KB(8个扇区)
  • 格式化时确定:一旦格式化,不可更改
  • 文件存取的最小单位:读写文件至少读写一个块
  • 提高效率:减少系统调用次数

块号的计算:

bash 复制代码
已知LBA:
块号 = LBA ÷ 8

已知块号:
起始LBA = 块号 × 8

3.2 引入"分区"概念

一块磁盘可以划分成多个独立的区域,每个区域称为分区(Partition)

3.2.1 为什么要分区?
  1. 逻辑隔离:系统盘、数据盘分离
  2. 多操作系统共存:Windows、Linux各占一个分区
  3. 管理方便:不同分区不同用途
  4. 安全性:一个分区损坏不影响其他

分治思想

3.2.2 分区的本质

分区的本质:一段连续的 LBA 范围

在现代磁盘上,分区通常用 起始 LBA + 长度(扇区数) 来描述。历史上曾按CHS/柱面对齐,但现代系统更多采用按 1MiB 等边界对齐,以提高性能并兼容 4KB 物理扇区。

bash 复制代码
早期分区磁盘平铺视图示例:
┌──────────────────────────────────────────────┐
│  Boot  │  分区1      │  分区2      │  分区3   │
│ Sector │  (C盘)      │  (D盘)      │  (E盘)   │
│  1KB   │             │             │          │
└──────────────────────────────────────────────┘
柱面0      柱面1-100    柱面101-200  柱面201-300
3.2.3 Linux下的分区
bash 复制代码
lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0  100G  0 disk 
├─sda1   8:1    0   50G  0 part /
├─sda2   8:2    0   30G  0 part /home
└─sda3   8:3    0   20G  0 part [SWAP]
  • sda:第一块SATA硬盘
  • sda1:第一个分区
  • sda2:第二个分区
  • sda3:第三个分区

3.3 引入"inode"概念

现在我们知道:

  • 磁盘 = 扇区数组(LBA寻址)
  • 操作系统 = 块数组
  • 一个磁盘可以有多个分区

但还有一个核心问题:文件的属性存储在哪里?

3.3.1 文件 = 内容 + 属性

我们之前学过:

bash 复制代码
ls -l test.c
-rw-r--r-- 1 root root 654 Sep 13 14:56 test.c

这一行包含了文件的属性:

  • 权限:-rw-r--r--
  • 硬链接数:1
  • 所有者:root
  • 所属组:root
  • 大小:654字节
  • 修改时间:Sep 13 14:56
  • 文件名:test.c
3.3.2 stat命令查看更多信息
bash 复制代码
stat test.c
  File: test.c
  Size: 654             Blocks: 8          IO Block: 4096   regular file
Device: 802h/2050d      Inode: 263715      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

关键信息:

  • Inode: 263715:inode号
  • Blocks: 8:占用8个块
  • IO Block: 4096:块大小4KB
  • 三个时间:Access(访问)、Modify(修改)、Change(属性变化)
3.3.3 inode的定义

inode(index node,索引节点):存储文件元信息(属性)的数据结构

核心特点:

  1. 一个文件一个inode
  2. inode有唯一的编号
  3. 属性与内容分离存储
  4. inode大小固定:通常128字节或256字节
3.3.4 ext2_inode结构体
c 复制代码
struct ext2_inode {
    __le16 i_mode;        // 文件类型和权限
    __le16 i_uid;         // 所有者UID
    __le32 i_size;        // 文件大小
    __le32 i_atime;       // 最后访问时间
    __le32 i_ctime;       // 状态改变时间(metadata change time)
    __le32 i_mtime;       // 最后修改时间
    __le32 i_dtime;       // 删除时间
    __le16 i_gid;         // 所属组GID
    __le16 i_links_count; // 硬链接计数
    __le32 i_blocks;      // 占用的块数
    __le32 i_flags;       // 文件标志
    
    // ... 其他字段
    
    __le32 i_block[EXT2_N_BLOCKS]; // 数据块指针数组(重要!)
    
    // ... 其他字段
};

最重要的字段:i_block[15]

这个数组存储了文件内容所在的块号,后面会详细讲解。

注意:文件名不在inode中!


四、Ext2文件系统详解

现在所有准备工作都完成了,让我们正式认识 Ext2 文件系统!

4.1 宏观认识

文件系统的目的:组织和管理磁盘中的文件

Ext2(Second Extended Filesystem)是 Linux 早期的主流文件系统。虽然如今已经发展出了 Ext3、Ext4 等更新的文件系统,但它们在整体设计思想和核心数据结构上,都继承自 Ext2。因此,理解 Ext2 是理解整个 Linux 文件系统体系的基础。

注意:文件系统通常以分区作为载体,它既包含磁盘上的数据结构布局,也包含操作系统中用于解析和管理这些结构的代码实现。不同分区可以使用不同的文件系统


4.1.1 整体布局

在一个已经格式化为 Ext2 的分区中,磁盘空间的整体布局如下:

text 复制代码
┌────────────┬──────────────────────────────────────────┐
│ Boot Block │           File System Area (Ext2)        │
│  (reserved)│                                          │
│            │  Block Group 0 | Block Group 1 | ...     │
└────────────┴──────────────────────────────────────────┘

注意:这里讨论的是分区内部结构,而不是整个磁盘。


Boot Block(引导保留区)

  • 位置:分区起始位置的第一个块(通常对应分区起始扇区)

  • 大小:与文件系统块大小相关(Ext2 中通常为 1KB)

  • 用途

    • 为启动程序(Bootloader)或历史兼容性预留
    • 可能包含分区级启动代码,也可能完全未使用
  • 重要说明

    • Boot Block 不属于文件系统的正式数据结构
    • 是否写入启动代码,取决于系统启动方式(BIOS / UEFI)和实际需求
    • 这是文件系统的约定性保留区域,并非硬件强制不可修改

SuperBlock 开始,文件系统才正式接管该分区的空间。


Block Group(块组)

在 Ext2 中,整个文件系统区域被划分为多个 Block Group(块组)

  • 一个分区包含 多个块组
  • 每个块组的 大小相同
  • 每个块组的 内部结构基本一致

为什么要引入块组?

text 复制代码
整个分区太大 → 管理复杂、访问效率低
       ↓
划分为多个块组 → 分而治之
       ↓
每个块组独立管理自己的 inode 和数据块

这种设计可以:

  • 提高空间分配和查找效率
  • 减少磁盘寻道距离
  • 提高文件系统的可靠性

4.2 Block Group 内部构成

每个块组在逻辑上包含如下结构:

text 复制代码
┌───────────────────────────────────────────┐
│ Block Group                                │
│                                           │
│  [SuperBlock backup]  (可选,备份)       │
│  [GDT backup]         (可选,备份)       │
│  Block Bitmap                              │
│  Inode Bitmap                              │
│  Inode Table                               │
│  Data Blocks                               │
└───────────────────────────────────────────┘

注意:

SuperBlock 和 GDT 在部分块组中以备份形式存在,并非每个块组都包含。

下面我们逐一理解这些组成部分。


4.3 Super Block(超级块)

SuperBlock 是整个 Ext2 文件系统的核心元数据结构。

它描述的是整个文件系统的全局状态,而不是某一个具体文件或块组。


4.3.1 作用

超级块中记录了文件系统的关键信息,包括但不限于:

  • inode 和 block 的总数量
  • 空闲 inode 和 block 的数量
  • block 和 inode 的大小
  • 每个块组包含的 block / inode 数量
  • 最近一次挂载时间、写入时间
  • 文件系统状态和魔数
  • 文件系统检查相关信息

只要 SuperBlock 损坏,文件系统就无法被正确识别。


4.3.2 ext2_super_block 结构体(简化)

c 复制代码
struct ext2_super_block {
    __le32 s_inodes_count;        // inode 总数
    __le32 s_blocks_count;        // block 总数
    __le32 s_r_blocks_count;      // 保留 block 数
    __le32 s_free_blocks_count;   // 空闲 block 数
    __le32 s_free_inodes_count;   // 空闲 inode 数
    __le32 s_first_data_block;    // 第一个数据块号
    __le32 s_log_block_size;      // block 大小(log2)
    __le32 s_log_frag_size;       // 片段大小
    __le32 s_blocks_per_group;    // 每个块组的 block 数
    __le32 s_frags_per_group;     // 每组片段数
    __le32 s_inodes_per_group;    // 每组 inode 数
    // ...
    __le32 s_mtime;               // 最近挂载时间
    __le32 s_wtime;               // 最近写入时间
    __le16 s_mnt_count;           // 挂载计数
    __le16 s_max_mnt_count;       // 最大挂载计数
    __le16 s_magic;               // 魔数(0xEF53)
    __le16 s_state;               // 文件系统状态
    __le16 s_errors;              // 错误处理方式
    __le32 s_lastcheck;           // 上次检查时间
    __le32 s_checkinterval;       // 检查间隔
    __le32 s_creator_os;          // 创建者 OS
    __le32 s_rev_level;           // 修订级别
    __le16 s_def_resuid;          // 保留块默认 uid
    __le16 s_def_resgid;          // 保留块默认 gid
    __le32 s_first_ino;           // 第一个非保留 inode
    __le16 s_inode_size;          // inode 大小
    __le16 s_block_group_nr;      // 本超级块所在块组号
    // ...
};

4.3.3 为什么多个块组中会出现超级块?

答案:冗余备份(可靠性设计)。

text 复制代码
Block Group 0 : SuperBlock(主)
Block Group N : SuperBlock(备份)

原因如下:

  • 超级块损坏 → 文件系统整体不可用
  • 磁盘可能出现坏道
  • 在多个块组中保存备份,可以提高容错能力
  • Block Group 0 一定包含主超级块
  • 其他块组中是否保存备份,取决于文件系统的具体策略(如 sparse super)

4.4 GDT(Group Descriptor Table,块组描述符表)

GDT存储每个块组的描述信息

4.4.1 ext2_group_desc结构体

在 ext2 中,块组描述符表(GDT)是一个由多个 ext2_group_desc 结构体组成的数组,其中第 i 个描述符记录了分区内第

i 个块组的元数据布局信息,如块位图、inode 位图和 inode 表在磁盘上的块号。

c 复制代码
struct ext2_group_desc {
    __le32 bg_block_bitmap;       // 块位图所在块号
    __le32 bg_inode_bitmap;       // inode位图所在块号
    __le32 bg_inode_table;        // inode表起始块号
    __le16 bg_free_blocks_count;  // 空闲块数
    __le16 bg_free_inodes_count;  // 空闲inode数
    __le16 bg_used_dirs_count;    // 目录数
    __le16 bg_pad;                // 填充
    __le32 bg_reserved[3];        // 保留
};
4.4.2 作用
bash 复制代码
假设有3个块组:

GDT[0] → 描述Block Group 0
  ├─ 块位图在哪里
  ├─ inode位图在哪里
  ├─ inode表在哪里
  └─ 空闲块/inode数量

GDT[1] → 描述Block Group 1
  └─ ...

GDT[2] → 描述Block Group 2
  └─ ...

4.5 Block Bitmap(块位图)

位图记录哪些数据块已被使用

bash 复制代码
Block Bitmap(假设只有16个数据块):
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ 1│ 1│ 0│ 1│ 0│ 0│ 0│ 1│ 1│ 0│ 0│ 0│ 0│ 0│ 0│ 0│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
  ↑  ↑     ↑           ↑  ↑
  已 已     已          已 已
  用 用     用          用 用

1 = 已使用
0 = 空闲

作用:

  • 快速查找空闲块
  • 分配新块时使用
  • 释放块时清除对应位

大小:

  • 一个块组有N个数据块
  • 需要N个bit来记录
  • 如果块大小4KB = 4096字节 = 32768 bit
  • 一个块位图块可以管理32768个数据块
  • 即:4KB位图可以管理 32768 × 4KB = 128MB

4.6 Inode Bitmap(inode位图)

位图记录哪些inode已被使用

bash 复制代码
Inode Bitmap(假设只有16个inode):
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 0│ 0│ 1│ 0│ 0│ 0│ 0│ 0│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
  ↑保留        ↑     ↑           ↑
  (前8个通常是系统保留)

1 = 已使用
0 = 空闲

注意:

  • 前几个inode通常被系统保留
  • inode 2 通常是根目录 /

4.7 Inode Table(inode表)

存储所有inode结构体的区域

bash 复制代码
Inode Table:
┌─────────────┐
│  inode 1    │ ← 128字节
├─────────────┤
│  inode 2    │ ← 128字节(根目录)
├─────────────┤
│  inode 3    │ ← 128字节
├─────────────┤
│  inode 4    │
├─────────────┤
│    ...      │
├─────────────┤
│  inode N    │
└─────────────┘

计算:

  • 假设块组有8192个inode
  • 每个inode 128字节
  • inode表大小 = 8192 × 128 = 1MB = 256个块

4.8 Data Blocks(数据块区)

存储文件实际内容的区域

bash 复制代码
Data Blocks:
┌───────┬───────┬───────┬───────┬───────┐
│Block 0│Block 1│Block 2│Block 3│  ...  │
└───────┴───────┴───────┴───────┴───────┘
   4KB     4KB     4KB     4KB

不同文件类型的存储:

  1. 普通文件:文件内容
  2. 目录:目录项(文件名 + inode号映射)
  3. 符号链接:目标路径

4.9 inode与datablock映射

核心问题:inode如何找到文件内容所在的数据块?

答案:i_block[15]数组

c 复制代码
struct ext2_inode {
    // ... 其他字段
    __le32 i_block[EXT2_N_BLOCKS];  // EXT2_N_BLOCKS = 15
};
4.9.1 直接块指针(0-11)
bash 复制代码
i_block[0] → 直接指向数据块0
i_block[1] → 直接指向数据块1
...
i_block[11] → 直接指向数据块11

可以直接访问 12 个数据块 = 12 × 4KB = 48KB
4.9.2 一级间接块指针(12)
bash 复制代码
i_block[12] → 指向一个间接块
              ↓
         ┌───────────┐
         │ 块号0     │ → 数据块
         │ 块号1     │ → 数据块
         │ ...       │
         │ 块号1023  │ → 数据块
         └───────────┘
         一个4KB块可以存储1024个块号(4字节×1024=4KB)

可以访问 1024 个数据块 = 1024 × 4KB = 4MB
4.9.3 二级间接块指针(13)
bash 复制代码
i_block[13] → 一级间接块
              ↓
         ┌─────────┐
         │ 块号0   │ → 二级间接块 → 1024个数据块
         │ 块号1   │ → 二级间接块 → 1024个数据块
         │ ...     │
         │ 块号1023│ → 二级间接块 → 1024个数据块
         └─────────┘

可以访问 1024 × 1024 = 1048576 个数据块 = 4GB
4.9.4 三级间接块指针(14)
bash 复制代码
i_block[14] → 一级间接块 → 二级间接块 → 三级间接块 → 数据块

可以访问 1024 × 1024 × 1024 = 1073741824 个数据块 = 4TB
4.9.5 最大文件大小
bash 复制代码
直接块:      12 × 4KB = 48KB
一级间接:    1024 × 4KB = 4MB
二级间接:    1024 × 1024 × 4KB = 4GB
三级间接:    1024 × 1024 × 1024 × 4KB = 4TB

总计:约 4TB

注:以上为在 block=4KB、块号占 4 字节时的理论寻址规模估算,实际最大文件大小还可能受具体实现与字段限制影响。


五、创建文件的过程

现在让我们通过一个完整的例子,理解文件系统是如何工作的。

5.1 touch一个新文件

bash 复制代码
touch abc
ls -i abc
263466 abc

5.2 四个核心步骤

步骤1:分配inode
bash 复制代码
1. 查找Inode Bitmap,找到空闲的inode
   ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
   │ 1│ 1│ 1│ 1│ 0│ 1│ 1│ 1│ 0│ 0│ 0│ 0│
   └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
                ↑找到空闲的inode 5
   
2. 在Inode Bitmap中标记为已使用
   ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
   │ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 0│ 0│ 0│ 0│
   └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
                ↑
   
3. 在Inode Table中初始化inode结构体
   inode[5].i_mode = 0644;
   inode[5].i_uid = 0;
   inode[5].i_size = 0;
   inode[5].i_ctime = 当前时间;
   inode[5].i_mtime = 当前时间;
   // ...
步骤2:分配数据块(如果有内容)

假设文件需要写入一些内容:

bash 复制代码
1. 查找Block Bitmap,找到空闲块
   ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
   │ 1│ 1│ 0│ 1│ 0│ 0│ 1│ 1│ 0│ 0│
   └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
         ↑  ↑  ↑     ↑
    找到空闲块:2, 4, 5, 8

2. 假设分配块2、4、5

3. 在Block Bitmap中标记
   ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
   │ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 1│ 0│ 0│
   └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

4. 将数据写入这些块
步骤3:记录块分配情况
bash 复制代码
在inode中记录数据块位置:

inode[5].i_block[0] = 2;   // 第一个块
inode[5].i_block[1] = 4;   // 第二个块
inode[5].i_block[2] = 5;   // 第三个块
inode[5].i_blocks = 3;     // 总共3个块
inode[5].i_size = 实际字节数;
步骤4:添加目录项
bash 复制代码
在当前目录的数据块中添加记录:

目录的数据块内容:
┌────────┬───────────┐
│ inode  │ 文件名    │
├────────┼───────────┤
│ 2      │ .         │  (当前目录)
│ 1048577│ ..        │  (父目录)
│ 263464 │ test.c    │
│ 263465 │ main.c    │
│ 263466 │ abc       │  ← 新添加
└────────┴───────────┘

目录项结构(简化):
struct ext2_dir_entry {
    __le32 inode;      // inode号
    __le16 rec_len;    // 记录长度
    __u8   name_len;   // 文件名长度
    __u8   file_type;  // 文件类型
    char   name[];     // 文件名
};

5.3 完整流程图

bash 复制代码
创建文件 abc 的完整流程:

┌─────────────────┐
│  用户执行touch  │
└────────┬────────┘
         ↓
┌────────┴────────┐
│  系统调用creat  │
└────────┬────────┘
         ↓
┌────────┴────────────────┐
│  VFS层                  │
│  1. 解析路径            │
│  2. 检查权限            │
│  3. 调用ext2_create     │
└────────┬────────────────┘
         ↓
┌────────┴────────────────┐
│  Ext2文件系统           │
│                         │
│  ① 分配inode            │
│     查Inode Bitmap      │
│     找到空闲inode 263466│
│     标记为已用          │
│     初始化inode结构     │
│                         │
│  ② 分配数据块(可选)     │
│     查Block Bitmap      │
│     找到空闲块          │
│     标记为已用          │
│                         │
│  ③ 更新inode            │
│     记录块号到i_block[] │
│     更新i_size等字段    │
│                         │
│  ④ 更新目录             │
│     在父目录数据块中    │
│     添加(263466, "abc") │
│                         │
│  ⑤ 更新元信息           │
│     Super Block         │
│     GDT                 │
└────────┬────────────────┘
         ↓
┌────────┴────────┐
│  返回成功       │
└─────────────────┘

六、总结

通过本篇文章,我们深入理解了从磁盘硬件到Ext2文件系统的完整知识体系:

核心知识点:

  1. 磁盘物理结构

    • 盘片、磁头、磁道、扇区、柱面
    • 扇区:512字节,存储基本单位
  2. 寻址方式

    • CHS:物理寻址,容量受限
    • LBA:逻辑寻址,磁盘=扇区数组
    • 固件负责CHS↔LBA转换
  3. 从硬件到文件系统

    • 块:8个扇区,4KB,提高效率
    • 分区:柱面为单位,逻辑隔离
    • inode:存储文件属性,固定大小
  4. Ext2文件系统

    • Boot Block + Block Groups
    • 每个块组:SB + GDT + 位图 + inode表 + 数据区
    • Super Block:文件系统全局信息
    • GDT:块组描述信息
    • 位图:管理空闲块和inode
    • inode表:存储所有inode
    • 数据区:存储文件内容
  5. inode与数据块映射

    • i_block[15]数组
    • 直接块 + 间接块 + 二级间接 + 三级间接
    • 最大文件约4TB
  6. 创建文件过程

    • 分配inode
    • 分配数据块
    • 更新inode
    • 添加目录项

重要原理图:

bash 复制代码
完整的文件访问流程:

文件名 "abc"
    ↓
查找目录(需要目录的inode)
    ↓
找到目录项:(263466, "abc")
    ↓
获得inode号:263466
    ↓
定位inode:块组号 = 263466 / 每组inode数
           inode表偏移 = 263466 % 每组inode数
    ↓
读取inode结构体
    ↓
从i_block[]获取数据块号
    ↓
读取数据块
    ↓
得到文件内容

下一篇预告:

在下一篇文章中,我们将学习:

  • 目录的本质:目录也是文件
  • 路径解析:从根目录开始的递归过程
  • 路径缓存:dentry结构体
  • 挂载分区:如何访问多个分区
  • 软硬链接:两种不同的文件引用方式

💡 思考题

  1. 为什么需要位图?能否用其他数据结构管理空闲块?
  2. 如果一个块组的inode用完了,文件会创建失败吗?
  3. 为什么inode要固定大小?如果可变会有什么问题?
  4. 删除一个大文件时,系统主要做什么操作?

以上就是Ext2文件系统的核心原理!下一篇我们将学习目录、路径和软硬链接!

相关推荐
Run_Teenage2 小时前
Linux:进程等待
linux·运维·服务器
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]file
linux·笔记·学习
为什么要内卷,摆烂不香吗2 小时前
Grafana 12.2.0导入仪表盘
运维·grafana
乾元2 小时前
AI 驱动的入侵检测与异常会话判别:从规则到行为分析前言:从“捕获敌人”到“守卫秩序”
运维·网络·人工智能·网络协议·安全
___波子 Pro Max.2 小时前
Linux ps命令-ef参数详解
linux
春日见2 小时前
眼在手上外参标定保姆级教学(vscode + opencv)
linux·运维·服务器·数码相机·opencv·ubuntu·3d
xwill*2 小时前
python 字符串拼接
linux·windows·python
superman超哥2 小时前
仓颉锁竞争优化深度解析
c语言·开发语言·c++·python·仓颉
yong99903 小时前
C#实现OPC客户端与S7-1200 PLC的通信
开发语言·网络·算法·c#