【Linux】Ext 文件系统篇:从 inode 到路径解析,搞懂挂载与软硬链接

这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

这篇目录

二、文件 = 东西 + 属性,在磁盘上是分开存的三、inode:文件属性的真正载体 四、块组内部到底有哪些区域? 五、格式化到底做了什么? 六、如何根据 inode 找到文件东西? 七、文件名到底保存在哪里?八、路径解析:为什么访问文件必须有路径?九、路径缓存:dentry 是怎么来的?十、挂载分区:路径和分区怎么关联? 十一、用 readdir 查看目录里的文件名和 inode十二、文件系统整体总结图十三、软硬链接:文件名和 inode 的关系再拔高 十四、全文总结

前言

上一篇已经把磁盘硬件、CHS、LBA、扇区、块和分区讲完了。简单回顾一下:

回顾内容对应概念磁盘物理结构CHS磁盘逻辑结构LBA文件系统管理单位block文件系统载体分区

这一篇继续往下看:Ext 系列文件系统如何在一个分区里管理大量文件。

核心思路其实还是“先描述,再组织”:

  • 文件内容放在哪里?放在 Data Blocks。
  • 文件属性放在哪里?放在 inode Table。
  • 哪些数据块被占用了?看 Block Bitmap。
  • 哪些 inode 被占用了?看 Inode Bitmap。
  • 一个块组里这些区域从哪里动手?看 GDT。
  • 整个文件系统有多少块、多少 inode?看 Super Block。
把这些串起来,Linux 文件系统的结构就清楚了。

一、从分区到块组:Ext 为什么要分组管理?

一个分区可能很大,如果直接把整个分区当成一个大区域管理,效率会很低。

因此 Ext 文件系统会继续把一个分区划分成多个块组,也就是 Block Group。

这种思想可以理解成“分而治之”:

  • 磁盘
分区 Block Group 0
  • Block Group 1
  • Block Group 2
  • ...
每个块组内部都有自己的管理信息和数据区域。这样查找、分配、释放数据块时,不需要每次都扫描整个分区。

1.1 Ext2、Ext3、Ext4 的关系

Ext2 是 Ext 系列较早的版本,Ext3 在 Ext2 基础上加入日志机制,Ext4 又继续扩展了容量、分配方式和性能。虽然版本在变化,但“分区划分为块组、inode 保存属性、数据块保存内容、位图管理分配状态”这条核心思路是一脉相承的。

为了把结构看清楚,本文仍然核心使用 Ext2 的经典数据结构来讲解,涉及 Ext4 命令时再结合实际实验说明。

1.2 Boot Block:文件系统前面的保留区域

在经典 Ext2 布局中,分区起始位置会预留前 1024 字节作为启动相关区域,Ext2 的主超级块从偏移 1024 字节处动手。这个区域可用于保存引导代码或启动信息,不作为普通文件数据块使用。

可以先把宏观布局记成:

  • 分区起始位置
Boot Block / 保留区域
  • Ext 文件系统主体
Block Group 0
  • Block Group 1
  • Block Group 2
  • ...

二、文件 = 内容 + 属性,在磁盘上是分开存的

Linux 下文件可以看成:

核心认知: 文件 = 内容 + 属性

在 Ext 文件系统中,这两部分是分开保存的:

文件组成存放位置文件内容Data Blocks文件属性Inode Table

文件内容以块为单位保存,常见块大小是 4KB。

文件属性保存在 inode 中。inode 可以理解成描述文件属性的结构体,里面记录权限、大小、时间、拥有者、数据块索引等信息。

这里有一个很关键的点:

注意: 文件名不保存在 inode 中。

为什么?因为 inode 要尽量保持固定大小。权限、时间、大小这些字段类型固定,但文件名是字符串,有长有短。如果把文件名放进 inode,会导致 inode 大小不固定,不利于文件系统管理。


三、inode:文件属性的真正载体

平时执行:

ls -l

能看到文件权限、硬链接数、所有者、所属组、大小、修改时间、文件名等信息。

如果想看更详细的信息,可以使用:

stat test.c

stat 中可以看到 inode 号、Blocks、IO Block、Access、Modify、Change 等信息。

还可以使用:

ls -i

直接查看目录中文件对应的 inode 编号。

三个时间可以简单区分:

时间含义Access最后访问时间Modify文件内容最后修改时间Change文件属性最后修改时间


3.1 inode 为什么能让空文件占空间?

空文件大小可能显示为 0,但它仍然需要 inode 来保存属性信息。

也就是说,空文件可以没有真正的数据块,但不能没有属性。

因此创建一个 0KB 文件也不是“什么都没有”,它至少需要目录项和 inode 等元数据来描述。


3.2 inode 的核心结构

Ext2 中 inode 的结构算是长,下面保留和本文最相关的字段:

struct ext2_inode {
    __le16 i_mode;          /* 文件类型和权限 */
    __le16 i_uid;           /* 文件拥有者 uid */
    __le32 i_size;          /* 文件大小 */
    __le32 i_atime;         /* 最后访问时间 */
    __le32 i_ctime;         /* inode 状态改变时间 */
    __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]; /* 指向数据块的数组 */
};

#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)

EXT2_N_BLOCKS = 15,也就是说 inode 中有 15 个块指针位置。

其中最重要的是:

__le32 i_block[EXT2_N_BLOCKS];

这个数组保存的是文件内容所在的数据块编号。拿到 inode 后,就可以通过这个数组继续找到文件内容。

3.3 i_block[15] 并不是只能记录 15 个数据块

经典 Ext2 inode 中共有 15 个块指针位置:

i_block 下标作用i_block[0] ~ i_block[11]12 个直接块指针i_block[12]一级间接块i_block[13]二级间接块i_block[14]三级间接块

直接块指针直接记录数据块号,适合小文件,查找速度快。

一级间接块不直接保存文件内容,而是保存一整块“数据块号数组”;二级间接块先指向一级索引块;三级间接块再多一层。这样既能让小文件快速访问,也能让大文件扩展到不少数据块。

以 4KB 块、每个块号占 4 字节为例,一个间接索引块理论上可以保存:

一个 4KB 间接索引块可保存的块号数量:4096 / 4 = 1024

因此文件较小时只用直接块,文件增大后再逐步使用一级、二级和三级间接块。


四、块组内部到底有哪些区域?

一个 Ext 块组内部大致包含这些内容:

区域作用Super Block描述整个文件系统的全局信息GDT描述每个块组的管理信息Block Bitmap标记 Data Blocks 中哪些块被占用Inode Bitmap标记 Inode Table 中哪些 inode 被占用Inode Table保存当前块组中的 inode 属性集合Data Blocks保存文件真正的内容


4.1 Super Block:描述整个文件系统

Super Block 保存整个文件系统的全局信息,像是 inode 总数、块总数、空闲块数、空闲 inode 数、每组多少块、每组多少 inode、挂载时间、写入时间、文件系统状态等。

常见字段可以这样理解:

struct ext2_super_block {
    __le32 s_inodes_count;      /* inode 总数 */
    __le32 s_blocks_count;      /* 块总数 */
    __le32 s_free_blocks_count; /* 空闲块数量 */
    __le32 s_free_inodes_count; /* 空闲 inode 数量 */
    __le32 s_first_data_block;  /* 第一个数据块 */
    __le32 s_log_block_size;    /* 块大小 */
    __le32 s_blocks_per_group;  /* 每个块组有多少块 */
    __le32 s_inodes_per_group;  /* 每个块组有多少 inode */
    __le16 s_magic;             /* 文件系统魔数 */
    __le16 s_state;             /* 文件系统状态 */
    __u8   s_uuid[16];          /* 文件系统 uuid */
    char   s_volume_name[16];   /* 卷名 */
    char   s_last_mounted[64];  /* 上次挂载位置 */
};

Super Block 可以理解成文件系统的“总说明书”。操作系统挂载文件系统时,需要读它来确认这个分区是什么文件系统、块大小是多少、inode 和 block 怎么分布。

Super Block 非常重要,如果主副本损坏,文件系统可能无法正常识别。经典 Ext 文件系统会在部分块组中保存 Super Block 和相关描述信息的备份副本,以提高容错能力。并不是每个现代块组都必须保存完整副本,具体分布会受 sparse_super 等特性影响,但“关键元数据需要备份”这个设计目的不变。


4.2 GDT:块组描述符表

GDT 全称是 Group Descriptor Table,块组描述符表。

它描述每个块组内部的布局,像是:

  • Block Bitmap 在哪里;
  • Inode Bitmap 在哪里;
  • Inode Table 从哪里动手;
  • 当前组还有多少空闲块;
  • 当前组还有多少空闲 inode;
  • 当前组有多少目录。
对应结构体如下:

struct ext2_group_desc {
    __le32 bg_block_bitmap;      /* Block Bitmap 所在块号 */
    __le32 bg_inode_bitmap;      /* Inode Bitmap 所在块号 */
    __le32 bg_inode_table;       /* Inode Table 起始块号 */
    __le16 bg_free_blocks_count; /* 空闲数据块数量 */
    __le16 bg_free_inodes_count; /* 空闲 inode 数量 */
    __le16 bg_used_dirs_count;   /* 已使用目录数量 */
    __le16 bg_pad;
    __le32 bg_reserved[3];
};

GDT 可以理解成每个块组的“目录表”。没有它,操作系统就不知道该去哪里找位图、inode 表和数据块。

主 GDT 保存全部块组的描述符。为了配合超级块容错,部分块组中还可能保存 GDT 的备份。学习时可以先理解成:Super Block 管整个文件系统,GDT 管每个块组的布局。


4.3 Block Bitmap:管理数据块是否被使用

Data Blocks 中有大量 4KB 数据块,文件系统必须知道哪些块被占用,哪些块还空闲。

这就需要 Block Bitmap。

位图值数据块状态bit = 1对应数据块已经被占用bit = 0对应数据块空闲

如果一个块组里有 100000 个数据块,那么 Block Bitmap 里就可以用 100000 个 bit 来表示这些块的使用情况。

这也是为什么删除大文件很快。

像是删除一个 3GB 电影文件时,系统一般不会把 3GB 数据内容一个字节一个字节清空,而是核心修改 inode 和 Block Bitmap 等管理信息:

  • 目录项删除;
  • inode 释放;
  • 数据块对应 bit 清 0。
数据内容可能还在磁盘上,只是文件系统已经把相关 inode 和数据块标记为可重新分配。于是误删文件有时存在恢复可能,前提是目录项、inode 线索和数据块尚未被新数据覆盖。

恢复也不是简单把某几个位图位重新置为 1。真正恢复时,还需要重新确定原来的目录项、inode、数据块顺序和文件大小等关系。误删后继续向该分区写入数据,会显著降低恢复成功率。


4.4 Inode Bitmap:管理 inode 是否被使用

inode 也需要管理空闲状态,所以有 Inode Bitmap。

位图值inode 状态bit = 1对应 inode 已经被使用bit = 0对应 inode 空闲

创建新文件时,文件系统要从 Inode Bitmap 中找一个空闲 inode,再在 Inode Table 中填充属性信息。


4.5 Inode Table:保存文件属性集合

Inode Table 保存当前块组中的 inode。

如果 inode 大小是 128 字节,一个 block 是 4KB,那么一个 4KB 块可以保存:

一个 4KB 块可容纳的 inode 数量:4096 / 128 = 32

所以 inode table 本身也是由多个 4KB block 组成的。

这也能解释为什么 inode table 的读取也会利用局部性:一次读入一个块,就可能同时读入附近多个 inode。


4.6 Data Blocks:保存文件内容

Data Blocks 是真正保存文件内容的地方。

不同文件类型对应的数据内容也不同:

文件类型Data Blocks 中保存什么普通文件文件真实内容目录文件文件名和 inode 号的映射关系软链接目标文件路径字符串

这句话很重要:

核心结论: 目录也是文件,目录的数据块中保存的是“文件名 → inode”的映射关系。

五、格式化到底做了什么?

平时说“格式化分区”,不是简单把磁盘清空。

对于文件系统来说,格式化的本质是:

格式化的本质: 在分区中写入文件系统的管理信息。

比如:

  • 写入 Super Block;
  • 写入 GDT;
  • 初始化 Block Bitmap;
  • 初始化 Inode Bitmap;
  • 准备 Inode Table;
  • 划分 Data Blocks;
  • 标记启动区域、超级块、位图、inode 表和根目录等已经被占用的资源。
在使用分区前,文件系统需要先把这些管理信息准备好,操作系统才能知道这个分区应该怎么管理。

文件系统被挂载后,内核会把需要的 Super Block、块组描述符、inode 和目录项等元数据加载或缓存到内存中。日常创建、删除、查询和修改文件,通常先修改内存中的管理状态,再按照文件系统策略写回磁盘。

还要注意:

注意: inode 编号和 block 编号只在同一个文件系统内部具有唯一意义。不同文件系统可以出现相同 inode 号,于是 inode 号必须结合所属文件系统理解。

不同分区可以各自有自己的 inode 编号体系,所以“inode 号”必须结合“在哪个分区”一起看才有意义。

5.1 根据 inode 号计算块组和组内下标

假设每个块组有 8192 个 inode,目标 inode 号是 20000。由于 inode 编号通常从 1 开始,可以计算:

定位结果:
块组号 = (20000 - 1) / 8192 = 2组内下标 = (20000 - 1) % 8192 = 3615

所以 inode 20000 位于第 2 号块组,在该组 inode 表中的下标是 3615。随后再结合 GDT 中的 bg_inode_table 起始块号,就能定位到具体 inode 所在的磁盘块和块内偏移。


六、如何根据 inode 找到文件内容?

假设现在已经拿到了某个文件的 inode 号。

文件系统大致会这样做:

1. 根据 inode 号算出它属于哪个块组;
2. 根据组内偏移找到 Inode Bitmap 对应 bit,确认该 inode 是否有效;
3. 到 Inode Table 中读取 inode 属性;
4. 根据 inode 中的 i_block[] 找到文件内容所在的数据块;
5. 读取 Data Blocks 得到文件内容。

inode 和数据块可以跨块组编号,但不能跨分区。也就是说:

同一个分区内部,inode 编号和 block 编号是统一编号的。

6.1 创建一个新文件时,文件系统做了什么?

以 touch abc 为例,可以把创建过程简化成四步:

1. 分配 inode:从 Inode Bitmap 中找到一个空闲位,在 Inode Table 中初始化权限、时间、链接数等属性;
2. 分配数据块:如果文件需要保存内容,就从 Block Bitmap 中寻找空闲块;空文件暂时可以没有普通数据块,但目录中仍要增加目录项;
3. 记录数据块映射:把分配到的数据块号记录进 inode 的 i_block[] 或相关索引结构;
4. 写入目录项:在父目录的数据块中加入一条“abc → inode 号”的映射。

所以创建文件并不是只写一份内容,而是同时更新 inode、位图、目录项和可能的数据块。


七、文件名到底保存在哪里?

前面说过,文件名不保存在 inode 中。

那文件名到底在哪里?

答案是:

文件名的位置: 文件名保存在它所在目录的数据块中。

目录也是文件,也有自己的 inode 和 Data Blocks。

目录的属性放在自己的 inode 中,目录的内容放在自己的数据块中。目录数据块中保存的是一组组映射关系:

目录项保存的核心映射: 文件名 → inode 号

所以大家平时使用文件名访问文件,其实系统底层要先通过目录内容找到文件名对应的 inode,再通过 inode 找到文件属性和内容。

简单流程如下:

1. 提交路径和文件名;
2. 逐级解析目录;
3. 找到“文件名 → inode”的映射;
4. 读取 inode;
5. 根据 inode 定位并读取 Data Blocks。

这也解释了为什么同一个目录下不能有两个完全相同的文件名:因为目录数据块里不能有重复的同名映射。


八、路径解析:为什么访问文件必须有路径?

假设访问:

ls code.c

看起来只给了文件名,没有路径。

其实不是没有路径,而是使用了进程当前工作目录 cwd。

文件名是用户提供的,路径可以由进程提供。进程的 PCB 中会维护当前工作目录,所以 ls code.c 可以理解成:

当前工作目录 + code.c

绝对路径从进程的根目录视图开始解析,相对路径从当前工作目录开始解析。内核会在进程的文件系统上下文中记录这些路径对象,于是用户只写 code.c 时,系统仍然知道应该从哪里开始查找。

如果是绝对路径,比如:

/home/user/test/code.c

内核就要从根目录 / 开始,一层一层解析:

/ → home → user → test → code.c

每一级目录都要读取目录文件的数据块,找到下一级名字对应的 inode。

解析路径时,内核只需要读取目录和 inode 等元数据到内核缓存,不需要把目录内容拷贝到用户进程内存。如果对应元数据不在缓存中,就要走磁盘 I/O;如果已经在缓存中,就可以直接在内存里完成查找。


九、路径缓存:dentry 是怎么来的?

如果每次访问文件都从 / 开始读取磁盘,那效率太低了。

所以 Linux 会把访问过的路径结构缓存在内存中,形成一棵树状结构。这个路径缓存相关的核心结构体叫:

struct dentry

dentry 可以理解成目录项缓存节点,它把“名字、父目录、inode、子节点”等信息组织起来。

核心结构如下:

struct dentry {
    atomic_t d_count;              /* 引用计数 */
    unsigned int d_flags;
    spinlock_t d_lock;
    struct inode *d_inode;         /* 对应 inode */
    struct hlist_node d_hash;      /* 用于 hash 查找 */
    struct dentry *d_parent;       /* 父目录 */
    struct qstr d_name;            /* 当前名字 */
    struct list_head d_lru;        /* LRU 淘汰链表 */
    struct list_head d_subdirs;    /* 子目录/子节点 */
    struct list_head d_alias;      /* inode 别名链表 */
    struct dentry_operations *d_op;
    struct super_block *d_sb;      /* 所属超级块 */
    int d_mounted;
};

dentry 有几个重要特点:

  • 每个文件都可以有对应的 dentry,包括普通文件;
  • dentry 组成内存中的路径树;
  • dentry 还会加入 LRU,方便淘汰长期不用的路径节点;
  • dentry 也会加入 hash,方便快速查找;
  • 路径缓存可以避免频繁从磁盘重新解析路径。

磁盘上确实保存目录文件及其目录项,但不会额外保存一棵直接可遍历的“内存指针树”。Linux 在路径解析过程中,通过 dentry 等结构把访问过的目录项组织成内存中的路径缓存树。


十、挂载分区:路径和分区怎么关联?

前面说 inode 号不能跨分区,而 Linux 又可以有多个分区。

那访问一个路径时,系统怎么知道这个文件属于哪个分区?

答案是挂载。

挂载的本质: 把一个分区和某个目录关联起来。

进入这个目录,就相当于进入这个分区对应的文件系统。

比如根分区挂载到 /,那么系统启动后,大家访问 /home、/etc、/usr 等路径时,首先都是在根分区的文件系统中解析。


10.1 用镜像文件模拟一个分区

可以用下面的命令制作一个 5MB 的磁盘镜像文件,然后格式化为 ext4,再挂载到目录上。

# 制作一个 5MB 的空白镜像文件,相当于模拟一个小分区
dd if=/dev/zero of=./disk.img bs=1M count=5

# 将镜像文件格式化为 ext4 文件系统
mkfs.ext4 disk.img

# 创建挂载目录
mkdir /mnt/mydisk

# 查看当前磁盘挂载情况
df -h

# 使用 loop 设备把镜像挂载到指定目录
sudo mount -o loop -t ext4 ./disk.img /mnt/mydisk

# 再次查看挂载结果
df -h

# 卸载挂载点
sudo umount /mnt/mydisk

# 卸载后再次查看
df -h

dd if=/dev/zero 表示从零设备读取全 0 数据,写出一个固定大小的镜像文件。

/dev/loop0 这类设备叫循环设备(loop device)。它是一种伪块设备,可以把普通文件映射成块设备,因此 ISO 镜像、磁盘镜像才可以像真实分区一样被格式化和挂载。

卸载前最好先退出挂载目录,并确保没有进程正在使用其中的文件,否则 umount 可能提示设备忙。

mkfs.ext4 disk.img 表示给这个镜像写入 ext4 文件系统的管理信息,也就是格式化。

mount -o loop -t ext4 ./disk.img /mnt/mydisk 表示通过 loop 设备把普通镜像文件当成块设备,再挂载到 /mnt/mydisk 目录。

挂载后,路径解析会按照路径前缀选择最具体、匹配最长的挂载点。比如文件位于 /mnt/mydisk/a.txt,解析到 /mnt/mydisk 时就会切换到该挂载点对应的文件系统中继续查找。


10.2 fopen 背后的路径解析和打开流程

当调用:

fopen("/mnt/mydisk/a.txt", "r");

内核大致会做这些事情:

1. 根据路径前缀判断属于哪个挂载点;
2. 在对应分区的文件系统中做路径解析;
3. 通过目录数据块找到文件名和 inode 的映射;
4. 根据 inode 找到文件属性和数据块;
5. 在内核中创建 struct inode、struct file 等结构;
6. 将打开文件对象挂到进程文件描述符表中;
7. 返回文件描述符给用户层,C 库再封装成 FILE*。

所以从用户层看只是 fopen 一行,底层其实串起了路径解析、挂载点选择、inode 查找、文件对象创建等过程。


十一、用 readdir 查看目录里的文件名和 inode

目录的数据块里保存的是文件名和 inode 映射。可以通过 opendir/readdir 简单验证。

#include
#include
#include
#include
#include
#include

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s \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("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
    }

    closedir(dir);
    return 0;
}

readdir 读到的就是目录项信息,其中 d_name 是文件名,d_ino 是 inode 号。

这也再次证明:

目录的内容不是普通文本,而是一组文件名和 inode 的映射关系。

十二、文件系统整体总结图

Ext 文件系统可以粗略串成这样:

  • 分区
Block Group Super Block
  • GDT
  • Block Bitmap
  • Inode Bitmap
  • Inode Table
  • Data Blocks

文件查找流程可以再压缩成一句话:

文件查找主线: 路径 → 目录项 → inode → Data Blocks。

十三、软硬链接:文件名和 inode 的关系再拔高

Linux 中有两类链接文件:

  • 软链接;
  • 硬链接。
理解软硬链接,本质就是继续理解“文件名、inode、数据块”的关系。

13.1 软链接:一个独立文件,内容是目标路径

创建软链接:

ln -s code.c code-soft
echo "hello code.c" >> code.c
cat code-soft

通过软链接访问时,能看到目标文件的内容。

但软链接本身是一个独立文件,因为它有自己的 inode。

软链接可以理解成 Windows 中的快捷方式。它自己的内容保存的是目标文件路径。

删除软链接不会删除目标文件,因为软链接和目标文件是两个 inode。

常见删除方式:

rm code-soft
unlink code-soft

软链接文件的类型一般以 l 开头,比如:

软链接常见权限显示形式:lrwxrwxrwx

13.2 硬链接:新增一个文件名和同一个 inode 的映射

创建硬链接:

ln myfile.c myfile-hard

硬链接和原文件的 inode 号相同,说明它们不是两个独立文件。

硬链接的本质是:

新增一组“文件名 → inode”的映射关系。

多个文件名可以指向同一个 inode。

inode 中有一个字段记录硬链接数,比如:

i_links_count

每多一个文件名指向该 inode,硬链接数就加 1。

删除文件时,系统并不是马上把数据块清空,而是:

1. 删除目录中的文件名映射;
2. inode 的硬链接数减 1;
3. 当硬链接数减到 0,并且内核中也没有进程继续持有该文件的打开引用时,文件系统才最终回收 inode 和数据块。


13.3 硬链接的用途:增加访问入口,防止单个文件名误删

硬链接可以为同一份文件数据提供多个文件名入口:

touch code.c
ln code.c code.c.bak
rm code.c
cat code.c.bak

即使 code.c 这个名字被删除,只要 code.c.bak 仍然指向同一个 inode,文件内容就还能访问。

这种用法可以防止“误删某一个文件名”,但不能替代真正备份:

  • 两个名字看到的是同一份数据,修改一处,另一处也会变化;
  • 如果数据块损坏,所有硬链接都会一起受影响;
  • 硬链接通常不能跨文件系统,因为 inode 号只在所属文件系统内有效;
  • 普通用户通常不能随意给目录创建硬链接,避免目录树形成环。
真正备份应该把数据复制到独立介质、独立文件系统或远端存储中。

13.4 目录中的 . 和 … 也是硬链接

创建目录:

mkdir dir
touch code.c
ll

普通文件 code.c 的硬链接数通常是 1,但目录 dir 可能显示为 2。

进入目录查看隐藏项:

cd dir
ls -ali

. 表示当前目录,它也是一个文件名到当前目录 inode 的映射关系,所以目录刚创建时链接数通常是 2:

  • dir 这个名字 → dir 的 inode
  • . 这个名字 → dir 的 inode
如果在 dir 下再创建子目录:

mkdir hello

然后回到上级目录查看:

父目录链接数会增加,因为子目录 hello 里的 .. 指向父目录 dir 的 inode。

所以:

常见目录链接数: 目录链接数 = 2 + 该目录下直接子目录数量

普通文件不会增加父目录链接数。普通文件创建时,只是在父目录数据块中增加一组“文件名 → 文件 inode”的映射关系,不会让父目录 inode 的 i_links_count 增加。


13.5 软链接和硬链接对比

对比项软链接硬链接是否独立目录项是是,新增一个目录项是否有独立 inode有自己的 inode与目标文件使用同一个 inode保存内容目标路径字符串不保存目标路径,目录项直接记录同一个 inode 号能否跨文件系统可以通常不可以能否指向目录可以普通用户通常不允许创建目录硬链接删除原文件名后可能变成失效链接只要链接数不为 0,数据仍可访问常见用途快捷方式、跨文件系统引用多个名字访问同一文件、防止单个名字误删

PDF 中的核心结论是:软链接本身是独立文件,硬链接则是新增“文件名 → 同一 inode”的映射;. 和 .. 也体现了目录项与 inode 的链接关系。


十四、全文总结

这篇这篇核心讲 Ext 文件系统的核心结构,以及路径、挂载、软硬链接背后的底层逻辑。

可以用一条线把知识串起来:

1. 分区被划分为多个 Block Group;
2. 块组中包含 Super Block、GDT、位图、Inode Table 和 Data Blocks;
3. 目录项保存“文件名 → inode”的映射;
4. inode 保存文件属性和数据块编号;
5. Data Blocks 保存文件真正的内容。

最重要的结论有这些:

1. Ext 文件系统会把分区划分成多个块组,这是分而治之的管理思想。
2. 文件内容保存在 Data Blocks,文件属性保存在 inode 中。
3. 文件名不在 inode 中,而是在目录文件的数据块中。
4. Block Bitmap 管理数据块是否被占用,Inode Bitmap 管理 inode 是否被占用。
5. Super Block 描述整个文件系统,GDT 描述每个块组。
6. 格式化不是简单清空磁盘,而是写入文件系统管理信息。
7. 删除文件快,是因为主要更新目录项、链接计数、inode 和位图,通常不会立刻逐字节擦除真实数据。
8. 路径解析要从目录中逐级找到文件名和 inode 的映射。
9. dentry 是内核中的路径缓存结构,可以减少重复路径解析带来的磁盘 I/O。
10. 挂载就是把某个分区和目录关联起来,路径解析时通过挂载点确定文件属于哪个文件系统。
11. 软链接是保存目标路径的独立文件;硬链接是新增一个指向同一 inode 的目录项。
12. . 和 .. 可以帮助理解目录硬链接数为什么会变化。

抓住一句话就够了:

Ext 文件系统不是直接按文件名找文件,而是通过目录项找到 inode,再通过 inode 找到数据块。

理解了这个过程,inode、块组、位图、路径解析、挂载、软硬链接就能真正连起来了。

资源分享:
【Linux】Ext 文件系统:从机械磁盘到 CHS/LBA,搞懂扇区、块与分区

【Linux】文件描述符背后的底层逻辑:重定向、VFS 与用户态缓冲区

【Linux基础IO】printf 为什么会写进文件?一文搞懂重定向和 dup2 底层原理


以上就是这次整理的全部内容,希望对你有所启发。如果有不同见解,欢迎在评论区交流讨论。

评论 (0)

暂无评论