4. VFS核心数据结构(下):目录项对象与文件对象

在VFS中,目录项对象(dentry)和文件对象(file)是连接进程与具体文件系统的关键桥梁。dentry负责路径解析与目录结构缓存,file则维护进程与打开文件之间的交互状态。本章深入剖析这两个对象的结构、dentry缓存(dcache)的工作原理以及文件操作表(file_operations)的注册与调用机制。

4.1 目录项对象(dentry)结构

dentry代表路径中的一个分量(如 /usr/src/linux 中的 usr、src、linux),它不直接对应磁盘上的数据,而是内存中用于加速路径查找的抽象。每个dentry关联一个inode,并参与形成目录树。

核心要点:dentry是VFS路径解析的最小缓存单元,一个dentry可以对应多个硬链接(同一inode),但每个路径分量只有一个dentry。

4.1.1 dentry结构体关键字段

字段 类型 说明
d_name struct qstr 目录项名称(含hash值,用于快速比较)
d_parent struct dentry * 父目录项指针
d_inode struct inode * 关联的inode(NULL表示负dentry)
d_sb struct super_block * 所属超级块
d_op struct dentry_operations * dentry操作函数表
d_flags unsigned int 标志位(如DCACHE_DISCONNECTED)
d_lru struct list_head LRU链表节点,用于dcache回收
d_subdirs struct list_head 子目录项链表头
d_child struct list_head 兄弟目录项链表节点

4.1.2 dentry状态与生命周期

  • 正dentry(positive):d_inode != NULL,表示路径分量有效。
  • 负dentry(negative):d_inode == NULL,用于缓存“文件不存在”的结果,避免重复磁盘访问。
  • 别名(alias):一个inode可被多个dentry引用(硬链接场景),通过 d_alias 链表管理。
  • 未连接(disconnected):dentry未挂入主目录树,常见于文件系统遍历或NFS回退路径。
开发提示:负dentry是性能优化的关键手段。当应用程序反复访问不存在的路径时,负dentry可避免每次发起磁盘I/O。但过多负dentry会占用内存,需通过 dentry_operations 的 d_delete 回调控制回收策略。

4.2 dentry缓存(dcache)工作原理

dcache是VFS中最核心的缓存机制之一,它将解析过的路径分量dentry保存在内存中,使后续路径查找无需重复遍历磁盘目录结构。dcache由两个主要数据结构支撑:全局hash表和LRU链表。

4.2.1 dcache架构

dcache 架构图 进程路径查找 dentry_hashtable (全局hash表) dentry链(hash桶) dentry实例 dentry_lru_list (LRU回收链表) shrink_dcache() dcache工作流程 1. 进程按路径分量计算hash → 2. 在dentry_hashtable中查找 → 3. 命中则直接返回dentry 4. 未命中则调用文件系统的lookup() → 5. 新dentry加入hash表与LRU链表

4.2.2 dcache查找与缓存命中

当进程访问 /home/user/file.txt 时,VFS逐分量查找:

  1. 从根dentry(/)开始,计算 home 的hash值,在dentry_hashtable中查找。
  2. 若命中,直接获得 home 的dentry,进而获取其inode。
  3. 若未命中,调用底层文件系统的 lookup 回调,从磁盘读取目录项,构造新dentry并插入hash表。
  4. 重复上述步骤处理 user 和 file.txt。
性能指标:dcache命中率通常超过95%。每次命中可避免一次磁盘目录项读取(约5-10ms),对系统整体I/O性能至关重要。

4.2.3 dcache回收与压力管理

当系统内存紧张时,内核通过 shrink_dcache() 回收dentry。回收策略遵循以下原则:

优先级 回收对象 条件
高 负dentry d_inode == NULL,且最近未被访问
中 未打开文件的dentry d_count == 0,且LRU链表尾部
低 已打开文件的dentry d_count > 0,仅当inode被回收时联动释放

4.3 文件对象(file)结构

文件对象代表进程打开的一个文件实例,它位于进程与inode之间,维护了读写位置、访问模式、文件操作表等运行时状态。每个 open() 系统调用创建一个新的file对象,多个进程可打开同一文件,产生多个独立的file对象。

4.3.1 file结构体关键字段

字段 类型 说明
f_path struct path 文件路径(含dentry和挂载点)
f_inode struct inode * 关联的inode(快捷方式)
f_op struct file_operations * 文件操作函数表
f_pos loff_t 当前读写偏移量
f_flags unsigned int 打开标志(O_RDONLY/O_WRONLY/O_RDWR等)
f_mode fmode_t 访问模式(读/写/执行)
f_count atomic_t 引用计数,决定何时释放
f_private void * 文件系统私有数据指针

4.3.2 文件对象与进程的关系

进程文件描述符与file对象关系 进程 A files_struct fd_array[0..n] fd 0 → stdin fd 1 → stdout fd 2 → stderr fd 3 → file对象 file对象 f_pos = 1024 f_flags = O_RDWR f_count = 2 inode i_mode i_size i_blocks 关键关系说明 • 每个进程有独立的files_struct,通过fd_array指向file对象 • 多个fd可指向同一个file对象(dup/fork共享) • 每个file对象唯一对应一个inode(但一个inode可对应多个file对象) • file对象持有f_pos,多进程同时读写同一文件时各自维护偏移 • f_count归零时释放file对象,但inode仍由dentry或缓存持有

4.4 文件操作表(file_operations)

file_operations 是VFS多态设计的核心体现。每个文件系统为inode注册一组操作函数,当进程通过file对象发起系统调用时,VFS通过 f_op 指针调用对应的实现。这种设计使VFS无需关心底层文件系统的具体实现细节。

4.4.1 file_operations结构体

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *, int, loff_t, loff_t);
    void (*show_fdinfo)(struct seq_file *, struct file *);
};

4.4.2 关键回调函数说明

回调 触发系统调用 典型实现逻辑
open open() 检查访问权限、初始化私有数据、预分配资源
release close() 释放file对象私有资源、回写脏数据
read read() 从磁盘读取数据到用户缓冲区,更新f_pos
write write() 从用户缓冲区写入磁盘,处理写时复制
llseek lseek() 修改f_pos,支持SEEK_SET/SEEK_CUR/SEEK_END
mmap mmap() 将文件映射到进程地址空间
fsync fsync() 将文件数据与元数据刷入磁盘

4.4.3 文件操作表的注册与多态调用

文件系统在创建inode时,通过 inode->i_fop 指针注册 file_operations。当进程打开文件时,VFS将inode的操作表复制到file对象的 f_op 字段:

// 文件系统在inode初始化时注册操作表
static const struct file_operations ext4_file_operations = {
    .llseek     = ext4_llseek,
    .read_iter  = ext4_read_iter,
    .write_iter = ext4_write_iter,
    .mmap       = ext4_file_mmap,
    .open       = ext4_file_open,
    .release    = ext4_release_file,
    .fsync      = ext4_sync_file,
};

// VFS open() 系统调用核心路径
int vfs_open(const struct path *path, struct file *file)
{
    struct inode *inode = path->dentry->d_inode;
    file->f_path = *path;
    file->f_inode = inode;
    file->f_op = fops_get(inode->i_fop);  // 关键:继承inode的操作表
    return do_dentry_open(file, inode, NULL);
}
注意事项:文件系统可以在 open 回调中替换 f_op 指针,实现“打开时多态”。例如,设备文件根据打开模式切换读写操作表,或加密文件系统在打开时插入透明加解密层。替换后,后续所有读写调用都走新操作表,无需修改VFS核心路径。

4.5 dentry与file对象的协同工作流

以下流程图展示了从进程发起 open() 到获取file对象的完整路径:

open() 系统调用完整流程 1. 进程调用 open() 2. 路径解析(dcache) 3. 获取dentry 4. 获取inode 5. 分配file对象 6. 调用 file_operations.open() 7. 返回fd给进程 关键数据结构变化 • dentry引用计数+1 • inode引用计数+1 • file对象分配 • f_op = inode->i_fop • f_pos 初始化为0 • fd_array[fd] = file • 进程files_struct更新 • 若open失败,回滚所有 引用计数与分配

4.6 本章小结

  • dentry 是路径分量的内存缓存,通过hash表加速查找,负dentry可避免不必要的磁盘I/O。
  • dcache 采用全局hash表 + LRU链表管理,在内存压力下优先回收负dentry和未引用dentry。
  • file对象 维护进程与打开文件的运行时状态,每个 open() 创建一个独立实例,通过 f_count 管理生命周期。
  • file_operations 是VFS多态的核心,文件系统通过注册回调函数实现差异化行为,VFS通过 f_op 指针统一调用。
  • 从 open() 到返回fd的完整流程涉及dentry查找、inode获取、file对象分配、操作表绑定等多个步骤,每一步都有对应的错误回滚机制。
下章预告:第5章将深入VFS路径解析的完整实现,包括符号链接处理、挂载点穿越、路径遍历优化等高级主题,帮助读者构建完整的路径查找知识体系。