4. 索引节点对象(inode)

索引节点(inode)是VFS层最核心的数据结构之一,它抽象了文件系统中所有文件、目录、设备节点等对象的元数据信息。每个文件或目录在VFS中都有一个唯一的inode实例,负责存储文件的权限、大小、时间戳、数据块指针等静态属性,并通过inode_operations操作表提供对文件元数据的操作接口。

核心概念: inode不包含文件名,文件名存储在目录项(dentry)中。一个inode可以被多个dentry引用(硬链接),但每个inode在文件系统内拥有唯一的编号(i_ino)。

4.1 inode结构体定义

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

字段 类型 说明
i_ino unsigned long inode编号,文件系统内唯一
i_count atomic_t 引用计数,表示有多少内核路径引用此inode
i_nlink unsigned int 硬链接计数,当为0时inode可被回收
i_mode umode_t 文件类型与权限位(S_IFMT掩码)
i_uid / i_gid kuid_t / kgid_t 文件所有者和所属组
i_size loff_t 文件大小(字节)
i_atime / i_mtime / i_ctime struct timespec64 访问时间、修改时间、状态变更时间
i_blocks blkcnt_t 文件占用的磁盘块数(512字节为单位)
i_op const struct inode_operations * inode操作表指针
i_fop const struct file_operations * 文件操作表指针(默认从inode继承)
i_sb struct super_block * 指向所属超级块
i_mapping struct address_space * 页缓存映射,管理文件数据页
i_data struct address_space 内嵌的address_space实例
i_dentry struct list_head 引用此inode的dentry链表头
i_list / i_sb_list struct list_head 全局inode链表和超级块inode链表
关键设计: inode结构体通过i_mode的高4位(S_IFMT)区分文件类型:普通文件(S_IFREG)、目录(S_IFDIR)、字符设备(S_IFCHR)、块设备(S_IFBLK)、FIFO(S_IFIFO)、套接字(S_IFSOCK)和符号链接(S_IFLNK)。VFS层根据此字段决定调用哪些操作函数。

4.2 inode编号(i_ino)

每个inode在所属文件系统中拥有唯一的编号i_ino,该编号由底层文件系统在创建inode时分配。VFS层不保证不同文件系统间的inode编号唯一性,但同一文件系统内必须唯一。

inode编号的分配策略因文件系统而异:

  • 基于位图的分配: ext2/ext3/ext4使用块位图和inode位图,从预分配的inode表中分配连续编号。
  • 基于哈希的分配: tmpfs使用哈希表管理inode,编号由递增计数器生成。
  • 基于对象的分配: btrfs使用B树管理inode,编号由对象ID和偏移量组合。
注意: i_ino的类型是unsigned long,在32位系统上最大值为2^32-1,在64位系统上为2^64-1。某些文件系统(如FAT)的inode编号由目录项偏移量生成,可能不连续。
inode编号分配策略对比 基于位图分配 ext2/ext3/ext4 预分配inode表 连续编号 基于哈希分配 tmpfs 哈希表管理 递增计数器 基于对象分配 btrfs B树管理 对象ID+偏移 inode编号特性 • 同一文件系统内必须唯一 • 不同文件系统间不保证唯一 • 32位系统上限2^32-1,64位系统上限2^64-1

4.3 inode操作表(inode_operations)

inode_operations结构体定义了VFS层对inode元数据进行操作的回调函数集合。每个文件系统必须实现这些回调,以提供对文件元数据的访问和修改能力。

struct inode_operations {
    // 目录项查找(用于目录inode)
    struct dentry * (*lookup) (struct inode *dir, struct dentry *dentry, unsigned int flags);
    
    // 创建新文件
    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);
    
    // 获取属性
    int (*getattr) (struct user_namespace *mnt_userns, const struct path *path,
                    struct kstat *stat, u32 request_mask, unsigned int query_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);
    
    // 列出扩展属性
    ssize_t (*listxattr) (struct dentry *dentry, char *buffer, size_t size);
    
    // 删除扩展属性
    int (*removexattr) (struct user_namespace *mnt_userns, struct dentry *dentry,
                        const char *name);
    
    // 更新inode时间戳
    int (*update_time) (struct inode *inode, struct timespec64 *time, int flags);
    
    // 权限检查
    int (*permission) (struct user_namespace *mnt_userns, struct inode *inode, int mask);
};
设计要点: inode_operations中的大部分回调函数是可选的。例如,普通文件inode通常只需要实现getattr、setattr和permission;目录inode则需要实现lookup、create、mkdir、unlink等目录操作。VFS层在调用前会检查函数指针是否为NULL。

下表列出了不同文件类型inode的典型操作表实现:

文件类型 必需操作 可选操作 典型实现
普通文件 getattr, setattr, permission getxattr, setxattr, update_time ext4_file_inode_operations
目录 lookup, create, mkdir, unlink, rmdir, rename symlink, link, getxattr ext4_dir_inode_operations
符号链接 get_link (或 readlink) getattr, setattr ext4_symlink_inode_operations
特殊文件(设备/FIFO/套接字) getattr, setattr, permission 无 def_blk_aops / def_chr_aops

4.4 inode的分配与回收

inode的生命周期管理涉及分配、初始化、缓存和回收四个阶段。VFS层提供了统一的分配接口alloc_inode和回收接口destroy_inode,底层文件系统可以通过超级块操作表super_operations中的回调定制这些行为。

4.4.1 inode分配流程

  1. 调用入口: VFS层通过new_inode()或new_inode_pseudo()函数请求分配inode。
  2. 超级块回调: 调用sb->s_op->alloc_inode(sb),文件系统在此回调中从slab缓存分配inode结构体内存。
  3. 初始化: VFS层调用inode_init_always()设置inode的通用字段(i_count=1, i_nlink=1, i_mode=0等)。
  4. 编号分配: 文件系统在alloc_inode回调中或之后分配inode编号(i_ino)。
  5. 加入链表: inode被加入超级块的s_inodes链表和全局inode_in_use链表。
// VFS层inode分配核心路径(简化)
struct inode *new_inode(struct super_block *sb)
{
    struct inode *inode;
    
    // 1. 通过超级块回调分配inode内存
    inode = alloc_inode(sb);
    if (!inode)
        return NULL;
    
    // 2. 初始化通用字段
    if (inode_init_always(sb, inode)) {
        destroy_inode(inode);
        return NULL;
    }
    
    // 3. 设置默认操作表
    inode->i_op = &empty_inode_operations;
    inode->i_fop = &empty_file_operations;
    
    // 4. 加入全局链表
    spin_lock(&inode_sb_list_lock);
    list_add(&inode->i_sb_list, &sb->s_inodes);
    spin_unlock(&inode_sb_list_lock);
    
    return inode;
}

4.4.2 inode回收流程

inode的回收由引用计数i_count和硬链接计数i_nlink共同控制:

  • i_count == 0: 表示没有内核路径引用此inode,可以加入空闲链表。
  • i_nlink == 0: 表示文件已被删除,inode可以被完全销毁。

回收路径分为两个阶段:

  1. 清理阶段(evict): 调用sb->s_op->evict_inode(inode),文件系统在此回调中释放磁盘块、清理页缓存、删除扩展属性等。
  2. 销毁阶段(destroy): 调用sb->s_op->destroy_inode(inode),文件系统释放inode结构体内存(通常归还slab缓存)。
关键约束: 在evict_inode回调中,文件系统必须调用truncate_inode_pages_final()清理所有页缓存,否则可能导致内存泄漏。VFS层在evict_inode返回后会自动从全局链表中移除inode。
inode生命周期管理流程 alloc_inode() inode_init_always() 加入s_inodes链表 inode使用中(i_count > 0, i_nlink > 0) 文件读写、元数据操作、权限检查 回收触发:i_count == 0 或 i_nlink == 0 evict_inode() 释放磁盘块、清理页缓存 destroy_inode() 释放内存、归还slab缓存 inode生命周期结束

4.5 inode缓存与slab分配器

为了提升inode分配和回收的性能,Linux内核使用slab分配器管理inode结构体的内存。每个文件系统在初始化时创建专用的slab缓存:

// ext4文件系统创建inode slab缓存示例
static int __init ext4_init_fs(void)
{
    // 创建ext4_inode_info的slab缓存
    ext4_inode_cachep = kmem_cache_create("ext4_inode_cache",
                                          sizeof(struct ext4_inode_info),
                                          0, SLAB_RECLAIM_ACCOUNT | SLAB_MEM_SPREAD,
                                          NULL);
    if (!ext4_inode_cachep)
        return -ENOMEM;
    return 0;
}

// 文件系统alloc_inode回调使用slab缓存分配
static struct inode *ext4_alloc_inode(struct super_block *sb)
{
    struct ext4_inode_info *ei;
    
    ei = kmem_cache_alloc(ext4_inode_cachep, GFP_NOFS);
    if (!ei)
        return NULL;
    
    // 初始化ext4私有字段
    ei->vfs_inode.i_ino = 0;
    // ... 其他初始化
    
    return &ei->vfs_inode;  // 返回VFS inode指针
}
性能优化: slab缓存通过对象复用减少内存分配开销,同时利用CPU缓存局部性提升访问速度。内核还维护了inode_unused和inode_in_use两个全局链表,用于快速查找和回收不再使用的inode。

4.6 本章小结

inode对象是VFS层文件元数据管理的核心,其设计体现了以下关键原则:

  • 抽象隔离: inode结构体通过i_op和i_fop操作表将VFS层与具体文件系统实现解耦。
  • 引用计数管理: i_count和i_nlink双计数机制确保inode在文件被打开或硬链接存在时不会被提前回收。
  • 生命周期回调: 通过super_operations中的alloc_inode、evict_inode和destroy_inode回调,允许文件系统定制inode的分配与回收行为。
  • 缓存友好: slab分配器和全局链表管理机制保证了inode分配的高效性和可回收性。