Linux 文件系统底层探秘:磁盘物理结构→inode→Ext 架构全链路

本博客深度整合磁盘物理结构、CHS/LBA寻址、磁盘逻辑抽象(一维/二维/三维数组)、inode原理、Ext块组架构、路径解析、挂载机制及软硬链接核心知识,配合命令演示、C代码示例、对比表格与大量示意图,真正做到"知其所以然"。适合Linux后端、运维、嵌入式、内核入门者,也可直接作为面试复习纲要。


前言

在 Linux 世界里,一切皆文件

支撑"文件"正常工作的底层基石,就是 Ext 系列文件系统。绝大多数 Linux 发行版(Ubuntu、CentOS、Debian)默认使用 Ext4,它的前身是 Ext2、Ext3。

很多同学只会用 ls/cd/mkdir,却不懂:

  • 磁盘到底怎么存数据?
  • CHS 和 LBA 是什么关系?磁盘如何从三维变成一维数组?
  • inode 是什么?为什么删除文件还要看链接数?
  • 目录到底是不是文件?
  • 软链接和硬链接本质区别是什么?
  • 挂载到底在做什么?
  • 为什么有时候磁盘没满,却提示无法创建文件?

这篇文章一次性把所有原理讲透。


一、磁盘物理结构:存储的最底层真相

机械硬盘是计算机中唯一的机械设备,相比内存虽然慢,但容量大、价格便宜。

1.1 核心部件

  • 盘片(Platter):涂有磁性材料,双面均可存储数据。
  • 主轴(Spindle):带动盘片高速旋转。
  • 磁头(Head) :每个盘面配一个磁头,负责读写。所有磁头共进退
  • 磁臂(Actuator Arm):驱动所有磁头同步移动。
  • 音圈电机:精准控制磁臂定位。

1.2 磁盘的存储结构:扇区、磁道、柱面

每个盘面被划分为一个个同心圆环,称为磁道(Track) 。磁道再被等分为扇区(Sector)

  • 扇区 :磁盘读写的最小物理单元,固定 512 字节
  • 磁道 :某一盘面上的同心圆,所有盘面上相同半径的磁道在逻辑上构成一个柱面(Cylinder)
  • 柱面:分区的最小单位。

单个盘片的磁道扇区视图:

多个盘片组成的柱面视图:

1.3 如何定位一个扇区?------ CHS 寻址

早期采用三维坐标定位扇区:

  • C(Cylinder,柱面):先找哪一个柱面
  • H(Head,磁头):再确定柱面内哪一个磁头(即哪一个盘面/磁道)
  • S(Sector,扇区) :最后定位该磁道上的第几个扇区(注意:扇区号从 1 开始

CHS 寻址的上限约为 8.4GB(1024柱面 × 256磁头 × 63扇区 × 512字节)。


二、磁盘的逻辑结构:从三维到一维的抽象(重点)

2.1 理解思路:像磁带一样拉直

磁带可以存储数据,把磁带"拉直"就形成线性结构。磁盘虽然是硬质的,但逻辑上可以想象成卷在一起的磁带 ,拉直后,每个扇区就有了一个线性下标,这就是 LBA(Logical Block Address) 的由来。

2.2 真实过程:从三维数组到一维 LBA

磁盘的真实物理结构可以这样逐层理解:

① 磁道 → 一维数组

某一盘面的某一个磁道展开,就是一条扇区序列,就像一个一维数组,每个元素是一个 512 字节的扇区。

② 柱面 → 二维数组

由于磁头共进退,所有盘面上相同半径的磁道构成一个柱面。柱面上的每个磁道扇区个数相同,所以一个柱面展开后就是一张二维的扇区数组表(行 = 磁头/盘面,列 = 扇区)。

③ 整盘 → 三维数组(多个二维数组)

整个磁盘就是多个柱面(柱面0、柱面1、柱面2...)叠加,即多张二维扇区数组表,形成一个三维数组




第 n 个 柱 面...

④ 最终抽象 → 一维 LBA 数组

学过 C/C++ 数组就会知道,三维数组在内存中本质上仍是一维的。同理,磁盘最终被抽象为一个以扇区为元素的一维数组 ,每个扇区有一个唯一的下标,即 LBA 地址
图示:

核心结论:从此往后,操作系统只认 LBA,磁盘的固件负责将 LBA 自动转换为 CHS 去定位物理扇区。

2.3 LBA ↔ CHS 转换公式

python 复制代码
# CHS → LBA(扇区号 S 从 1 开始,LBA 从 0 开始)
LBA = C × (磁头数 × 每磁道扇区数) + H × 每磁道扇区数 + S - 1

# LBA → CHS
C = LBA // (磁头数 × 每磁道扇区数)
H = (LBA % (磁头数 × 每磁道扇区数)) // 每磁道扇区数
S = (LBA % 每磁道扇区数) + 1

重点:磁头数 × 每磁道扇区数 = 单个柱面的扇区总数,这是换算中的关键中间量。


三、从物理到逻辑:块、分区与文件系统

3.1 块(Block):文件系统的最小读写单位

逐个扇区读写效率太低,操作系统一次读写多个连续扇区,这个逻辑单位称为块(Block)

  • 常见大小:4KB(= 8 个扇区)
  • 格式化时确定,不可更改
  • 文件存取、空间分配均以块为单位
python 复制代码
# 块号与 LBA 的简单换算(以 4KB 块为例)
块号 = LBA // 8
LBA  = 块号 × 8 + 块内扇区偏移

3.2 分区(Partition)

磁盘可被划分为多个分区,柱面是分区的最小单位。分区本质上就是划定起始和结束柱面,每个区内的扇区分布就清晰了。

Linux 下磁盘设备文件示例:

  • /dev/sda:第一块 SCSI/SATA 磁盘
  • /dev/sda1:第一块磁盘的第一个分区

图示:

3.3 格式化 = 创建文件系统

所谓"格式化",就是向分区写入管理数据,构建文件系统。后续所有文件的增删查改都在这个框架下进行。


四、inode:文件的真正灵魂(核心重点)

4.1 核心思想

Linux 文件 = 元信息(inode)+ 数据内容(data block)

属性和内容分离存储。文件名不属于 inode!

每个文件有且只有一个 inode,inode 大小固定(通常 128 字节256 字节),任何文件的内容大小可以不同,但属性大小一定相同。

4.2 inode 里存了什么?

c 复制代码
struct ext2_inode {
    __le16  i_mode;         // 文件类型和权限
    __le16  i_uid;          // 所有者用户ID
    __le32  i_size;         // 文件大小(字节)
    __le32  i_atime;        // 最后访问时间 (Access)
    __le32  i_ctime;        // 属性变更时间 (Change)
    __le32  i_mtime;        // 内容修改时间 (Modify)
    __le32  i_dtime;        // 删除时间
    __le16  i_gid;          // 组ID
    __le16  i_links_count;  // 硬链接计数 ★★★
    __le32  i_blocks;       // 文件占用的块数
    __le32  i_flags;        // 文件标志
    __le32  i_block[15];    // 数据块指针数组 ★★★
};

4.3 查看 inode 信息

bash 复制代码
stat test.c      # 查看完整 inode 元信息
ls -li test.c    # 仅查看 inode 号

4.4 文件三个时间戳

时间 含义 触发条件
Access(atime) 最后访问时间 读文件、执行文件
Modify(mtime) 内容修改时间 写入文件内容
Change(ctime) 属性变更时间 权限、所有者变更,甚至 mtime 变化也会触发

五、多级索引:inode 如何一步步找到数据块

好的,我给你一个精简、带高亮、复制到CSDN直接能用的版本。


五、多级索引:inode 如何找到数据块

inode 里有个关键的数组叫 i_block[15],一共 15 个指针,分成两部分干活:

对照图片来看,就两条链路:

  • 上面那条 :12 个直接指针,直连数据块,路径最短,速度最快。
  • 下面那条 :3 个间接指针,一层层往下指,每一层都是装满指针的索引块,最终才指到数据块。

为什么要搞这么复杂?

一句话:小文件走捷径,大文件绕远路但能装得下。

大部分文件都不大,12 个直接块就够用,查找一次到位。偶尔遇到超大文件,再动用间接指针,牺牲一点速度,换来巨大容量。


能管多大?(以 4KB 块为例)

一个索引块能装 1024 个指针(4KB ÷ 4字节 = 1024),于是:

c 复制代码
// 直接块:12个指针,一次到位
直接块   = 12 × 4KB            = 48KB

// 一级间接:1个索引块 → 1024个数据块
一级间接 = 1024 × 4KB          = 4MB

// 二级间接:套两层索引
二级间接 = 1024 × 1024 × 4KB   = 4GB

// 三级间接:套三层索引
三级间接 = 1024 × 1024 × 1024 × 4KB = 4TB

汇总如下:

指针类型 原理 最大容量
直接块 (12个) 直连数据块 48 KB
一级间接 1层索引表 4 MB
二级间接 2层索引表 4 GB
三级间接 3层索引表 4 TB

六、Ext 文件系统架构:块组模型

6.1 整体布局

Ext2/3/4 将分区划分为多个等大小的块组(Block Group)

  • Boot Block:固定 1KB,存放启动代码和分区表,文件系统不能修改。

6.2 单个块组的 6 大组成

每个块组的内部结构如下:

  1. 超级块(Super Block)
    存放全局信息:块/inode 总量、大小、空闲量、挂载时间等。每个块组都有备份(第一个必须,后续可选),防止局部损坏。
  2. GDT(块组描述符表)
    记录本组内位图、inode 表、数据块的起始位置及空闲计数。同样有备份。
  3. 块位图(Block Bitmap)
    用位标记数据块占用情况(0空闲,1占用)。
  4. inode 位图(Inode Bitmap)
    标记 inode 占用情况。
  5. inode 表(Inode Table)
    该组所有 inode 的数组。
  6. 数据块(Data Blocks)
    存放文件真实内容或目录内容。

⚠️ 注意:inode 编号和 block 号均以分区为单位,不可跨分区!

6.3 超级块代码片段

c 复制代码
struct ext2_super_block {
    __le32  s_inodes_count;
    __le32  s_blocks_count;
    __le32  s_free_blocks_count;
    __le32  s_free_inodes_count;
    __le16  s_magic;             // 魔数 0xEF53
    // ... 更多字段
};

6.4 创建文件全过程(以 touch abc 为例)

当前目录下执行 touch abc,inode 号假设为 263466:

  1. 存储属性:找到一个空闲 inode(263466),写入文件类型、权限、时间戳等。
  2. 存储数据:(空文件暂不分配数据块)若有内容,找到空闲块写入。
  3. 记录分配:更新 inode 位图和块位图,在 inode 中记录块号列表。
  4. 添加目录项 :在当前目录的数据块中添加 (263466, "abc"),完成文件名与 inode 的绑定。

七、目录的本质:90% 的人理解错了

7.1 目录也是文件!

目录文件的数据块中存放的是"文件名 → inode号"的映射表。

磁盘上没有"目录"这个特殊概念,只有文件属性 + 文件内容。

7.2 C 代码验证

c 复制代码
// readdir_demo.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <directory>\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("名称: %-20s inode号: %lu\n", entry->d_name, entry->d_ino);
    }

    closedir(dir);
    return 0;
}
bash 复制代码
gcc readdir_demo.c -o readdir_demo
./readdir_demo /
ls -li /          # 对比 inode 号,完全一致

7.3 路径解析全过程

以访问 /home/user/test.c 为例:

  1. 从根目录 / 开始(inode 号固定,通常为 2,开机即知)。
  2. 读根目录的数据块,找到 home 对应的 inode。
  3. 跳转至 home 的 inode,读其数据块,找到 user 的 inode。
  4. 进入 user 目录,找到 test.c 的 inode,打开文件。

任何路径的解析都要从根目录逐级向下,路径的第一斜杠即根目录。

7.4 路径缓存(dentry)

每次都从根目录解析太慢。内核为每个打开的文件在内存中维护一个 dentry 结构,形成树形路径缓存。dentry 同时挂载在 LRU 链表 (用于淘汰)和哈希表(用于快速查找)中,极大加速重复访问。

c 复制代码
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 *    

八、挂载(Mount):多分区统一成一棵树

8.1 挂载的本质

分区格式化后不能直接使用,必须挂载到现有目录树中的某个空目录(挂载点)上。挂载后访问该目录即访问该分区。

8.2 动手实验:用文件模拟分区挂载

bash 复制代码
# 1. 创建 5MB 空镜像
dd if=/dev/zero of=./disk.img bs=1M count=5

# 2. 格式化为 Ext4
mkfs.ext4 disk.img

# 3. 创建挂载点
sudo mkdir -p /mnt/mydisk

# 4. 挂载
sudo mount -t ext4 ./disk.img /mnt/mydisk
df -h | grep mydisk

# 5. 卸载
sudo umount /mnt/mydisk

8.3 Loop 设备

/dev/loop0 ~ /dev/loop7回环设备,允许把普通文件当作块设备使用,实现像物理磁盘一样挂载 ISO、镜像文件。

bash 复制代码
ls -l /dev/loop*

九、文件系统核心原理全景总结

经过前面从物理磁盘到软硬链接的层层剖析,我们现在可以站在全局高度,把 Ext 文件系统的核心设计串成一条完整的逻辑链。

9.1 一张表看清核心概念演变

层次 核心概念 本质 关系
物理层 扇区(Sector) 磁盘最小物理读写单元,512B 物理基础
寻址层 LBA 扇区的一维线性地址 CHS→LBA,固件自动转换
逻辑层 块(Block) 文件系统最小读写单元,常见4KB 块 = 8个扇区,格式化时确定
管理层 块组(Block Group) 分区内多个等大的管理单元 每个块组独立管理所属 inode 和数据块
元数据层 inode 文件的"身份证",存属性和数据块指针 一个文件对应一个 inode,大小固定
数据层 Data Block 存放文件内容或目录内容 inode 中 i_block[15] 指向这些块
目录层 目录文件 存"文件名→inode号"映射 目录也是文件,数据块存映射表
缓存层 dentry 内存中的路径缓存结构 树形组织,挂 LRU+Hash,加速路径解析
虚拟层 挂载(Mount) 将分区根目录嫁接到目录树节点 实现多分区统一目录树

9.2 从磁盘到文件的全链路流程

第一步:物理寻址

  • 磁盘物理上由盘片、磁头、柱面、扇区组成。
  • 磁头共进退,所有盘面同一半径的磁道构成一个柱面。
  • 一个磁道展开为一维扇区数组一个柱面展开为二维数组(行=磁头/盘面,列=扇区)整个磁盘是多个柱面叠加的三维数组
  • 最终 OS 将所有扇区抽象为一维 LBA 线性地址数组,磁盘固件负责 LBA ↔ CHS 自动转换。

第二步:块与分区

  • OS 按"块"(8个扇区,4KB)为最小单位进行读写。
  • 磁盘被划分为多个分区,每个分区内部再划分为多个等大的块组。

第三步:文件系统格式化

  • 格式化 = 向分区写入管理信息:超级块、GDT、块位图、inode位图、inode表等。
  • 这些管理信息定义了如何在该分区内分配 inode 和数据块、如何找到文件。

第四步:文件创建与存储

  • 文件 = 元属性(inode) + 数据内容(Data Block),两者分离存储。
  • inode 固定大小(128/256B),存储权限、时间戳、硬链接计数、数据块指针数组 i_block[15]
  • 文件名不在 inode 中 ,而是存储在父目录的数据块里,形成"文件名→inode号"映射。

第五步:文件定位(路径解析)

  • 访问 /home/user/test.txt 时,从根 / 开始(inode固定),逐级读目录数据块,查找下一级 inode,直到目标文件。
  • 内核用 dentry 树形结构在内存中缓存已解析路径,挂入 LRU(淘汰冷数据)和 Hash 表(快速查找)。

第六步:分区整合(挂载)

  • 分区格式化后不能直接使用,必须 mount 到某个目录(挂载点)。
  • 挂载后,该目录即成为访问该分区的入口,多个分区由此统一为一棵目录树。

9.3 关键设计思想的总结

① 分层抽象

  • 物理层(扇区/磁道/柱面) → 寻址层(LBA) → 逻辑层(块) → 管理层(块组) → 元数据层(inode) → 缓存层(dentry) → 虚拟文件系统层(VFS)。
  • 每一层屏蔽下层复杂性,上层只需关心本层接口。

② 分离原则

  • 属性与内容分离:inode 存属性,Data Block 存内容。
  • 文件名与 inode 分离:文件名在目录中,inode 独立编号。
  • 这种分离让硬链接(多个文件名指向同一 inode)成为可能,也带来灵活性。

③ 局部性原理

  • 块组设计让 inode 和对应的数据块尽量靠近,减少磁头寻道。
  • 位图集中管理空闲资源,分配和回收效率高。

④ 索引灵活性

  • i_block[15] 中 12 直接块 + 1/2/3 级间接块的设计,同时兼顾小文件的高效与大文件的容量。

⑤ 缓存加速

  • dentry 路径缓存、inode 缓存、页缓存等多级缓存机制,大幅减少磁盘 I/O。

Ext 文件系统完整架构示意图:


十、硬链接与软链接:彻底弄懂

10.1 硬链接(Hard Link)

bash 复制代码
touch original.txt
ln original.txt hardlink.txt
ls -li original.txt hardlink.txt

输出示例:

复制代码
263466 -rw-r--r-- 2 user user 0 Jan 10 12:00 hardlink.txt
263466 -rw-r--r-- 2 user user 0 Jan 10 12:00 original.txt
  • 多个文件名指向同一个 inode,链接计数为 2。
  • 不占用新 inode,只是目录中多了一条映射记录。
  • 不能跨分区不能给目录创建... 由系统维护)。

10.2 软链接(Symbolic Link)

bash 复制代码
ln -s original.txt softlink.txt
ls -li original.txt softlink.txt
复制代码
263466 -rw-r--r-- 1 user user 0 ... original.txt
261680 lrwxrwxrwx 1 user user 12 ... softlink.txt -> original.txt
  • 相当于 Windows 快捷方式,独立文件,有自己的 inode 和数据块。
  • 数据块中存储的是目标文件路径(字符串)。
  • 可跨分区、可链接目录。
  • 源文件删除后,软链接变为"悬挂"状态(失效)。

10.3 对比总结表

特性 硬链接 软链接
inode 号 相同 不同
跨文件系统 不可以 可以
链接目录 不可以 可以
源文件删除后 仍然可用(计数>0) 失效(悬挂)
本质 多个目录项指向同一 inode 存放目标路径的独立文件

10.4 删除文件的真正逻辑

rm file 的执行流程:

  1. 在其父目录的数据块中删除 "file" → inode号 的记录。
  2. 对应的 inode 中 i_links_count 减 1。
  3. 若链接数降为 0 且无进程占用,则释放 inode 及所有数据块(在位图中标记为空闲)。

10.5 软硬链接的典型用途

  • 硬链接 :文件备份、... 的实现。
  • 软链接 :快捷方式、库版本管理(如 /usr/bin/python -> python3)。

十一、高频易错点与面试常考

  1. 扇区 ≠ 块:扇区 512B(物理),块常见 4KB(逻辑)。
  2. 文件名不在 inode 里:文件名存在目录的数据块中。
  3. 硬链接不能跨分区:inode 编号只在分区内唯一。
  4. 软链接是独立文件:有自己的 inode,存的是路径字符串。
  5. 访问文件需要路径上所有目录的 x 权限
  6. "磁盘满"可能有两种
    • 数据块满:df -h 查看。
    • inode 耗尽:df -i 查看(每个文件消耗一个 inode)。
  7. mv 在同一分区内极快:仅修改目录项,不动实际数据。跨分区则是复制+删除。

十二、常用命令速查表

命令 功能
fdisk -l 查看磁盘与分区
stat <file> 查看文件完整 inode 信息
ls -i / ls -li 查看 inode 号及详细信息
mkfs.ext4 /dev/sda1 格式化分区
mount /dev/sda1 /mnt 挂载分区
umount /mnt 卸载分区
ln file link 创建硬链接
ln -s file link 创建软链接
df -h 查看磁盘空间使用
df -i 查看 inode 使用情况
dumpe2fs /dev/sda1 查看文件系统详细信息

十三、总结

从扇区、磁道、柱面的三维物理结构,到拉直成一维 LBA 数组的逻辑抽象;从 Ext 的块组布局,到 inode 的多级索引;从目录本质的颠覆认知,到 dentry 路径缓存的加速机制;最后落地的挂载、软硬链接的实际使用------这篇博客带你从底层硬件到上层命令,全面吃透 Linux Ext 文件系统。

掌握这些,你将:

  • 真正理解"磁盘满"和"inode 耗尽"的区别
  • 能灵活运用硬链接、软链接管理文件
  • 清楚 mount、格式化、分区的底层到底做了什么
  • 在面试中对文件系统问题对答如流

如果本文对你有帮助,欢迎点赞、收藏、评论,你的支持是我持续输出的动力!

相关推荐
阿Y加油吧8 小时前
二刷 LeetCode:118. 杨辉三角 & 198. 打家劫舍 复盘笔记
笔记·算法·leetcode
70asunflower8 小时前
半导体产业的经济逻辑、技术瓶颈与AI芯片格局:一份学习笔记
人工智能·笔记·学习
minji...8 小时前
Linux 网络套接字编程(七)TCP服务端和客户端的实现——网络版本计算器
linux·运维·服务器·网络·c++·tcp/ip·udp
Misnice8 小时前
DevOps 介绍
运维·devops
liann1198 小时前
3.3_tasklist和netstat命令详解
运维·windows·计算机网络·安全·信息与通信
mounter6258 小时前
Linux Kernel Design Patterns (Part 2):从经典链表到现代 XArray,拆解内核复杂数据结构的设计哲学
linux·数据结构·链表·设计模式·内存管理·kernel
虚幻如影8 小时前
web端安全测试报告模板
linux·服务器·安全
凉、介8 小时前
C 语言类型强转引发的隐蔽内存破坏问题分析
c语言·开发语言·笔记·学习·嵌入式