2. 文件系统核心概念:inode、超级块、目录项、数据块、日志机制、挂载点

好,咱们正式开始聊文件系统的核心概念。说实话,我刚开始做存储那会儿,觉得文件系统就是个“存文件的东西”,直到有一次帮客户恢复一个被误格式化的数据库服务器,才真正意识到——不懂这些底层概念,数据恢复就是瞎蒙。

这一章,我会把六个核心概念掰开揉碎了讲。你想想看,文件系统说白了就是一套管理磁盘空间的“账本”。inode 是索引,超级块是总账,目录项是目录,数据块是仓库,日志是流水账,挂载点是入口。咱们一个一个来。

2.1 inode:文件的身份证

inode 这词儿,全称是 index node。我习惯叫它“索引节点”。每个文件都有一个唯一的 inode 编号,就像人的身份证号。

inode 里存了什么?不是文件名,而是文件的元数据。包括:

  • 文件类型(普通文件、目录、符号链接等)
  • 权限(rwx 那套)
  • 所有者、所属组
  • 文件大小
  • 时间戳(atime、mtime、ctime)
  • 数据块指针(指向实际数据存放的位置)

嗯,这里要注意:文件名并不在 inode 里。文件名存在目录项里。inode 和文件名是分开的。这个设计,说白了就是为了支持硬链接。

避坑指南:我曾经遇到一个案例,客户误删了重要文件,但文件还在被进程占用。这时候 inode 没有被释放,数据块也没被覆盖。我直接用 debugfs 工具从 inode 里把数据块指针读出来,硬是把文件恢复了。所以记住:文件删除 ≠ 数据消失,只要 inode 没被复用,就有戏。

来看一个 inode 的简化结构(以 ext4 为例):

struct ext4_inode {
    __le16  i_mode;        // 文件类型和权限
    __le16  i_uid;         // 用户ID
    __le32  i_size;        // 文件大小(低32位)
    __le32  i_atime;       // 访问时间
    __le32  i_ctime;       // 创建时间
    __le32  i_mtime;       // 修改时间
    __le32  i_dtime;       // 删除时间
    __le16  i_gid;         // 组ID
    __le16  i_links_count; // 硬链接计数
    __le32  i_block[15];   // 数据块指针数组
    // ... 其他字段省略
};

看到那个 i_block[15] 了吗?这 15 个指针就是关键。前 12 个是直接块指针,第 13 个是一级间接块指针,第 14 个是二级间接,第 15 个是三级间接。这种设计,说白了就是用空间换时间——小文件直接访问,大文件通过间接层索引。

2.2 超级块:文件系统的“户口本”

超级块(Superblock)是整个文件系统的元数据中的元数据。它记录了文件系统的全局信息,比如:

  • 文件系统类型(ext4、xfs、btrfs 等)
  • 块大小(通常是 4KB)
  • inode 总数和已用数量
  • 块总数和空闲块数量
  • 文件系统状态(是否干净、是否有错误)
  • 挂载次数、最后挂载时间

我个人的经验是:超级块损坏,基本等于文件系统“失忆”。没有超级块,你就不知道 inode 表在哪、数据块位图在哪,整个文件系统就成了一堆乱码。

警告:大多数文件系统会在多个位置备份超级块。比如 ext4 默认在块组 0、1、3、5、7... 等位置都有备份。我曾经用 mke2fs -n 命令查看备份超级块的位置,然后用 e2fsck -b 指定备份块来修复。这是数据恢复的基本功。

超级块的核心字段(简化版):

struct ext4_super_block {
    __le32  s_inodes_count;      // inode 总数
    __le32  s_blocks_count;      // 块总数
    __le32  s_r_blocks_count;    // 保留块数
    __le32  s_free_blocks_count; // 空闲块数
    __le32  s_free_inodes_count; // 空闲 inode 数
    __le32  s_first_data_block;  // 第一个数据块
    __le32  s_log_block_size;    // 块大小(以 1024 为底的对数)
    __le32  s_blocks_per_group;  // 每块组块数
    __le32  s_inodes_per_group;  // 每块组 inode 数
    __le16  s_magic;             // 魔数(0xEF53)
    // ... 其他字段
};

那个 s_magic 字段,值是 0xEF53。数据恢复时,我经常用这个魔数来扫描磁盘,定位超级块的位置。说白了,就是大海捞针时的那根“针”。

2.3 目录项:文件名到 inode 的桥梁

目录项(dentry,directory entry)把文件名和 inode 编号关联起来。每个目录其实就是一个特殊的文件,里面存着一堆目录项。

目录项的结构很简单:

struct ext4_dir_entry {
    __le32  inode;    // inode 编号
    __le16  rec_len;  // 目录项长度
    __u8    name_len; // 文件名长度
    __u8    file_type;// 文件类型
    char    name[];   // 文件名(变长)
};

你想想看,为什么删除文件很快?因为操作系统只是把目录项里的 inode 编号置为 0,或者标记为“已删除”。数据块根本没动。这就是为什么数据恢复软件能找回刚删除的文件——只要目录项还没被覆盖。

技巧:我在做数据恢复时,经常先扫描目录项。如果目录项还在,直接读出 inode 编号,然后去 inode 表里找数据块指针。如果目录项被覆盖了,那就得用“文件签名扫描”了——比如 PDF 文件开头是 %PDF,JPEG 开头是 FFD8FF。这招叫“基于内容的恢复”。

2.4 数据块:真正的“货架”

数据块(Data Block)是文件系统里存实际数据的地方。一个文件的数据可能分散在多个数据块里,这些块通过 inode 里的指针串联起来。

数据块的大小通常是 4KB,但也可以配置为 1KB、2KB 或 8KB。块越大,大文件性能越好,但小文件会浪费空间(内部碎片)。

文件系统用“块位图”(block bitmap)来管理哪些块是空闲的、哪些被占用了。位图里每一位对应一个数据块,1 表示已用,0 表示空闲。

我遇到过最头疼的情况是:块位图损坏。这时候文件系统不知道哪些块是空闲的,可能会把正在使用的块分配给新文件,导致数据覆盖。嗯,这就是为什么文件系统要有日志机制。

2.5 日志机制:文件系统的“后悔药”

日志(Journaling)机制,说白了就是先把要做的操作记下来,再真正执行。如果中途断电或崩溃,重启后根据日志恢复现场。

日志有三种模式:

模式 行为 安全性 性能
writeback 只记录元数据,数据先写 低 高
ordered 先写数据,再记录元数据 中 中
data 数据和元数据都记录 高 低

我个人习惯用 ordered 模式。为什么?writeback 虽然快,但数据可能不一致——文件内容写了一半,元数据却记录了完整大小,读出来就是乱码。data 模式太慢,写个文件要等日志刷完。ordered 模式折中,保证数据先落盘,元数据后记录,崩溃后至少数据是完整的。

避坑指南:我曾经遇到一个 ext3 文件系统,因为突然断电导致日志回放失败。系统启动时自动进入修复模式,但修复到一半卡住了。我手动用 fsck -fy 强制修复,结果把一些 inode 标记为“已删除”。后来我用 debugfs 的 logdump 命令,把日志里的未提交事务提取出来,手动恢复了部分数据。所以记住:日志不只是崩溃恢复用的,它也是数据恢复的宝藏。

2.6 挂载点:文件系统的“入口”

挂载点(Mount Point)是一个目录,把存储设备上的文件系统“接入”到系统的目录树中。比如你把 U 盘插上,系统自动挂载到 /media/usb,这个 /media/usb 就是挂载点。

挂载点本质上是一个“粘合点”。它把设备上的根目录(inode 2)和系统目录树上的某个节点关联起来。挂载后,你访问 /media/usb/foo.txt,系统就知道要去 U 盘的文件系统里找 inode 编号对应的数据块。

挂载时,内核会读取超级块,初始化文件系统的驱动,建立各种缓存(dentry cache、inode cache)。所以挂载慢,往往是因为超级块读取慢或者文件系统检查(fsck)耗时。

技巧:数据恢复时,我经常用 mount -o ro,noexec,loop 以只读方式挂载损坏的磁盘镜像。这样既能访问数据,又不会写入任何东西。记住:恢复数据的第一原则就是“只读操作”。任何写入都可能让数据彻底消失。

小结

这六个概念,是文件系统的基石。我画个简单的图帮你理解它们的关系:

超级块 指向 inode 表 指针 数据块 目录项 文件名→inode# 日志区域 记录操作 挂载点(/mnt) 读取 图例 超级块 inode 表 数据块 目录项 日志 挂载点

你看,超级块是总指挥,告诉系统 inode 表在哪、数据块在哪。目录项把文件名映射到 inode 编号。inode 通过指针找到数据块。日志记录所有操作,保证崩溃后能恢复。挂载点则是整个系统的入口。

下一章,咱们会深入微内核架构下的文件系统设计。你会发现,微内核把文件系统从内核里“踢”出去,变成了用户态服务。这带来了灵活性,但也给数据恢复带来了新的挑战。准备好了吗?