Linux中Ext系列文件系统

一、硬件理解

1. 磁盘的物理结构:从盘片到扇区

现代机械硬盘(HDD)由多个盘片(platter) 堆叠组成,每个盘片的正反两面都可存储数据,对应一个磁头(head)。所有盘片共用一个主轴旋转,磁头通过传动臂在盘片表面径向移动,实现数据的读写。

  • 磁道(Track):盘片上以同心圆形式划分的环形区域,是数据存储的物理路径。
  • 柱面(Cylinder):所有盘片上相同半径的磁道组合成一个"圆柱体",称为柱面。柱面数量 = 磁道数。
  • 扇区(Sector) :磁道被进一步划分为更小的弧段,即扇区。扇区是磁盘存储数据的最小物理单位,传统大小为512字节(现代硬盘可能使用4KB高级格式,但逻辑上仍兼容512B)。
  • 磁头(Head):每个盘面配有一个磁头,负责读写对应盘面的数据。磁头随传动臂同步移动,因此同一时刻所有磁头位于同一柱面。

💡 关键细节:磁头是"共进退"的 ------ 当磁头移动到某个柱面时,所有盘面的对应磁道同时可被访问,这为后续的CHS寻址提供了基础。


2. 如何定位一个扇区?------ CHS 寻址法

在早期的硬盘系统中,操作系统通过**柱面-磁头-扇区(Cylinder-Head-Sector, CHS)**三元组来精确定位任意一个扇区:

  • C(柱面):决定磁头在径向上的位置(即哪个磁道环)。
  • H(磁头):决定访问哪个盘面(即哪个盘片的哪一面)。
  • S(扇区):决定在该磁道上从哪个起始点开始读写。

例如,要访问第3个柱面、第2个磁头、第5个扇区的数据,系统会驱动磁头移动到第3柱面,选择第2磁头对应的盘面,然后等待盘片旋转到第5扇区的位置进行读写。

⚠️ 注意:CHS寻址有容量限制!由于早期BIOS和IDE接口使用10bit表示柱面、8bit表示磁头、6bit表示扇区,最大支持约8.4GB硬盘(2^10 × 2^8 × 2^6 × 512B = 8.4GB)。现代硬盘已普遍采用LBA(逻辑块地址)寻址,但底层仍可能映射回CHS。


3. 磁盘容量计算公式

磁盘总容量可通过以下公式估算:

复制代码
总容量 = 柱面数 × 磁头数 × 每磁道扇区数 × 每扇区字节数

例如,若某硬盘参数为:

  • 柱面数 = 1024
  • 磁头数 = 63
  • 每磁道扇区数 = 63
  • 每扇区 = 512 字节

则总容量 ≈ 1024 × 63 × 63 × 512 ≈ 2.1 GB(实际因格式化开销略小)

✅ 实际使用中,可通过 fdisk -llsblk 命令查看磁盘分区表和容量信息,如图中所示的 /dev/vda 设备及其分区详情。


4. 磁盘的逻辑结构:从三维数组到一维线性空间

虽然磁盘在物理上是立体的------由多个盘片堆叠而成,每个盘片有多个磁道,每个磁道又划分为多个扇区------但在逻辑上,我们可以将其抽象为一个三维数组

  • 第一维:柱面(Cylinder) ------ 所有盘片上相同半径的磁道组成一个柱面,相当于"环"。
  • 第二维:磁头(Head) ------ 对应盘面编号,决定访问哪个盘面。
  • 第三维:扇区(Sector) ------ 每个磁道上的最小存储单元。

💡 举例:若硬盘有100个柱面、2个磁头(即2个盘面)、每磁道63个扇区,则总扇区数 = 100 × 2 × 63 = 12,600 个扇区。

这种结构就像把一整卷磁带"展开"成一张二维表格,再把多张表格叠起来形成三维空间。但操作系统并不直接操作这个三维结构,而是将其"拉直"成一个一维线性数组,这就是LBA的由来。


5. LBA:逻辑块地址 ------ 操作系统的统一语言

LBA是一个从0开始的连续整数,代表磁盘上每一个扇区的唯一编号。它屏蔽了底层复杂的CHS几何结构,让文件系统可以像访问内存一样按"块号"读写数据。

LBA的优势:
  • 简化寻址:无需关心磁头、柱面、扇区的物理位置。
  • 支持大容量硬盘:突破CHS的8.4GB限制(因LBA使用32位或48位地址)。
  • 便于文件系统管理:ext4、XFS等文件系统都以"块"为单位分配空间,而块本质就是若干个连续扇区的LBA集合。
CHS ↔ LBA 转换公式:

假设:

  • C = 柱面号(从0开始)
  • H = 磁头号(从0开始)
  • S = 扇区号(从1开始)
  • HPC = 每柱面磁头数(即盘面数)
  • SPT = 每磁道扇区数

则:

复制代码
LBA = (C × HPC + H) × SPT + (S - 1)

反向转换:

复制代码
C = LBA // (HPC × SPT)
H = (LBA % (HPC × SPT)) // SPT
S = (LBA % SPT) + 1

💡 注意:现代硬盘和操作系统已不再直接使用CHS,而是通过固件将LBA映射到物理位置。用户和文件系统只需关心LBA即可。


二、引入文件系统

1. 引入"块"(Block)概念

硬盘的物理存储单元是"扇区"(Sector),通常大小为 512 字节或 4KB。但操作系统不会直接按扇区读写,因为效率太低。因此,文件系统引入了"块"(Block)的概念------它是文件系统管理数据的最小单位。

  • 块的大小:由文件系统格式化时决定,常见为 4KB(即 8 个 512 字节扇区)。
  • 块的作用:操作系统以"块"为单位进行数据的读取和写入,提高 I/O 效率。
  • 块的寻址 :每个块有唯一的逻辑块地址(LBA),通过公式 块号 = LBA / 每块扇区数 可计算。

💡 注意:一个文件可能占用多个不连续的块,文件系统负责记录这些块的分布。


2. 引入"分区"(Partition)概念

一块物理硬盘可以被划分为多个逻辑区域,称为"分区"。在 Windows 中我们熟悉的 C:、D: 盘就是分区的体现。在 Linux 中,分区同样存在,但更强调其作为"设备文件"的属性(如 /dev/sda1)。

  • 分区的本质:是对硬盘空间的逻辑划分,便于管理和隔离不同用途的数据(如系统、用户数据、交换空间等)。
  • 分区表:记录每个分区的起始柱面、结束柱面、大小等信息。柱面是硬盘物理结构中的最小可寻址单位,由磁头、柱面、扇区共同定位。
  • LBA 与分区的关联:知道分区的起始 LBA 和大小,就能确定该分区内所有块的地址范围。

📌 关键点:分区是文件系统存在的"容器",而"块"是文件系统内部管理数据的基本单元。


3. 引入"inode"(索引节点)概念

这是理解 Linux 文件系统的核心!在 Linux 中,文件名 ≠ 文件本身。文件名只是指向文件元数据的一个"标签",而真正的文件信息(属性、数据位置等)存储在 inode 中。

1. 什么是 inode?
  • 定义:inode 是文件系统中用于存储文件元数据(metadata)的数据结构。
  • 内容:包括文件大小、所有者、权限、创建/修改时间、链接数、数据块指针等。
  • 唯一性:每个文件(或目录)都有且仅有一个 inode,由一个唯一的 inode 号标识。
2. 如何查看 inode?

使用 ls -li 命令可以同时显示文件的 inode 号和详细信息:

复制代码
[root@localhost linux]# ls -li
total 16
1052669 -rw-rw-r-- 1 whb whb 225 Oct 17 19:09 file1.txt
1052007 -rw-rw-r-- 1 whb whb 488 Oct 17 19:06 file2.c
...

第一列数字即为 inode 号。

3. inode 的结构(以 ext2 为例)
复制代码
struct ext2_inode {
    __le16 i_mode;      /* 文件类型和权限 */
    __le16 i_uid;       /* 所有者 UID */
    __le32 i_size;      /* 文件大小(字节) */
    __le32 i_atime;     /* 最后访问时间 */
    __le32 i_ctime;     /* 创建时间 */
    __le32 i_mtime;     /* 最后修改时间 */
    __le32 i_dtime;     /* 删除时间 */
    __le16 i_gid;       /* 所属组 GID */
    __le16 i_links_count; /* 硬链接数 */
    __le32 i_blocks;    /* 占用的块数 */
    __le32 i_flags;     /* 文件标志 */
    union {
        struct { ... } linux1;
        struct { ... } hurd1;
        struct { ... } masix1;
    } osd1;             /* OS 相关字段 */
    __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 { ... } linux2;
        struct { ... } hurd2;
        struct { ... } masix2;
    } osd2;             /* OS 相关字段 */
};

⚠️ 重要提醒

  • 文件名存储在 inode 中,而是存储在目录项(directory entry)里。
  • inode 大小通常是 128 字节或 256 字节(现代文件系统如 ext4 默认为 256 字节)。
  • 一个 inode 对应一个文件,但一个文件可以有多个文件名(硬链接),它们共享同一个 inode。

三、ext2文件系统

ext2(Second Extended File System)是 Linux 早期广泛使用的文件系统,其设计简洁、高效,至今仍被用于教学和研究。它采用"块组"(Block Group)的组织方式,将磁盘空间划分为多个逻辑单元,每个单元独立管理自己的元数据和数据块,从而提升性能和容错能力。

ext2首先将整个分区划分为若干个大小相等的 Block Group(块组)。每个块组在物理上连续,且内部结构完全相同。这种设计让文件系统可以"分而治之"------管理好一个块组,就能管理所有块组。

启动块(Boot Sector):大小为固定的1KB,用于存储引导信息。这是PC标准预留的,任何文件系统都不能占用或修改它,所以ext2的实际数据从启动块之后才开始。


1. 超级块(Super Block)

超级块存储的是整个分区级别的信息,例如:

  • block总数、inode总数

  • 空闲block/inode数量

  • block大小(通过 s_log_block_size 计算)

  • 每组的block数、inode数

  • 挂载次数、最近检查时间、魔数(0xEF53)等

⚠️ 重要 :超级块一旦损坏,整个文件系统几乎无法挂载。因此,ext2在每个块组的开头都保留一份超级块副本(第一个块组必须要有,后面的可选),以提高容错性。

内核中用 struct ext2_super_block 描述它,字段多达70+,涵盖了从兼容性标志到日志、UUID、卷名等丰富信息。


2. GDT ------ 块组的"户口本"

GDT中每一个 ext2_group_desc 对应一个块组,记录该组的:

  • bg_block_bitmap ------ 块位图所在的块号

  • bg_inode_bitmap ------ inode位图所在的块号

  • bg_inode_table ------ inode表的起始块号

  • 空闲block/inode数量、目录数量等

与超级块类似,GDT也在每个块组中保留副本,确保元数据的可靠性。


3. 位图(Bitmap)

位图是高效管理空闲资源的关键。

  • Block Bitmap:每个bit对应一个数据块,1表示已用,0表示空闲。

  • Inode Bitmap:每个bit对应一个inode,标记是否被分配。

分配新文件时,文件系统会先扫描位图找到空闲位置,然后置1,速度极快。


4. inode表 (inode Table)

inode(索引节点)存储文件的元数据,包括:

  • 文件大小

  • 所有者(UID/GID)

  • 权限(rwx)

  • 时间戳(创建/修改/访问)

  • 数据块指针(指向Data Blocks)

注意:文件名并不在inode中 ,而是存放在目录的数据块里。

inode Table是一个4KB大小的数据块,一个inode大小固定为128字节,所以inode Table会保存32个inode,当文件系统和IO交互的时候直接访问inode Table即32个inode


5. 数据块 (Data Blocks)

数据块根据文件类型存放不同内容:

  • 普通文件:直接存放文件二进制内容。

  • 目录文件 :存放该目录下的文件名及其对应的inode编号(可用 ls -li 查看)。ls -l 看到的其他信息(大小、权限等)则来自inode。

这种"目录存名字 + inode存属性 + 数据块存内容"的三层分离,是ext2高效灵活的基础。


6. 其他问题

1. 理解文件系统的载体是分区

文件系统的载体是分区,因为分区是硬盘上被独立划分出来的连续逻辑存储空间,文件系统必须在这个空间上"画"出自己的数据结构(超级块、GDT、位图、inode表、数据块),才能组织和管理文件。

所以数据块和inode是可以跨组编号但是不能跨分区,在同一分区内部inode编号和块号是唯一的

2. 怎么通过inode找到分组和数据块

分组的大小是固定的,inode 编号通过数学运算得到块组号,再通过 GDT 定位到 inode 本体;inode 内部的 15 个指针(直接/间接/双重/三重)层层索引,最终找到存放文件内容的数据块。


四、inode和datablock映射

1. 解剖 inode:不只是属性容器

ext2 的磁盘 inode 结构远比你想象的丰富(见代码片段)。除了我们熟悉的 i_mode(权限)、i_size(大小)、i_atime/i_ctime/i_mtime(时间戳)外,最关键的是那块用于块寻址的数组:

cpp 复制代码
__le32 i_block[EXT2_N_BLOCKS];  /* Pointers to blocks */

EXT2_N_BLOCKS = 15,这 15 个指针构成了 inode 到 data block 的完整映射体系。

2. 15 个指针如何撑起大文件?

这 15 个指针并非全直接指向数据块,而是分层设计的:

指针类型 数量 作用
直接块指针 12 直接指向存储文件数据的磁盘块
一级间接块指针 1 指向一个块,该块内存放更多数据块指针
二级间接块指针 1 指向一个块,该块指向多个一级间接块
三级间接块指针 1 指向一个块,该块指向多个二级间接块

这种多级索引结构,使得 ext2 能够用极小的 inode 开销(默认 128 或 256 字节)管理从几 KB 到几十 GB 的文件。


3. 知道 inode 号,如何在分区中找到它?

这依赖于文件系统格式化时建立的分组管理结构。每个分组包含:

  • Super Block(超级块)

  • Group Descriptor Table(组描述符表)

  • Block Bitmap(块位图)

  • Inode Bitmap(inode 位图)

  • inode 表数据块区

给定 inode 号后,系统按以下步骤定位:

cpp 复制代码
分组号 = (inode号 - 1) / 每组的inode数
组内偏移 = (inode号 - 1) % 每组的inode数

4. 创建一个新文件(touch)背后发生了什么?

touch abc 为例,得到的 inode 号为 263466。内核实际执行了 4 个关键动作

  1. 存储属性

    在 inode 位图中找到一个空闲 inode(263466),将文件的权限、大小、时间等写入该 inode。

  2. 存储数据

    从 block bitmap 中分配 3 个空闲数据块(如 300、500、800),将用户数据依次写入这些块。

  3. 记录分配情况

    将这 3 个块号按顺序填入 inode 的 i_block[] 数组中(前 3 个直接块指针)。

  4. 添加文件名到目录

    在当前目录的数据块中新增一条目录项:(inode=263466, 文件名="abc")

    从此,文件名 ↔ inode ↔ 属性 ↔ 数据块 的完整链条就建立了。


5. 增、删、查、改,本质都是 inode 操作

操作 核心动作
查(读) 根据文件名 → 目录项找到 inode → 读取 i_block → 加载数据块
改(写) 同上定位 inode,然后修改数据块(可能新分配/释放块)
增(创建) 分配空闲 inode + 分配空闲块 + 写入目录项
删(删除) 从目录项移除文件名,释放 inode 和数据块(标记位图空闲)

⚠️ 注意:删除文件时,inode 和数据块并不会立即擦除,只是标记为"空闲",这也是数据恢复工具的原理基础。


五、目录和路径

1. 目录的本质:特殊文件与 inode 的关联

在 ext2 文件系统中,目录本身是一种特殊的文件------它不存储"内容",而是存储「文件名 → inode 编号」的映射关系。

  • inode(索引节点):是文件系统为每个文件/目录分配的"身份证",记录了文件的元数据(权限、大小、时间戳、数据块指针等)。
  • 目录项(dentry) :目录文件中每一行记录,格式为 文件名 + inode 编号。例如执行 ls -i 时,输出的 inode 号 文件名 就是目录项的直接体现。

代码视角:遍历目录的逻辑

通过系统调用(如 opendir()readdir())可以读取目录内容,本质是解析目录文件中的 dentry 结构。示例伪代码如下:

cpp 复制代码
DIR *dir = opendir("/path/to/dir");  // 打开目录文件
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {  // 逐条读取目录项
    printf("文件名: %s, inode: %u\n", entry->d_name, entry->d_ino);
}
closedir(dir);  // 关闭目录

2. 路径解析:从字符串到 inode 的"寻址"过程

当用户输入 /home/user/test.txt 这样的路径时,内核需要逐层解析路径组件 ,最终定位到目标文件的 inode。这一过程称为路径查找(Path Lookup)

路径解析的核心步骤

  1. 起始点确定 :以根目录 / 或当前工作目录(CWD)为起点,获取其 inode。
  2. 逐组件查找 :对路径中每个子目录名(如 homeuser),在当前目录的 dentry 列表中匹配文件名,找到对应 inode。
  3. 类型校验:若当前组件是目录,继续向下解析;若为普通文件,终止查找并返回 inode。

关键数据结构:dentry(目录项缓存)

为了加速路径解析,内核维护了 dentry cache (目录项缓存),用 struct dentry 表示:

复制代码
struct dentry {
    atomic_t d_count;        // 引用计数
    unsigned int d_flags;    // 标志位
    struct inode *d_inode;   // 指向对应的 inode
    struct list_head d_child;// 父目录的子目录链表
    struct list_head d_subdirs; // 子目录链表
    char d_name[NAME_MAX];   // 文件名
};
  • 缓存命中:若路径组件的 dentry 已在缓存中,直接复用,避免重复磁盘 I/O。
  • 缓存淘汰:采用 LRU(最近最少使用)策略,内存紧张时释放冷数据。

六、挂载

当我们能根据inode号在分区内自由定位文件,能通过目录项找到目标inode时,似乎已在单个分区内"为所欲为"。但一个根本问题始终悬而未决:inode不能跨分区,Linux却支持多个分区,系统怎么知道我该去哪个分区找文件?


1. 从"分区独立"到"全局树"的鸿沟

在Ext2/Ext4等文件系统中,每个分区都拥有自己独立的inode表、数据块和目录树。inode号只在所属分区内唯一,不同分区的inode 1001毫无关联。这意味着:

  • 访问 /home/user/a.txt 时,系统不能只靠inode号

  • 必须首先确定 /home/user 这个路径对应哪个分区


2. 实验验证:分区不能"裸用"

cpp 复制代码
# 创建一个5MB的文件作为模拟分区
$ dd if=/dev/zero of=/tmp/myfs bs=1M count=5

# 格式化为ext4文件系统
$ mkfs.ext4 /tmp/myfs

# 尝试直接读写该"分区文件" ------ 失败!
$ echo "hello" > /tmp/myfs/hello.txt  
# 输出:No such file or directory

结论

分区写入文件系统后,无法直接使用,必须通过挂载(mount)与目录树中的某个目录关联,才能被用户访问。这就好比一块硬盘分区是"孤岛",挂载点是搭建到主大陆的"桥梁"。


3. 挂载的本质:建立"路径前缀 → 分区"的映射

Linux内核维护一张挂载表(mount table),记录了每个挂载点与对应分区的关联:

挂载点(目录) 设备/分区 文件系统类型
/ /dev/sda2 ext4
/home /dev/sda3 ext4
/mnt/usb /dev/sdb1 vfat

当访问 /home/user/file.txt 时,内核进行最长前缀匹配

  1. 检查路径的每一级前缀://home/home/user

  2. 匹配到 /home 是挂载点,对应 /dev/sda3

  3. 将路径剩余部分 /user/file.txt 交给 /dev/sda3 分区的文件系统解析

  4. 在该分区的根目录下找 user 目录,再找 file.txt

关键认知 :每个分区都有自己独立的根目录(inode号为2),挂载操作本质上是把某个分区的根目录"嫁接"到主目录树的某个节点上


4. 跨分区访问的完整链路(以 /home/test.log 为例)

cpp 复制代码
用户态访问 /home/test.log
        ↓
VFS(虚拟文件系统)层:解析路径
        ↓
挂载表匹配 → 最长前缀匹配到 /home(挂载点)
        ↓
找到对应分区 /dev/sda3,读取其超级块
        ↓
在该分区的根目录(inode 2)下查找 "test.log"
        ↓
返回该分区的inode号 → 读取数据块

重点 :切换分区发生在挂载点边界,一旦进入某个分区,其内部所有路径查找(包括目录项、inode)均局限于该分区,绝不跨区。


七、软硬链接

在 Linux 文件系统中,硬链接软链接是两种用于实现"一个文件多个访问路径"的机制,它们在底层原理、使用场景和行为表现上存在本质区别。


1. 硬链接:inode 的多重映射

硬链接的本质是多个文件名指向同一个 inode。这意味着,无论你通过哪个文件名访问文件,操作的都是同一份数据块。例如:

复制代码
touch abc
ln abc def
ls -li abc def
# 输出:263466 abc 和 263466 def ------ inode 号完全一致
  • 删除行为:删除一个硬链接文件,只是将该目录项移除,并将 inode 的硬链接计数减 1。只有当计数归零时,系统才会真正释放磁盘空间。
  • 限制:硬链接不能跨文件系统创建,也不能对目录创建(防止循环引用)。
  • 典型用途
    • . .. 就是目录的硬链接;
    • 用于文件备份或共享,避免重复存储。

2. 软链接:独立文件的"快捷方式"

软链接是一个独立的文件,它拥有自己的 inode,内容仅保存目标文件的路径字符串。例如:

复制代码
ln -s abc abc.s
ls -li abc.s
# 输出:261678 lrwxrwxrwx ... abc.s -> abc ------ 有独立 inode,类型为符号链接
  • 行为特征
    • 删除原文件后,软链接会变成"悬空链接",访问时会报错;
    • 可以跨文件系统、可以对目录创建;
    • 权限为 lrwxrwxrwx,实际访问权限取决于目标文件。
  • 典型用途
    • 类似 Windows 的快捷方式,用于简化路径或版本管理;
    • 程序部署中指向不同版本的库或配置文件。