4. VFS核心数据结构(下):目录项对象与文件对象
在VFS中,目录项对象(dentry)和文件对象(file)是进程与文件系统交互的直接接口。dentry负责路径解析与目录结构缓存,file则管理打开文件的状态与读写操作。理解这两个对象的结构与协作关系,是掌握VFS运行机制的关键。
4.1 目录项对象(dentry)的结构与作用
目录项对象(dentry)是VFS在内存中表示路径名与inode之间映射关系的核心数据结构。它不直接对应磁盘上的目录项,而是内核在路径查找过程中动态创建和缓存的中间层。
4.1.1 dentry的主要作用
- 加速路径解析:缓存路径名到inode的映射,避免重复的磁盘目录项读取。
- 维护目录层次:通过父子指针构建内存中的目录树结构。
- 支持硬链接:多个dentry可以指向同一个inode。
- 负缓存:缓存不存在的路径名,快速返回“文件不存在”。
4.1.2 dentry结构体核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
d_name | struct qstr | 目录项名称(快速字符串) |
d_parent | struct dentry * | 父目录项指针 |
d_inode | struct inode * | 关联的inode(NULL表示负缓存) |
d_op | const struct dentry_operations * | dentry操作函数表 |
d_sb | struct super_block * | 所属超级块 |
d_flags | unsigned int | 状态标志(DCACHE_*) |
d_lru | struct list_head | LRU链表节点,用于回收 |
d_subdirs | struct list_head | 子目录项链表头 |
d_child | struct list_head | 兄弟目录项链表节点 |
4.1.3 dentry状态与生命周期
dentry在内存中经历三种状态:
- 被使用(DCACHE_USED):当前有进程引用,d_count > 0,不会被回收。
- 未使用(DCACHE_UNUSED):d_count == 0,但仍在LRU链表中,可被回收。
- 负缓存(DCACHE_NEGATIVE):d_inode == NULL,表示路径不存在,用于加速失败查找。
性能关键点: dentry缓存是VFS路径解析性能的核心。内核通过hash表(dentry_hashtable)快速查找dentry,LRU算法回收未使用的dentry。在大型目录场景下,dentry缓存命中率直接影响系统调用(如stat、open)的延迟。
4.1.4 dentry操作函数表(dentry_operations)
struct dentry_operations {
int (*d_revalidate)(struct dentry *, unsigned int);
int (*d_weak_revalidate)(struct dentry *, unsigned int);
int (*d_hash)(const struct dentry *, struct qstr *);
int (*d_compare)(const struct dentry *, unsigned int,
const char *, const struct qstr *);
int (*d_delete)(const struct dentry *);
void (*d_release)(struct dentry *);
void (*d_prune)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
struct vfsmount *(*d_automount)(struct path *);
int (*d_manage)(const struct dentry *, bool);
};
关键回调说明:
d_revalidate:在路径查找时验证dentry是否仍然有效(网络文件系统常用)。d_hash:自定义hash计算,用于支持大小写不敏感等特殊需求。d_compare:自定义名称比较函数。d_delete:最后一个引用释放时调用,决定是否允许dentry进入未使用状态。d_automount:处理自动挂载点(如NFS回退)。
4.1.5 dentry与inode关系SVG图
4.2 文件对象(file)的结构与作用
文件对象(file)代表进程打开的一个文件实例。它保存了读写位置、访问模式、文件指针等动态状态。每个进程的struct files_struct中维护了一个文件描述符表,每个描述符指向一个struct file。
4.2.1 file结构体核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
f_path | struct path | 文件路径(包含dentry和vfsmount) |
f_inode | struct inode * | 缓存指针,指向关联inode |
f_op | const struct file_operations * | 文件操作函数表 |
f_pos | loff_t | 当前读写偏移量 |
f_flags | unsigned int | 打开标志(O_RDONLY/O_WRONLY等) |
f_mode | fmode_t | 访问模式(读/写/执行) |
f_count | atomic_long_t | 引用计数 |
f_owner | struct fown_struct | 异步I/O所有者 |
f_cred | const struct cred * | 打开时的安全凭证 |
private_data | void * | 文件系统私有数据 |
4.2.2 文件对象与进程的关系
进程通过文件描述符(fd)访问文件对象。多个fd可以指向同一个file对象(如dup/fork共享),多个file对象也可以指向同一个inode(如多次打开同一文件)。
面试高频题: 为什么
lseek只影响当前file对象的f_pos,而不会影响其他打开同一文件的进程?因为每个struct file是独立的,f_pos是文件实例的私有状态。这正是多线程并发读写时需要加锁保护f_pos的原因。
4.2.3 文件对象生命周期SVG图
4.3 文件操作函数表(file_operations)
file_operations是VFS中最重要的多态接口之一。每个文件系统为它的inode注册一组操作函数,VFS通过f_op指针调用具体实现,实现“同一套系统调用,不同文件系统行为”的设计目标。
4.3.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 *);
__poll_t (*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.3.2 核心回调函数分类
| 类别 | 回调函数 | 说明 |
|---|---|---|
| 定位 | llseek | 修改文件读写位置 |
| 读写 | read / write | 同步读写(用户态缓冲区) |
| 迭代读写 | read_iter / write_iter | 支持iov_iter的读写(现代接口) |
| 目录遍历 | iterate / iterate_shared | 读取目录项列表 |
| 内存映射 | mmap | 将文件映射到进程地址空间 |
| 生命周期 | open / release | 文件打开和关闭时的钩子 |
| 同步 | fsync / fasync | 数据同步到磁盘 |
| 锁 | lock / flock | 文件锁操作 |
| 空间管理 | fallocate | 预分配/释放磁盘空间 |
4.3.3 系统调用到file_operations的调用路径
以read()系统调用为例,VFS的调用链如下:
- 用户态调用
read(fd, buf, count),触发系统调用sys_read。 - 内核通过fd找到
struct file *f,获取f->f_op。 - 调用
f->f_op->read(f, buf, count, &f->f_pos)。 - 具体文件系统(如ext4)实现自己的read函数,执行磁盘I/O。
- 更新
f->f_pos,返回读取字节数。
设计模式: file_operations是典型的策略模式。VFS定义接口契约,每个文件系统提供自己的实现。例如ext4的
ext4_file_operations、procfs的proc_reg_file_ops、socket的socket_file_ops,行为完全不同但对外接口一致。
4.3.4 不同文件系统的file_operations对比
| 文件系统 | file_operations实例 | 特点 |
|---|---|---|
| ext4 | ext4_file_operations | 支持直接I/O、DAX、加密 |
| procfs | proc_reg_file_ops | 动态生成内容,无磁盘操作 |
| sysfs | sysfs_file_operations | 读写内核属性,单值操作 |
| sockfs | socket_file_ops | 网络socket读写,无seek |
| tmpfs | shmem_file_operations | 基于内存,支持共享映射 |
4.3.5 file_operations调用流程SVG图
4.4 dentry、file、inode三者协作关系
在VFS中,dentry、file、inode三者通过指针相互关联,共同完成文件操作:
- dentry:负责路径名到inode的映射,是路径查找的缓存层。
- inode:负责存储文件的元数据和磁盘块映射,是持久化信息的载体。
- file:负责管理打开文件的状态(位置、模式、私有数据),是进程与inode之间的会话层。
常见误区: dentry不是磁盘上的目录项(dirent)。磁盘目录项是存储在目录文件中的固定长度记录,而dentry是内核在内存中构建的缓存结构。两者通过
dentry->d_inode和inode的目录项读取回调间接关联。
4.4.1 核心数据结构关系总结表
| 数据结构 | 生命周期 | 磁盘对应 | 主要作用 |
|---|---|---|---|
| dentry | 动态创建,LRU回收 | 无直接对应 | 路径缓存、目录层次维护 |
| inode | 文件存在则存在 | 磁盘inode | 元数据、数据块映射 |
| file | open到close | 无 | 打开状态、读写位置、操作函数 |