4. inode 分配与回收:inode 位图、inode 表、mkfs 时 inode 数量计算、动态 inode 分配机制

inode 是文件系统元数据的核心载体,其分配与回收机制直接决定了文件系统的创建效率、存储密度与故障恢复能力。本章深入剖析 inode 位图、inode 表的结构与操作,推导 mkfs 阶段 inode 数量的计算公式,并解析动态 inode 分配机制在 ext4 与 btrfs 中的实现差异。

4.1 inode 位图:分配状态的“比特地图”

inode 位图(inode bitmap)是文件系统用于跟踪每个 inode 分配状态的核心数据结构。每个比特位对应一个 inode:1 表示已分配,0 表示空闲。

关键指标: 位图大小 = 块组内 inode 总数 / 8(字节)。例如,一个块组包含 8192 个 inode,则位图占用 1024 字节(1 个块)。
inode 位图结构示意 inode 位图(1 块 = 1024 字节 = 8192 比特) 0 1 0 0 1 1 0 0 …… 共 8192 比特 空闲(0) 已分配(1) 分配流程: 1. 扫描位图,找到第一个比特为 0 的位置 2. 将该比特置 1,返回对应 inode 编号 3. 更新 inode 表,写入元数据
故障排查要点: 位图与 inode 表不一致是常见故障。例如,位图标记为 0(空闲)但 inode 表中仍有有效数据,会导致 inode 被覆盖分配,引发数据损坏。使用 fsck 的 -c 选项可强制检查位图一致性。

4.2 inode 表:元数据的结构化存储

inode 表(inode table)是连续存储 inode 结构体的区域。每个 inode 占用固定大小(ext4 默认 256 字节),包含文件属性、权限、时间戳、数据块指针等关键信息。

字段 偏移(字节) 大小(字节) 说明
i_mode 0 2 文件类型与权限位
i_uid 2 2 属主用户 ID
i_size 4 4 文件大小(低 32 位)
i_atime 8 4 最后访问时间
i_ctime 12 4 最后状态变更时间
i_mtime 16 4 最后修改时间
i_dtime 20 4 删除时间
i_block[15] 40 60 数据块指针数组(15 个 × 4 字节)
i_blocks 100 4 已分配块数(512 字节扇区单位)
常见损坏模式: inode 表中的 i_block 指针指向无效块号,或 i_size 与 i_blocks 不匹配,会导致文件读取时出现 I/O error 或 Structure needs cleaning。修复时需结合 debugfs 的 stat 命令逐字段检查。

4.3 mkfs 时 inode 数量计算

在格式化阶段,mkfs.ext4 根据文件系统大小与配置参数计算 inode 总数。核心公式如下:

inode_count = (blocks_count * bytes_per_block) / bytes_per_inode

其中:
- blocks_count = 文件系统总块数
- bytes_per_block = 块大小(默认 4096 字节)
- bytes_per_inode = 每 inode 字节比(默认 16384,即每 16KB 空间分配 1 个 inode)

实际计算示例: 一个 1TB 的文件系统,块大小 4096 字节,bytes_per_inode=16384:

blocks_count = 1TB / 4096 = 268,435,456 块
inode_count = (268,435,456 * 4096) / 16384 = 67,108,864 个 inode
文件系统大小 bytes_per_inode inode 总数 适用场景
100 GB 16384 6,553,600 通用服务器
100 GB 8192 13,107,200 邮件服务器(大量小文件)
100 GB 32768 3,276,800 视频存储(大文件为主)
调优建议: 使用 mkfs.ext4 -i <bytes_per_inode> 可自定义 inode 密度。对于预计存储大量小文件(如容器镜像层)的场景,建议将 -i 设为 4096 或 8192;对于大文件存储,可设为 65536 以节省 inode 表空间。

4.4 动态 inode 分配机制

传统 ext2/ext3 在 mkfs 阶段固定 inode 数量,无法在线扩展。ext4 引入了动态 inode 分配机制,允许在文件系统挂载后按需分配新的 inode 块。

动态 inode 分配流程(ext4) 用户创建文件请求 扫描 inode 位图 空闲 inode 充足? 是 分配位图 + 写入 inode 表 返回 inode 编号 否 触发动态 inode 扩展 分配新块组 → 初始化位图与 inode 表 更新超级块中的 inode 计数
动态分配触发条件: 当文件系统空闲 inode 数量低于 inode_ratio 阈值(默认 4%)时,ext4 会尝试分配新的块组。若磁盘空间不足,则返回 ENOSPC。使用 df -i 可监控 inode 使用率。

4.4.1 ext4 动态分配实现

ext4 通过 ext4_claim_inode() 函数实现动态分配,核心步骤:

  1. 位图扫描: 在当前块组中查找空闲 inode,若找到则直接分配。
  2. 跨组搜索: 若当前块组无空闲 inode,按 flex_bg 组策略扫描相邻块组。
  3. 新组创建: 若所有现有块组均满,调用 ext4_add_new_group() 分配新块组,初始化位图与 inode 表。
  4. 元数据同步: 更新超级块中的 s_inodes_count 与 s_free_inodes_count。
// 内核源码关键路径(简化)
struct inode *ext4_claim_inode(struct super_block *sb, ...)
{
    // 1. 尝试在当前块组分配
    ino = ext4_find_next_zero_bit(bitmap, group_size, 0);
    if (ino < group_size)
        goto found;

    // 2. 跨组搜索
    for (group = 0; group < ngroups; group++) {
        ino = ext4_find_next_zero_bit(...);
        if (found) goto found;
    }

    // 3. 动态扩展
    if (ext4_add_new_group(sb, ngroups) < 0)
        return ERR_PTR(-ENOSPC);

    // 4. 从新组分配
    ino = ext4_find_next_zero_bit(new_bitmap, ...);
found:
    ext4_set_bit(ino, bitmap);
    return iget_locked(sb, ino);
}

4.4.2 btrfs 动态分配对比

btrfs 采用完全不同的策略:无固定 inode 表,inode 元数据作为 B-tree 条目动态分配。每次创建文件时,在 fs tree 中插入新的 inode item,无需预分配位图。

特性 ext4 btrfs
inode 存储 固定 inode 表 + 位图 B-tree 动态条目
最大 inode 数 mkfs 时固定(可调) 仅受磁盘空间限制
分配延迟 位图扫描 O(n) B-tree 插入 O(log n)
碎片风险 位图连续分配,碎片低 B-tree 分裂可能导致元数据碎片
动态扩展 需分配新块组 直接扩展 B-tree
故障案例: ext4 文件系统 inode 耗尽时,即使磁盘仍有大量空闲空间,也无法创建新文件。此时 df -h 显示空间充足,但 df -i 显示 inode 使用率 100%。解决方案:使用 resize2fs 扩展文件系统以增加 inode 数量,或迁移至 btrfs/xfs 等动态 inode 文件系统。

4.5 回收机制:删除文件时的 inode 释放

删除文件时,内核执行以下回收步骤:

  1. 硬链接计数递减: i_nlink 减 1,若不为 0 则仅删除目录项。
  2. 数据块释放: 调用 truncate_inode_pages() 释放数据块,更新块位图。
  3. inode 位图清零: 将对应比特位从 1 置为 0。
  4. inode 表清理: 将 inode 结构体中的关键字段清零(如 i_mode、i_block)。
  5. 超级块更新: s_free_inodes_count 递增。
// 删除文件时的 inode 回收路径(ext4)
void ext4_evict_inode(struct inode *inode)
{
    // 1. 截断数据块
    truncate_inode_pages(inode->i_mapping, 0);

    // 2. 释放数据块
    ext4_free_blocks(inode, ...);

    // 3. 清除 inode 位图
    ext4_clear_bit(inode->i_ino, bitmap);

    // 4. 清零 inode 表
    memset(inode_table + offset, 0, sizeof(struct ext4_inode));

    // 5. 更新超级块
    percpu_counter_inc(&sbi->s_freeinodes_counter);
}
回收故障排查: 若删除文件后 inode 位图未清零,会导致 inode 泄漏(fsck 报告 Unattached inode)。使用 debugfs -R "stat <inode_number>" /dev/sdX 可检查 inode 的 i_dtime 字段是否被正确设置。

4.6 本章小结

  • inode 位图 是分配状态的一级索引,位图与 inode 表不一致是常见故障根源。
  • inode 表 存储完整的元数据结构,i_block 指针损坏会导致数据不可访问。
  • mkfs 阶段 通过 bytes_per_inode 计算 inode 总数,需根据文件大小分布合理调优。
  • 动态 inode 分配 在 ext4 中通过新块组扩展实现,btrfs 则完全动态化,无固定上限。
  • 回收机制 涉及位图清零、inode 表清理与超级块更新,任一环节失败都会导致元数据不一致。