【Linux之旅】深入 Linux Ext 系列文件系统:从磁盘物理结构到软硬链接的底层逻辑

前言

你是否曾好奇:当你在 Linux 中执行touch test.txt时,文件究竟是如何 "住进" 磁盘的?为什么删除文件时,有时删的是 "别名",有时却能彻底清空数据?为什么ls -li会显示一个看似无关的数字(inode 号)?这些问题的答案,都藏在 Linux 最经典的Ext 系列文件系统(Ext2/Ext3/Ext4)中。

今天,我们就从 "磁盘硬件" 出发,一步步拆解 Ext 文件系统的设计逻辑 ------ 从物理扇区到逻辑块,从 inode 到块组,再到目录、挂载和软硬链接,带你看懂 Linux 文件存储的底层原理。

请君浏览

    • 前言
    • [一. 开篇:为什么需要文件系统?](#一. 开篇:为什么需要文件系统?)
    • 二、磁盘基础:从物理结构到逻辑地址
      • [2.1 磁盘物理结构:盘片、磁道、扇区与柱面](#2.1 磁盘物理结构:盘片、磁道、扇区与柱面)
      • [2.2 磁盘逻辑结构:从物理结构抽象为线性结构](#2.2 磁盘逻辑结构:从物理结构抽象为线性结构)
      • [2.3寻址方式:从 CHS 到 LBA](#2.3寻址方式:从 CHS 到 LBA)
    • [三、Ext 文件系统核心:块、分区与 inode](#三、Ext 文件系统核心:块、分区与 inode)
      • [3.1 块(Block):文件存取的最小单位](#3.1 块(Block):文件存取的最小单位)
      • [3.2 分区(Partition):磁盘的 "逻辑切片"](#3.2 分区(Partition):磁盘的 “逻辑切片”)
      • [3.3 inode:文件的 "身份证"](#3.3 inode:文件的 “身份证”)
    • [四、Ext2 文件系统架构:块组与管理结构](#四、Ext2 文件系统架构:块组与管理结构)
      • [4.1 宏观认识](#4.1 宏观认识)
      • [4.2 超级块(Super Block):文件系统的 "说明书"](#4.2 超级块(Super Block):文件系统的 “说明书”)
      • [4.3 GDT(Group Descriptor Table):块组的 "目录"](#4.3 GDT(Group Descriptor Table):块组的 “目录”)
      • [4.4 块位图(Block Bitmap)与 inode 位图(Inode Bitmap):空闲资源的 "登记簿"](#4.4 块位图(Block Bitmap)与 inode 位图(Inode Bitmap):空闲资源的 “登记簿”)
      • [4.5 inode 表(Inode Table):文件属性的 "仓库"](#4.5 inode 表(Inode Table):文件属性的 “仓库”)
      • [4.6 数据块(Data Blocks):文件内容的 "存储柜"](#4.6 数据块(Data Blocks):文件内容的 “存储柜”)
    • 五、目录、路径解析与挂载:如何找到文件?
      • [5.1 目录的本质:文件名与 inode 的映射表](#5.1 目录的本质:文件名与 inode 的映射表)
      • [5.2 路径解析和路径缓存:从根目录开始的 "寻宝游戏"](#5.2 路径解析和路径缓存:从根目录开始的 “寻宝游戏”)
      • [5.3 挂载:分区与目录的 "绑定"](#5.3 挂载:分区与目录的 “绑定”)
    • [六、软硬链接:文件的 "别名" 机制](#六、软硬链接:文件的 “别名” 机制)
      • [6.1 硬链接:inode 相同的 "亲兄弟"](#6.1 硬链接:inode 相同的 “亲兄弟”)
      • [6.2 软链接:存储路径的 "快捷方式"](#6.2 软链接:存储路径的 “快捷方式”)
      • [6.3 软硬链接对比](#6.3 软硬链接对比)
    • [七、总结:Ext 文件系统的设计思想](#七、总结:Ext 文件系统的设计思想)
    • 尾声

一. 开篇:为什么需要文件系统?

磁盘是计算机的 "仓库",但它本身只是一堆 "冰冷的硬件"------ 由盘片、磁头、扇区组成,只能按物理地址(如 "第 3 个盘片第 5 个磁道第 2 个扇区")存储数据。如果直接让用户按物理地址操作磁盘,不仅效率极低(每次只能操作 1 个扇区,512 字节),还会导致数据混乱(文件内容互相覆盖)。

文件系统(File System)操作系统用于管理磁盘等存储设备的一套规则、数据结构和接口集合------ 它就像磁盘的 "管理员 + 翻译官",一边对接底层冰冷的存储硬件(如磁盘、U 盘),一边为上层用户 / 程序提供 "文件名、路径" 等友好的操作方式,核心目标是让 "存储数据" 变得有序、高效、安全。

文件系统的核心作用,就是给磁盘 "制定规则":

  1. 把磁盘空间划分为 "可管理的单元"(如块、分区);
  2. 分离存储 "文件属性"(如大小、权限、修改时间)和 "文件内容";
  3. 建立 "文件名→文件数据" 的映射(通过目录和 inode);
  4. 管理空闲资源(哪些空间可用,哪些已占用)。

简单说:没有文件系统的磁盘,只是一堆能存储 0 和 1 的 "裸硬件";有了文件系统,磁盘才变成了能分类存放 "文件" 的 "智能仓库"。

Ext 系列文件系统(Ext2/Ext3/Ext4)是 Linux 最主流的文件系统,其中 Ext2 是基础,Ext3/Ext4 在其之上增加了日志(防止断电数据丢失)等特性,核心设计完全一致。我们以 Ext2 为原型,讲解文件系统的底层逻辑。

二、磁盘基础:从物理结构到逻辑地址

磁盘(Disk)是计算机中用于长期存储数据的硬件设备,它是块设备,也是操作系统和用户数据的 "最终归宿"------ 它通过物理介质(磁性材料、闪存芯片等)记录二进制数据(0 和 1),即使断电也不会丢失数据,是计算机 "持久化存储" 的核心载体。

是我们要理解文件系统,首先得懂磁盘的 "语言"------ 物理结构和寻址方式。

2.1 磁盘物理结构:盘片、磁道、扇区与柱面

机械磁盘的物理结构类似 "多碟 CD 机",核心组件包括:

  • 盘片:圆形金属盘,两面都能存储数据(类似 CD 的正反两面);
  • 磁头:对应每个盘面,负责读写数据(类似 CD 机的激光头);
  • 磁道:盘片上的同心圆,每个同心圆是一个磁道(类似 CD 上的纹路);
  • 扇区 :磁道被分成的扇形区域,是磁盘最小物理存储单位(默认 512 字节);
  • 柱面:所有盘片上 "半径相同的磁道" 组成的圆柱(磁头移动时 "共进退",同一柱面的磁道无需移动磁头,读写更快)。

我们在磁盘中通过一个个的扇区来存储数据,而扇区就是磁道被分成的扇形区域,如下图所示:

当然,磁盘并不是只有一张盘片,一个磁盘中有多个盘面,因此就有多个磁头,这些磁头是共进退的,也就是说它们会同时执行不同盘面的相同位置的扇区,因此所有盘面中的同心圆,也就是相对位置相同的磁道就组成了一个柱面,如下图所示:

这就是磁盘的物理结构,我们就可以通过这些来计算一个磁盘可以存储的容量大小。

举个例子:一张磁盘有 2 个盘片(4 个盘面),每个磁道有 63 个扇区,共 1024 个磁道,其容量计算为:容量 = 磁头数(4)× 柱面数(1024)× 每磁道扇区数(63)× 每扇区字节数(512)= 131072 × 512 = 67108864字节 = 64MB

2.2 磁盘逻辑结构:从物理结构抽象为线性结构

上面我们知道了磁盘的物理结构,那么在OS中我们该如何去把物理结构抽象成逻辑结构呢?

我们可以把每一个盘片看作是"磁带",我们可以将盘在一起的"磁带"拉成一条直线,形成线性结构:

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

这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做LBA :

但实际上,磁盘的逻辑结构并不是由一个个盘片为单位展开的,我们上面说过,磁盘的所有磁头是共进退的,因此实际上磁盘的逻辑结构是由一个个柱面为单位展开的,整个磁盘所有盘⾯的同⼀个磁道,即柱⾯展开:

柱⾯上的每个磁道,扇区个数是⼀样的,因此我们一个柱面展开后就是一个二维数组,那么整个磁盘就是多张⼆维的扇区数组表,那么我们也就知道在磁盘上进行寻址(也就是物理结构上)需要先找到哪⼀个柱⾯(Cylinder) ,再确定柱⾯内哪⼀个磁道(其实就是磁头位置, Head) ,在确定扇区(Sector),所以就有了CHS寻址。

我们之前学过C/C++的数组,在我们看来,整个磁盘其实全部都是⼀维数组:

那么每⼀个扇区都有⼀个下标,这个下标我们叫做LBA(Logical Block Address)地址,其实就是线性地址。这样,我们在OS中就可以通过一个数字下标去找到对应的扇区。这就是磁盘的逻辑结构。

因此OS只需要使用LBA就可以进行寻址了,对于LBA地址转成CHS地址,CHS如何转换成为LBA地址由磁盘⾃⼰来做,不需要我们关心,下面我们来简单看一下它们如何进行转换。

2.3寻址方式:从 CHS 到 LBA

要读取一个扇区,需要定位 "哪个磁头(盘面)、哪个柱面(磁道)、哪个扇区"------ 这就是CHS 寻址(Cylinder/Head/Sector)。

但 CHS 有个致命局限:早期系统用 8 位存磁头、10 位存柱面、6 位存扇区,最大支持容量仅 8GB(2^8 × 2^10 × 2^6 × 512B),无法满足大磁盘需求。于是LBA 寻址(Logical Block Address)应运而生:把磁盘所有扇区看作一个 "一维数组",每个扇区对应一个唯一的 "逻辑地址"(数组下标),比如第 0 个扇区、第 1 个扇区...... 磁盘硬件会自动将 LBA 地址转换为 CHS 地址,上层(操作系统)只需用 LBA 即可。

CHS 与 LBA 的转换公式:

  • CHS 转 LBALBA = 柱面号 × 磁头数 × 每磁道扇区数 + 磁头号 × 每磁道扇区数 + 扇区号 - 1(扇区号从 1 开始,LBA 从 0 开始);
  • LBA 转 CHS柱面号 = LBA // (磁头数×每磁道扇区数)磁头号 = (LBA % (磁头数×每磁道扇区数)) // 每磁道扇区数扇区号 = (LBA % 每磁道扇区数) + 1

对用户而言,磁盘从此变成了 "按 LBA 访问的一维数组",文件系统只需基于 LBA 管理数据。

三、Ext 文件系统核心:块、分区与 inode

Ext 文件系统在磁盘基础上,定义了三个关键概念:块、分区、inode,它们是文件存储的 "基石"。

3.1 块(Block):文件存取的最小单位

硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区。Ext 文件系统将连续的多个扇区组合成 "块",作为文件存取的最小单位(类似把几页纸装订成 "小册子",一次拿一本),⼀次性读取⼀个"块"(block)。

  • 块大小:格式化时指定(常见 4KB,即 8 个扇区),一旦确定不可修改;

  • 块地址:基于 LBA 计算,比如块大小 4KB(8 扇区),则块号 = LBA // 8块内扇区 = LBA % 8

  • 查看块信息:用stat命令,比如stat test.c会显示Blocks: 8(表示文件占用 8 个块,共 32KB)、IO Block: 4096(块大小 4KB)。

"块"是⽂件系统的基本单位。

3.2 分区(Partition):磁盘的 "逻辑切片"

其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将它分区成C,D,E盘。那么C,D,E就是不同的分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备都是以⽂件形式存在,那么它是怎么分区的呢?

柱⾯是分区的最⼩单位 ,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起始柱⾯和结束柱⾯号码。 此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平⾯,如下图所⽰:

  • 分区本质:划定磁盘的 "起始柱面" 和 "结束柱面",每个分区的块从 0 开始编号;
  • 分区目的:隔离数据(如系统分区和数据分区)、支持不同文件系统;
  • 查看分区:用fdisk -l命令,比如/dev/vda1是第一个分区,StartEnd列显示其扇区范围。

柱⾯⼤⼩⼀致,扇区个数⼀致,那么其实只要知道每个分区的起始和结束柱⾯号,就可以知道每⼀个柱⾯有多少个扇区,那么该分区的大小以及该分区的LBA起止范围也就清楚了。

3.3 inode:文件的 "身份证"

我们知道文件 = 属性(元数据) + 内容。Ext 文件系统将 "文件属性" 单独存储在inode(索引节点) 中,每个文件对应一个唯一的 inode(在同一个分区中),包含:

  • 基础属性:文件类型(普通文件 / 目录 / 链接)、权限(rwx)、所有者(UID/GID)、大小、修改时间(Access/Modify/Change);
  • 内容映射:inode 中的i_block数组(Ext2 中 15 个元素),存储 "文件内容所在的块号"(直接块、间接块,支持大文件存储);
  • 关键特点:inode 号唯一(以分区为单位),文件名不存储在 inode 中(文件名存在目录的内容里)。

查看 inode:用ls -li命令,第一列就是 inode 号,如下图所示:

表示test.c的 inode 号是 525789。

⽂件数据都储存在"块"中,那么很显然,我们还必须找到⼀个地⽅储存⽂件的元信息(属性信息),⽐如⽂件的创建者、⽂件的创建⽇期、⽂件的⼤⼩等等。这种储存⽂件元信息的区域就叫做inode,中⽂译名为"索引节点"。

因此Linux中⽂件的存储是属性和内容分离存储的。下面是Linux源码中的inode,它是一个结构体:

cpp 复制代码
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
    __le16 i_mode; /* File mode */
    __le16 i_uid; /* Low 16 bits of Owner Uid */
    __le32 i_size; /* Size in bytes */
    __le32 i_atime; /* Access time */
    __le32 i_ctime; /* Creation time */
    __le32 i_mtime; /* Modification time */
    __le32 i_dtime; /* Deletion Time */
    __le16 i_gid; /* Low 16 bits of Group Id */
    __le16 i_links_count; /* Links count */
    __le32 i_blocks; /* Blocks count */
    __le32 i_flags; /* File flags */
    union {
        struct {
            __le32 l_i_reserved1;
        } linux1;
        struct {
            __le32 h_i_translator;
        } hurd1;
        struct {
            __le32 m_i_reserved1;
        } masix1;
    } osd1; /* OS dependent 1 */
    __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
    __le32 i_generation; /* File version (for NFS) */
    __le32 i_file_acl; /* File ACL */
    __le32 i_dir_acl; /* Directory ACL */
    __le32 i_faddr; /* Fragment address */
    union {
        struct {
            __u8 l_i_frag; /* Fragment number */
            __u8 l_i_fsize; /* Fragment size */
            __u16 i_pad1;
            __le16 l_i_uid_high; /* these 2 fields */
            __le16 l_i_gid_high; /* were reserved2[0] */
            __u32 l_i_reserved2;
        } linux2;
        struct {
            __u8 h_i_frag; /* Fragment number */
            __u8 h_i_fsize; /* Fragment size */
            __le16 h_i_mode_high;
            __le16 h_i_uid_high;
            __le16 h_i_gid_high;
            __le32 h_i_author;
        } hurd2;
        struct {
            __u8 m_i_frag; /* Fragment number */
            __u8 m_i_fsize; /* Fragment size */
            __u16 m_pad1;
            __u32 m_i_reserved2[2];
        } masix2;
    } osd2; /* OS dependent 2 */
};
/*
* Constants relative to the data blocks
*/
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)

我们要知道,⽂件名属性并未纳⼊到inode数据结构内部,至于为什么,下面我们再做解释。并且对于任何文件,它们的内容大小是不同的,但属性大小一定是相同的,因为它们的属性都存放在inode结构体中,这里也是一个inode中不存放文件名的原因:文件名的大小是不固定的,如果在inode中存放文件名,那么inode的大小也将不固定。

到目前为止,我们对文件系统的核心有了一定的了解,但是:

  • 我们已经知道硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,读取的基本单位是"块"。"块"⼜是硬盘的每个分区下的结构,难道"块"是随意的在分区上排布的吗?那要怎么找到"块"呢?
  • 还有就是上⾯提到的存储⽂件属性的inode,⼜是如何放置的呢?

⽂件系统就是为了组织管理这些的,下面让我们来看一看Ext2文件系统是如何做的。

四、Ext2 文件系统架构:块组与管理结构

文件系统的载体是分区,也就是说每一个分区都有对应的文件系统去进行管理,因此我们只需要了解一个文件系统对一个分区的管理,就可以推广到该文件系统对其他分区的管理。

4.1 宏观认识

所有的准备⼯作都已经做完,是时候认识下⽂件系统了。我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。在 Linux 系统中,最常⻅的是 Ext2 系列的⽂件系统。其早期版本为 Ext2,后来⼜发展出 Ext3 和 Ext4。Ext3 和 Ext4 虽然对 Ext2 进⾏了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的 Ext2 作为演⽰对象。

Ext2⽂件系统将整个分区划分成若⼲个同样⼤小的块组 (Block Group),每个块组结构相同,类似 "小区分栋管理"------ 既方便维护,又能减少磁头移动(同一块组的块物理位置相近)。如下图所⽰:

只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件

上图中启动块(Boot Block/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,⽤来存储磁盘分区信息和启动信息,任何⽂件系统都不能修改启动块。启动块之后才是ext2⽂件系统的开始。

因此我们研究Ext2文件系统,主要就在于研究块组,下面让我们来看一下一个块组是由哪几部分构成的:

4.2 超级块(Super Block):文件系统的 "说明书"

超级块是文件系统的 "大脑",存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:

  • 基础参数:块总数、inode 总数、空闲块 /inode 数、块大小、inode 大小;
  • 状态信息:最近挂载时间、最近写入时间、文件系统状态(正常 / 错误);

Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了。超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份,这些super block区域的数据保持⼀致。

下面是Linux内核源码中存储超级块的结构体:

cpp 复制代码
/*
* Structure of the super block
*/
struct ext2_super_block {
    __le32 s_inodes_count; /* Inodes count */
    __le32 s_blocks_count; /* Blocks count */
    __le32 s_r_blocks_count; /* Reserved blocks count */
    __le32 s_free_blocks_count; /* Free blocks count */
    __le32 s_free_inodes_count; /* Free inodes count */
    __le32 s_first_data_block; /* First Data Block */
    __le32 s_log_block_size; /* Block size */
    __le32 s_log_frag_size; /* Fragment size */
    __le32 s_blocks_per_group; /* # Blocks per group */
    __le32 s_frags_per_group; /* # Fragments per group */
    __le32 s_inodes_per_group; /* # Inodes per group */
    __le32 s_mtime; /* Mount time */
    __le32 s_wtime; /* Write time */
    __le16 s_mnt_count; /* Mount count */
    __le16 s_max_mnt_count; /* Maximal mount count */
    __le16 s_magic; /* Magic signature */
    __le16 s_state; /* File system state */
    __le16 s_errors; /* Behaviour when detecting errors */
    __le16 s_minor_rev_level; /* minor revision level */
    __le32 s_lastcheck; /* time of last check */
    __le32 s_checkinterval; /* max. time between checks */
    __le32 s_creator_os; /* OS */
    __le32 s_rev_level; /* Revision level */
    __le16 s_def_resuid; /* Default uid for reserved blocks */
    __le16 s_def_resgid; /* Default gid for reserved blocks */
/*
* These fields are for EXT2_DYNAMIC_REV superblocks only.
* *
Note: the difference between the compatible feature set and
* the incompatible feature set is that if there is a bit set
* in the incompatible feature set that the kernel doesn't
* know about, it should refuse to mount the filesystem.
* *
e2fsck's requirements are more strict; if it doesn't know
* about a feature in either the compatible or incompatible
* feature set, it must abort and not try to meddle with
* things it doesn't understand...
*/
    __le32 s_first_ino; /* First non-reserved inode */
    __le16 s_inode_size; /* size of inode structure */
    __le16 s_block_group_nr; /* block group # of this superblock */
    __le32 s_feature_compat; /* compatible feature set */
    __le32 s_feature_incompat; /* incompatible feature set */
    __le32 s_feature_ro_compat; /* readonly-compatible feature set */
    __u8 s_uuid[16]; /* 128-bit uuid for volume */
    char s_volume_name[16]; /* volume name */
    char s_last_mounted[64]; /* directory where last mounted */
    __le32 s_algorithm_usage_bitmap; /* For compression */
    /*
* Performance hints. Directory preallocation should only
* happen if the EXT2_COMPAT_PREALLOC flag is on.
*/
    __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
    __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
    __u16 s_padding1;
    /*
* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
*/
    __u8 s_journal_uuid[16]; /* uuid of journal superblock */
    __u32 s_journal_inum; /* inode number of journal file */
    __u32 s_journal_dev; /* device number of journal file */
    __u32 s_last_orphan; /* start of list of inodes to delete */
    __u32 s_hash_seed[4]; /* HTREE hash seed */
    __u8 s_def_hash_version; /* Default hash version to use */
    __u8 s_reserved_char_pad;
    __u16 s_reserved_word_pad;
    __le32 s_default_mount_opts;
    __le32 s_first_meta_bg; /* First metablock block group */
    __u32 s_reserved[190]; /* Padding to the end of the block */
};

查看超级块信息:用dumpe2fs /dev/vda1 | grep -i superblock命令,可看到超级块的位置和内容。

4.3 GDT(Group Descriptor Table):块组的 "目录"

GDT也叫块组描述符表,用来描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组对应一个描述符,记录该块组的 "资源位置":

  • 块位图位置(bg_block_bitmap):该块组的块位图存在哪个块;
  • inode 位图位置(bg_inode_bitmap):该块组的 inode 位图存在哪个块;
  • inode 表位置(bg_inode_table):该块组的 inode 表从哪个块开始;
  • 空闲资源数:bg_free_blocks_count(空闲块数)、bg_free_inodes_count(空闲 inode 数)。
cpp 复制代码
// 磁盘级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];
};

4.4 块位图(Block Bitmap)与 inode 位图(Inode Bitmap):空闲资源的 "登记簿"

位图是 "高效管理空闲资源" 的工具,用 1 个 bit 表示 1 个资源的状态(0 = 空闲,1 = 占用):

  • 块位图:1 个 bit 对应 1 个块,标记该块组中哪些块已占用、哪些空闲;
  • inode 位图:1 个 bit 对应 1 个 inode,标记该块组中哪些 inode 已占用、哪些空闲。

相信大家在日常中会发现我们下载东西时会比较慢,但是删除时却非常的快,这是因为我们下载内容时需要通过位图来找到空闲的数据块和inode号,再对其进行写入,而删除时我们直接将对应的位图标记为空即可,不需要再将对应的数据块以及inode清空。

这也是为什么删除文件后,用数据恢复工具能找回文件(只要数据块没被新内容覆盖)------ 本质是 "逻辑上释放空间,物理上保留数据"。

4.5 inode 表(Inode Table):文件属性的 "仓库"

inode 表是连续的块,存储该块组所有 inode 的具体内容(每个 inode 默认 128 字节或 256 字节)。比如块组的 inode 表从块100 开始,那么块100 ~ 块103 可能存储了 64 个 inode(128 字节 / 个)。

每个 inode 有唯一编号(分区内连续),比如第一个块组的 inode 从 1 开始,第二个块组从 "每块组 inode 数 + 1" 开始。也就是说在一个分区内每一个inode号只有一个。

如上图所示,每一行就代表一个文件的文件属性。

在一个文件的inode中存在着12个直接指针块以及一级到三级的间接块索引表指针,用来指向该文件所对应的内容存放的数据块,如下图所示:

这里我们要知道,由于每一个块组的大小是相同的,也就是说每一个块组中的数据块的个数是一样的,那么当一个文件的内容过大时,其内容也可能存放到其他块组的数据块中,具体是哪些数据块都存放在索引表中。

4.6 数据块(Data Blocks):文件内容的 "存储柜"

数据块是块组中最大的区域,存储文件的实际内容,根据文件类型不同,存储方式也不同:

  • 普通文件:直接存储文件内容(小文件用直接块,大文件用间接块);
  • 目录 :存储 "文件名→inode 号" 的映射表(比如目录test的数据块中,有test.c → 1052007的记录);
  • 链接文件:硬链接无独立数据块(复用目标文件的 inode),软链接的数据块存储 "目标文件路径"。

数据块的块号在一个分区中也是唯一的,也就是说inode号和数据块都是跨组编号的,不能跨分区,所以在同一个分区内部inode号和块号都是唯一的。

分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block Bitmap、Inode Bitmap等管理信息,这些管理信息统称:⽂件系统。

只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定是哪⼀个inode,拿到对应inode后⽂件属性和内容就全部都有了。

下面我们来看一下创建一个文件的步骤:

  1. 存储属性内核先找到⼀个空闲的i节点(例如263466)。内核把⽂件信息记录到其中。
  2. 存储数据该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块数据复制到300,下⼀块复制到500,以此类推。
  3. 记录分配情况⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加⽂件名到⽬录新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来。

五、目录、路径解析与挂载:如何找到文件?

我们平时用/home/hcy/test.c这样的路径访问文件,但 Linux 底层是如何通过路径找到磁盘上的扇区?这需要理解 "目录的本质""路径解析" 和 "挂载" 三个关键点。

5.1 目录的本质:文件名与 inode 的映射表

我们上面说过,inode中只存放文件的属性,并不存放文件名,但是我们在访问文件时都是使用的文件名,基本上不会使用inode号,这是为什么呢?

这需要我们对目录有一个清晰的认识!那么在Linux中是如何看待目录的呢?我们知道在Linux下一切皆文件,目录也是一种文件,那么它也有对应属性和内容,也就是说目录也有对应的inode和数据块,目录的属性不必多说,关键在于目录文件的内容:

  • 目录文件的内容保存的是文件名和inode号的映射关系!

也就是说,文件的文件名都保存在它所在目录文件的内容中!

比如ls -li /home/hcy显示:

这意味着/home/hcy的数据块中,有两条记录:test.c → 525789demo1 → 525787demo1是目录,其 inode 指向自己的 inode 表和数据块)。

所以当我们访问⽂件时,必须打开当前⽬录,根据⽂件名来获得对应的inode号,然后进行⽂件访问,因此我们想要访问文件就必须要知道当前目录,本质是必须能打开当前工作目录文件,查看目录文件的内容。

5.2 路径解析和路径缓存:从根目录开始的 "寻宝游戏"

我们已经知道访问文件需要打开当前⼯作⽬录⽂件,查看当前⼯作⽬录⽂件的内容,但是当前⼯作⽬录不也是一个⽂件吗?我们访问当前⼯作⽬录不也是只知道当前⼯作⽬录的⽂件名吗?要访问它也得知道当前⼯作⽬录的inode啊。所以我们也要打开当前目录的上级目录,同理,上级目录也有它自己的上级目录,这个过程类似于递归,出口是"/"根⽬录。

实际上,任何⽂件都有其路径,当访问⽬标⽂件,⽐如: /home/hcy/test.c都要从根⽬录开始,依次打开每⼀个⽬录,根据⽬录名,依次访问每个⽬录下指定的⽬录,直到访问到test.c。这个过程叫做Linux路径解析。

这也就是访问文件必须有路径的原因,我们可以用相对路径和绝对路径两种方式来进行访问,其实它们的本质是一样的:能使用相对路径访问是因为我们要访问的文件的PCB中存放着当前工作目录cwd,当我们使用相对路径时OS会自动将其拼接成绝对路径。

路径/home/hcy/test.c的解析过程,就像从 "小区大门" 找 "某栋楼某户":

  1. 根目录(/)的 inode 号是固定的(通常是 2),系统开机后已知;
  2. 打开根目录的数据块,找到home → inode-A的记录;
  3. 打开home的 inode(inode-A),找到其数据块位置,从中找到whb → inode-B的记录;
  4. 打开hcy的 inode(inode-B),找到其数据块位置,从中找到test.c → inode-C的记录;
  5. 打开test.c的 inode(inode-C),从i_block数组找到存储内容的数据块,读取文件。

这就是 "路径解析" 的核心 ------ 从根目录开始,逐层解析每个目录的 "文件名→inode" 映射,直到找到目标文件的 inode。

根⽬录固定⽂件名,inode号,⽆需查找,系统开机之后就必须知道

Linux 的根目录及系统缺省目录构成了目录树的基础骨架,用户新建的目录则对其进行扩展,两者共同形成了完整的目录树;而路径是基于这个目录树,描述文件 / 目录位置的访问标识 ------ 因此系统和用户共同构建了 Linux 路径结构的基础 。

思考一下:在Linux的磁盘中真的存在目录树吗?也就是目录的树状结构。答案是不存在的,在磁盘中只有文件的属性和内容。那么在我们访问文件时,是不是就意味着每一次访问都需要从根目录开始进行路径解析呢?从原则上来说是这样的,但是这种方式的效率太过低下,因此OS会缓存历史路径结构。

所以在Linux中目录的概念是由OS产生的,OS自己在内存中进行路径维护。

Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry

cpp 复制代码
struct dentry {
    atomic_t d_count;
    unsigned int d_flags; /* protected by d_lock */
    spinlock_t d_lock; /* per dentry lock */
    struct inode *d_inode; /* Where the name belongs to - NULL is negative */
    /*
    * The next three fields are touched by __d_lookup. Place them here
    * so they all fit in a cache line.
    */
    struct hlist_node d_hash; /* lookup hash list */
    struct dentry *d_parent; /* parent directory */
    struct qstr d_name;
    struct list_head d_lru; /* LRU list */
    /*
    * d_child and d_rcu can share memory
    */
    union {
        struct list_head d_child; /* child of parent list */
        struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs; /* our children */
    struct list_head d_alias; /* inode alias list */
    unsigned long d_time; /* used by d_revalidate */
    struct dentry_operations *d_op;
    struct super_block *d_sb; /* The root of the dentry tree */
    void *d_fsdata; /* fs-specific data */
    #ifdef CONFIG_PROFILING
    struct dcookie_struct *d_cookie; /* cookie, if any */
    #endif
    int d_mounted;
    unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
}

对于dentry,我们需要注意以下几点:

  • 每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中形成整个树形结构
  • 整个树形节点也同时会⾪属于LRU(Least Recently Used,最近最少使⽤)结构中,进⾏节点淘汰
  • 整个树形节点也同时会⾪属于Hash,⽅便快速查找

更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径。

这也是为什么当我们在整个根目录下查找某些文件时第一次很慢,第二次查找相同的文件时就会很快的原因。

5.3 挂载:分区与目录的 "绑定"

我们已经能够根据inode号在指定分区找到对应的⽂件,也能根据目录⽂件的内容通过文件名找到指定的inode,也就是说在指定的分区内,我们可以为所欲为了。可是问题来了,我们该怎么知道我们再哪一个分区呢?这里我们直接说结论:

分区写⼊⽂件系统,⽆法直接使⽤,需要和指定的⽬录关联,进⾏挂载才能使⽤。所以,可以根据访问⽬标⽂件的"路径前缀"准确判断在哪⼀个分区。

也就是说我们需要将分区挂载到目录上才可以使用这个分区(单单的只对分区初始化是不能直接使用的),通过进入这个目录,就相当于进入这个分区。

分区(如/dev/vda1)本身是 "孤立的逻辑磁盘",必须通过挂载(mount) 与某个目录(如/mnt)关联,才能被访问。

  • 挂载本质:将分区的文件系统 "挂载" 到目录上,此后访问该目录,实际是访问分区的文件系统;

  • 挂载命令:mount -t ext4 /dev/vda1 /mnt(将/dev/vda1分区以 Ext4 格式挂载到/mnt);

  • 卸载命令:umount /mnt(解除关联);

  • 查看挂载:df -h命令,显示所有已挂载的分区和关联目录。

比如用dd if=/dev/zero of=disk.img bs=1M count=5创建一个 5MB 的模拟磁盘,用mkfs.ext4 disk.img格式化为 Ext4,再mount disk.img /mnt/mydisk,此时/mnt/mydisk就对应这个模拟磁盘的文件系统。

至此,对于文件系统我们已经基本上了解了,下面我们通过一张图来进行一下总结:

六、软硬链接:文件的 "别名" 机制

有时我们需要给文件创建 "别名",Linux 提供了两种链接方式:硬链接和软链接,它们的底层逻辑完全不同。

6.1 硬链接:inode 相同的 "亲兄弟"

硬链接是 "给文件的 inode 多一个文件名映射"------ 多个文件名对应同一个 inode,本质是 "同一文件的不同别名"。

  • 创建命令:ln 目标文件 硬链接名(如ln test.c test.hard);
  • 核心特点:
    1. 硬链接与目标文件的 inode 号相同(ls -li可验证);
    2. 硬链接数记录在 inode 的i_links_count中(创建硬链接时 + 1,删除时 - 1,为 0 时才释放磁盘);
    3. 不能跨分区(inode 号仅在分区内唯一);
    4. 不能链接目录(防止循环引用,如ln /home /home/link会报错)。

用途 :文件备份(删除原文件,硬链接仍能访问)、系统特殊目录(.是当前目录的硬链接,..是上级目录的硬链接,ls -li . ..可看到它们的 inode 号对应目录的 inode)。

红色框圈住的就是每个文件对应的引用计数,也就是硬链接的数量,对于目录文件demo1来说,由于在其目录下还有.目录指向自己,所以它的引用计数是2。因此我们还可以通过引用计数来计算一个目录下有几个目录:size = 当前目录的引用计数 - 2

6.2 软链接:存储路径的 "快捷方式"

软链接是 "独立的文件"------ 其 inode 与目标文件不同,数据块中存储的是 "目标文件的路径"(类似 Windows 的快捷方式)。

  • 创建命令:ln -s 目标文件 软链接名(如ln -s test.c test.soft);
  • 核心特点:
    1. 软链接的 inode 号与目标文件不同(ls -li显示lrwxrwxrwxl表示软链接);
    2. 目标文件删除后,软链接变成 "无效链接"(访问时提示 "没有那个文件或目录");
    3. 可跨分区(只需存储目标文件的绝对路径);
    4. 可链接目录(如ln -s /home/whb/doc /home/whb/link_doc)。

用途 :简化路径(如ln -s /usr/local/bin/python3 /usr/bin/python)、跨分区访问文件。

在windows中我们经常使用的快捷方式就是一种软连接,通过删除快捷方式并不会影响对应的文件。

6.3 软硬链接对比

特性 硬链接(Hard Link) 软链接(Symbolic Link)
inode 号 与目标文件相同 独立 inode(不同)
数据块内容 无独立数据块(复用目标文件) 存储目标文件路径
跨分区 不支持 支持(绝对路径)
链接目录 不支持 支持
目标文件删除后 仍能访问(链接数 > 0) 无效(断链)
本质 同一文件的别名 独立文件(指向目标路径)

七、总结:Ext 文件系统的设计思想

Ext 系列文件系统的成功,源于其清晰的分层设计和高效的资源管理:

  1. 分层管理:磁盘→分区→块组→块 /inode,从宏观到微观,既便于维护,又减少磁头移动;
  2. 分离存储:inode 存属性,数据块存内容,目录存 "文件名→inode" 映射,解耦不同职责;
  3. 高效复用:位图管理空闲资源(O (1) 查找),间接块支持大文件,硬链接复用 inode;
  4. 冗余备份:超级块、GDT 多块组备份,防止单点故障;
  5. 用户友好:通过路径、文件名隐藏底层复杂的 inode 和块地址,同时提供软硬链接灵活管理文件。

理解 Ext 文件系统,不仅能帮我们解决 "文件找不到"、"磁盘满了" 等实际问题,更能让我们看透 Linux "一切皆文件" 的设计哲学 ------ 无论是普通文件、目录,还是设备(如/dev/sda),本质都是通过 inode 和数据块管理,用统一的接口访问。

下次当你执行touch test.txt时,不妨想想:Linux 在底层创建了一个 inode,分配了数据块,在当前目录的数据块中添加了test.txt → inode号的记录 ------ 这就是文件 "住进" 磁盘的全过程。

尾声

本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

更多内容可见主页

相关推荐
HLC++11 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室13 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy15 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕16 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫17 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生17 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1119 小时前
linux基础IO
linux·运维·服务器
BelongPanda20 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯21 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983421 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器