4. 索引节点(inode)深度解析

索引节点(inode)是 VFS 中用于描述文件或目录元数据的核心数据结构。每个文件、目录、符号链接等在 VFS 层都对应一个唯一的 inode。inode 不包含文件名(文件名存储在目录项 dentry 中),但包含了除文件名之外的所有文件属性:权限、大小、时间戳、数据块指针等。本章深入剖析 inode 的结构体定义、关键字段、操作函数表以及分配与回收机制。

4.1 inode 结构体定义与关键字段

在 Linux 内核源码 include/linux/fs.h 中,struct inode 是 VFS 层最核心的数据结构之一。以下列出其关键字段,按功能分组说明:

字段分组 字段名 类型 说明
标识与状态 i_ino unsigned long inode 号,文件系统内唯一
i_count atomic_t 引用计数,决定 inode 何时可被回收
i_nlink unsigned int 硬链接计数,为 0 时文件数据可被删除
i_state unsigned long inode 状态标志(I_DIRTY、I_LOCK、I_FREEING 等)
文件属性 i_mode umode_t 文件类型与权限位(S_IFREG、S_IFDIR、rwx 等)
i_uid / i_gid kuid_t / kgid_t 文件所有者 UID 和 GID
i_size loff_t 文件大小(字节),目录则代表目录项总大小
i_atime / i_mtime / i_ctime struct timespec64 访问时间、修改时间、状态变更时间
数据块映射 i_blocks blkcnt_t 文件占用的磁盘块数(512 字节为单位)
i_blkbits unsigned int 块大小的位数(如 12 表示 4KB 块)
关联结构 i_sb struct super_block * 指向所属超级块,标识文件系统
i_mapping struct address_space * 页缓存映射,管理文件数据页
i_dentry struct list_head 关联的 dentry 链表(可能有多个硬链接)
操作函数表 i_op const struct inode_operations * inode 操作函数指针表
i_fop const struct file_operations * 文件操作函数指针表(默认从 inode 继承)
文件系统私有 i_private void * 具体文件系统私有数据指针
关键理解: i_count 和 i_nlink 是 inode 生命周期的两个独立计数器。i_count 表示内核中引用该 inode 的对象数量(如打开的文件描述符、dentry 引用),i_nlink 表示目录树中指向该 inode 的硬链接数量。只有当 i_count == 0 且 i_nlink == 0 时,inode 才会被真正释放。

4.2 inode 操作函数表 (inode_operations)

struct inode_operations 定义了在 inode 级别执行的操作,主要涉及目录和文件元数据的管理。与 file_operations 不同,inode_operations 不处理读写数据,而是处理创建、删除、链接、权限检查等元数据操作。

// include/linux/fs.h (简化核心成员)
struct inode_operations {
    // 目录项查找(仅目录 inode 有效)
    struct dentry * (*lookup) (struct inode *dir, struct dentry *dentry,
                               unsigned int flags);

    // 创建新文件(目录 inode 上创建普通文件)
    int (*create) (struct inode *dir, struct dentry *dentry,
                   umode_t mode, bool excl);

    // 创建硬链接
    int (*link) (struct dentry *old_dentry, struct inode *dir,
                 struct dentry *new_dentry);

    // 删除硬链接
    int (*unlink) (struct inode *dir, struct dentry *dentry);

    // 创建符号链接
    int (*symlink) (struct inode *dir, struct dentry *dentry,
                    const char *symname);

    // 创建目录
    int (*mkdir) (struct inode *dir, struct dentry *dentry, umode_t mode);

    // 删除目录
    int (*rmdir) (struct inode *dir, struct dentry *dentry);

    // 重命名(移动)
    int (*rename) (struct inode *old_dir, struct dentry *old_dentry,
                   struct inode *new_dir, struct dentry *new_dentry,
                   unsigned int flags);

    // 权限检查(可被 LSMs 覆盖)
    int (*permission) (struct inode *inode, int mask);

    // 获取文件属性(填充 struct kstat)
    int (*getattr) (struct user_namespace *mnt_userns,
                    const struct path *path, struct kstat *stat,
                    u32 request_mask, unsigned int flags);

    // 设置文件属性
    int (*setattr) (struct user_namespace *mnt_userns,
                    struct dentry *dentry, struct iattr *attr);

    // 获取扩展属性
    ssize_t (*getxattr) (struct dentry *dentry, const char *name,
                         void *buffer, size_t size);

    // 设置扩展属性
    int (*setxattr) (struct user_namespace *mnt_userns,
                     struct dentry *dentry, const char *name,
                     const void *value, size_t size, int flags);

    // 返回 inode 对应的文件操作表(用于特殊文件如设备)
    struct file_operations * (*get_inode_ops) (struct inode *inode);
};
适配要点: 编写文件系统驱动时,必须根据文件系统特性实现 inode_operations 中的必要回调。例如,一个只读文件系统可以将 create、mkdir、unlink 等写操作设为 NULL 或返回 -EROFS。对于普通文件 inode,lookup 通常为 NULL;对于目录 inode,lookup 是必须实现的。

4.3 inode 的分配与回收流程

inode 的分配与回收涉及 VFS 层与具体文件系统层的协作。VFS 提供通用框架,具体文件系统负责底层存储的分配与释放。

4.3.1 inode 分配流程

  1. VFS 调用 alloc_inode(sb):通过超级块中的 s_op->alloc_inode 回调,请求具体文件系统分配一个 inode 对象。
  2. 文件系统分配内存:具体文件系统(如 ext4)从 slab 缓存或 kmem_cache 中分配 struct inode 或其扩展结构(如 ext4_inode_info)。
  3. VFS 初始化通用字段:inode_init_always() 设置 i_ino、i_sb、i_mapping、i_op、i_fop 等通用字段,初始化 spinlock、链表头、原子计数等。
  4. 文件系统填充特定字段:文件系统通过 s_op->read_inode(旧接口)或 inode->i_op->getattr 从磁盘读取元数据填充 inode。
  5. 插入 inode 哈希表:VFS 将新分配的 inode 插入全局 inode 哈希表 inode_hashtable,以便快速查找。
  6. 返回 dentry 关联:inode 与 dentry 关联后,通过 d_instantiate() 建立双向链接。

4.3.2 inode 回收流程

  1. 引用计数归零:当所有持有该 inode 引用的对象(dentry、file 等)释放后,i_count 降为 0。
  2. VFS 调用 destroy_inode(inode):通过 s_op->destroy_inode 回调通知文件系统。
  3. 文件系统清理私有数据:释放文件系统私有扩展结构中的资源(如 ext4 的 extent 缓存、日志句柄等)。
  4. VFS 清理通用资源:释放 i_mapping 中的页缓存(如果还有脏页则写回),从哈希表删除 inode。
  5. 归还 slab 缓存:文件系统将 inode 内存归还给 slab 分配器,或直接 kfree()。
注意: inode 回收并不等同于磁盘上 inode 的删除。磁盘 inode 的释放由 i_nlink 控制:当 i_nlink 降为 0 时,文件系统会在适当时候(如最后一个文件描述符关闭后)调用 s_op->delete_inode 或 evict_inode 来释放磁盘空间。VFS 的 inode 对象回收与磁盘 inode 释放是两个独立但关联的阶段。

4.4 inode 生命周期状态机

下图展示了 inode 从分配到回收的核心状态转换,以及关键引用计数的影响:

inode 生命周期状态机 未使用 (slab 缓存) i_count=0, i_nlink=0 分配中 alloc_inode() 调用 活动 (Active) i_count > 0 已关联 dentry 未链接 (Unlinked) i_nlink = 0 等待 i_count 归零 回收中 destroy_inode() 文件系统请求 inode_init_always() unlink/rmdir i_count 归零 i_nlink=0 & i_count=0 归还 slab 缓存 关键状态说明 • 未使用:slab 缓存中的空闲 inode • 活动:被 dentry 引用,可正常访问 • 未链接:已从目录树删除 • 回收中:释放内存资源 • 回收后回到 slab 缓存复用

4.5 文件系统驱动适配要点

在实现具体文件系统驱动时,inode 相关的适配工作主要集中在以下几个方面:

适配项 必须实现 说明
s_op->alloc_inode 是 分配 inode 对象(通常从 slab 缓存分配包含私有扩展的结构体)
s_op->destroy_inode 是 释放 inode 对象及私有资源
s_op->evict_inode 是 当 inode 被驱逐时调用,负责清理页缓存、释放磁盘 inode(如果 i_nlink 为 0)
i_op->lookup 目录必须 在目录中查找子 dentry 对应的 inode
i_op->create 读写 FS 必须 在目录中创建新文件 inode
i_op->mkdir 读写 FS 必须 在目录中创建子目录 inode
i_op->unlink 读写 FS 必须 删除目录项,减少 i_nlink
i_op->permission 推荐 自定义权限检查逻辑(默认使用通用权限模型)
i_op->getattr 推荐 填充文件属性到 struct kstat
最佳实践: 对于大多数文件系统,建议使用 kmem_cache_create() 创建专用的 slab 缓存来分配 inode 结构体。这样不仅可以提高分配效率,还能通过 slab 缓存统计信息监控 inode 使用情况。例如 ext4 使用 ext4_inode_cachep 分配 struct ext4_inode_info(内嵌 struct inode 作为第一个成员)。

4.6 inode 与 dentry 的协作关系

inode 和 dentry 是 VFS 中紧密协作的两个核心结构。理解它们的关系对于文件系统驱动开发至关重要:

  • inode 代表文件本身:存储元数据,不关心文件名。
  • dentry 代表路径分量:存储文件名和父目录信息,指向对应的 inode。
  • 多对多关系:一个 inode 可以被多个 dentry 引用(硬链接),一个 dentry 只能指向一个 inode。
  • 生命周期依赖:dentry 通过 d_inode 指针持有 inode 引用(增加 i_count),防止 inode 被提前回收。
  • 查找流程:路径解析时,VFS 先查找 dentry 缓存,若未命中则调用 i_op->lookup 从磁盘加载 inode 并创建 dentry。
inode 与 dentry 协作关系 dentry d_name: "file.txt" d_parent: 父 dentry inode i_ino: 12345 i_nlink: 2 i_count: 2 d_inode 指针 dentry (硬链接) d_name: "link.txt" d_inode 指向同一 inode 关键点 • dentry 增加 i_count • 硬链接共享同一 inode • i_nlink 跟踪硬链接数

4.7 常见问题与避坑指南

常见错误 1:忘记实现 evict_inode
如果文件系统没有正确实现 evict_inode,当 inode 被驱逐时,页缓存中的脏页可能不会被写回,导致数据丢失。必须确保在 evict_inode 中调用 truncate_inode_pages_final() 清理页缓存。
常见错误 2:i_count 与 i_nlink 混淆
在判断是否可以释放磁盘 inode 时,必须同时检查 i_nlink == 0 和 i_count == 0。仅 i_nlink == 0 表示文件已被删除,但如果有进程仍持有打开的文件描述符,inode 对象必须保留直到 i_count 也归零。
调试技巧: 可以通过 /proc/fs/<filesystem>/inode 或 slabtop 命令监控 inode 分配情况。内核也提供了 inotify 机制来跟踪 inode 事件。在驱动开发阶段,建议在 alloc_inode 和 destroy_inode 中添加调试打印,确认 inode 生命周期管理正确。