Linux VFS机制简析(一)
本文主要基于Linux内核文档,简单分析Linux VFS机制,以期对编写新的内核文件系统(通常是给分布式文件系统编写内核客户端)的场景有所帮助。
个人渊源
切入正文之前先扯点别的,舰队我在04年刚接触Linux时就深入分析了VFS,当时刚毕业入职一家做NAS存储的公司,需要对VFS、block device、MD等内核模块深入了解。时隔10几年之后的今天,因给一个分布式文件系统做内核客户端,重拾VFS发现一切还是熟悉的味道。这十几年过去了,内核版本从2.6到4.x,VFS的机制和整体架构变化不大,依然是各种底层文件系统和用户态接口之间不可或缺的转换层。
Overview
VFS(Virtual File System)是Linux内核里提供文件系统接口给用户态应用程序的一个虚拟文件系统层。同时VFS还提供了抽象化的操作接口以方便实现内核的底层文件系统。
Directory Entry Cache (dcache)
VFS实现open、stat、chmod等类似的文件系统调用,他们传递一个pathname参数给VFS。VFS根据文件路径pathname搜索directory entry cache(dentry cache或者dcache)获取对应的dentry。所以dcache是一个高速目录项缓存,用于映射文件路径和dentry。dentry结构用于优化查询性能,只存在于内存中,不实际存储到磁盘。
内存限制,并不是所有dentry都能在缓存命中,当根据pathname找不到对应dentry时,VFS调用lookup接口向底层文件系统查找获取inode信息,以此建立dentry和其对应的inode结构。
Inode
每个dentry通常对应一个inode结构用于描述文件、目录等的基本元数据信息。如果底层是磁盘存储,Inode结构会保存到磁盘。当需要时从磁盘读取到内存中进行缓存。一个inode结构可以被多个dentry指向,如硬链接。对于网络文件系统(分布式文件系统),Inode结构需要通过网络协议获取到缓存中。
VFS通过父目录的lookup方法来获取某个文件的inode信息,该方法由底层文件系统实现。一旦获取了inode信息,open,stat等无聊的操作直接从缓存里进行,变得很快。
File
Open一个文件还需要另外一个数据结构:File。File用于表示一个处于Open状态的文件,同一个文件被Open多次对应不同的File结构。应用程序打开文件后对应一个句柄(FD, file descriptor),每个FD都对应到内核的一个File结构,因此File结构直接存放在进程的FD表里,通过FD可以快速获取到File数据结构。
VFS实现用户态文件读写关闭操作时,通过用户态的FD来获取对应的File结构,然后调用对应的底层文件系统方法。只要有File结构正在使用,就增加dentry的引用计数,保证dentry和inode结构没有从缓存里删除。
Registering and Mounting a Filesystem
通过如下函数进行文件系统的注册和注销操作:
#include
extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);
其中struct file_system_type用于描述文件系统基本信息和mount()等操作。当挂载文件系统到目录时,调用对应file_system_type里的mount()函数。原文件系统目录树上挂载点会附上新的vfsmount,当路径解析到挂载点时,会自动跳转到vfsmount的根目录。
通过/proc/filesystems可以查看到所有注册的文件系统类型。
struct file_system_type
结构体file_system_type的定义如下:
struct file_system_type {
115 const char *name;
116 int fs_flags;
117 struct dentry *(*mount) (struct file_system_type *, int,
118 const char *, void *);
119 void (*kill_sb) (struct super_block *);
120 struct module *owner;
121 struct file_system_type * next;
122 struct list_head fs_supers;
123 struct lock_class_key s_lock_key;
124 struct lock_class_key s_umount_key;
125 };
其中,name是文件系统名称,如ext4, xfs等等。fs_flags为各种标识,如FS_REQUIRES_DEV, FS_NO_DCACHE等。mount()函数指针用于挂载一个新的文件系统实例。kill_sb()函数指针用于关闭文件系统实例。owner是VFS内部使用,通常设置为THIS_MODULE。next也是VFS内部使用,初始化时设置为NULL即可。s_lock_key和s_umount_key是lockdep相关的结构。
mount()函数有几个参数:fs_type为对应的file_sytem_type结构指针。flags为挂载的标识。dev_name为挂载的设备名,对于网络文件系统通常是一个网络路径。data为挂载的选项,通常为一组ASCII字符串。
mount()必须返回文件系统目录树的root dentry。文件系统的super block增加一个引用计数并处于locked状态。mount失败时返回ERR_PTR(err)。mount()函数可以选择返回一个已经存在的文件系统的一个子树,而不是创建一个新的文件系统实例,这种情况返回的是子树的root dentry。
底层文件系统实现mount,可以直接调用通用的mount实现:mount_bdev(在块设备上挂载文件系统)、mount_nodev(挂载没有设备的文件系统)和mount_single(挂载在不同的mounts间共享实例的文件系统),并提供一个fill_super()的回调函数用于创建root dentry和inode。比如FUSE就通过调用mount_nodev来实现mount操作。
其中file_super()回调函数的参数包括:struct super_block sb(文件系统sb,需要在fill_super()里进行初始化)、void data(文件系统挂载的选项字符串)、int silent(是否忽略error)。
当然也可以参考通用的mount实现自己的mount操作,比如Ceph就直接调用了sget()函数创建sb并通过set()回调函数初始化sb。
Mount Options
mount函数会传递一个options的字符串,以逗号隔开。它是mount命令输入的选项(通过-o设置)。options的格式可以是如下两种:
- option
- option=value
Linux内核头文件linux/parser.h里定义了帮助解析options的API。可以从现有的文件系统代码里找到使用方法。
如果一个文件系统使用了mount options,则必须实现s_op->show_options()函数将选项进行显示。显示的规则如下:
- 如果option不是默认值,则必须显示。
- 如果option等于默认值,则可选择是否显示。
Superblock and struct super_operations
Superblock超级块(简称sb,莫名哈哈一笑)代表一个挂载的文件系统,其数据结构保存了文件系统基本的元数据信息。其中s_op指向了struct super_operations,为sb这一级的函数操作合集。
super_operations的定义如下:
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, int);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
int (*nr_cached_objects)(struct super_block *);
void (*free_cached_objects)(struct super_block *, int);
};
所有的函数,如果没有特别说明,都在没有持有锁的情况下被调用,因此大部分这些函数都可以安全地进行阻塞操作。所有的函数都只在进程上下文中被调用(区别于中断处理或者中断处理下半部分)。
alloc_inode:被inode_alloc()函数调用用于分配inode内存并进行inode结构初始化。如果函数未定义,则简单的分配一个'struct inode'。通常alloc_inode用于底层文件系统分配一个包含inode结构体的更大的结构体(特定的inode结构,如:fuse_inode)。
destroy_inode:被destroy_inode()函数调用用于释放inode相关申请的资源。只有alloc_inode定义了才需要定义destroy_inode,并且释放的也是alloc_inode里申请的相关资源。
dirty_inode:由VFS调用标记inode dirty(元数据信息被修改过并且没有同步到磁盘或服务器)。
write_inode:由VFS调用用于将inode同步到磁盘。第二个参数用于标识是否同步写盘。
drop_inode:VFS在当inode的引用计数减为0时,调用该函数。调用者已经持有了inode->i_lock。该函数返回0,则inode将可能被丢到LRU链表里,返回1则会由调用者继续调用evict_inode和destroy_inode。如果文件系统不需要缓存inode,则该函数可以设置为NULL或者generic_delete_inode(函数里直接return 1)。
delete_inode:VFS删除inode时直接调用该函数。由于查看的Linux文档版本是2.6.39,所以有该函数指针,在3.10版本已经没有了detele_inode。
put_super:VFS想要释放sb时调用(如umount操作)。调用者已经持有sb的lock。
sync_fs:VFS想要把该文件系统所有的脏数据刷盘时调用。
freeze_fs:目前只有LVM使用。用于冻结文件系统,不能进行写入操作。
unfreeze_fs:解冻文件系统,使其可以写入。
statfs:用于获取文件系统的统计信息。
remount_fs:用于重新挂载文件系统,调用者持有kernel lock。
clear_inode:同样在3.10版本没有了。
umount_begin:用于umount文件系统。
show_options:用于/proc/mounts里显示文件系统的mount选项。
quota_read和quota_write:用于读写文件系统的quota文件。
nr_cached_objects和free_cache_objects:用于返回可以释放的cache对象个数,以及进行实际的释放对象操作。
可以看到super_operations包含了inode的分配、初始化和释放。inode里的i_op字段指向了底层文件系统inode相关操作合集:struct inode_operations。
struct inode_operations
struct inode_operations定义如下,它描述了VFS如何管理inode对象。
struct inode_operations {
int (*create) (struct inode *,struct dentry *, umode_t, bool);
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char __user *,int);
void * (*follow_link) (struct dentry *, struct nameidata *);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*permission) (struct inode *, int);
int (*get_acl)(struct inode *, int);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*update_time)(struct inode *, struct timespec *, int);
int (*atomic_open)(struct inode *, struct dentry *,
struct file *, unsigned open_flag,
umode_t create_mode, int *opened);
};
同样,如果没有特别注明,所有函数都在没有锁持有的情况下调用。
create:由open和create系统调用使用。入参inode为父目录的inode,入参dentry为新创建的,没有对应的inode(negative dentry)。底层文件系统需要调用d_instantiate()将dentry和新创建的inode进行关联。只有目录类型的inode才会调用该函数指针。
lookup:VFS需要查找目录下面某个inode信息是调用该函数。入参dentry里携带了要查找的文件name。该函数里需要调用d_add()将找到的inode插入到dentry。并且inode的i_count字段需要递增。如果inode没有找到,则dentry插入一个NULL inode(这种dentry称为一个negative dentry)。只有在底层真实错误时才能返回error,此时open、create、mknode等涉及创建inode的操作都会失败。同样也只有目录类型的inode才会调用该函数指针。
在lookup函数里,可以将dentry的d_op字段初始化为自己的dentry_operations,来定制对dentry和dcache的一些管理函数操作合集。
link:link系统调用使用,用于创建硬链接。同样需要调用d_instantiate()来关联dentry和inode。
unlink:unlink系统调用使用,用于删除一个inode关联的文件或目录。
symlink:symlink系统调用使用,用于创建一个软链接。
mkdir:mkdir系统调用使用,用于创建一个子目录。
rmdir:rmdir系统调用使用,用于删除一个子目录。
mknod:mknod系统调用使用,用于创建一个设备inode(char,block)或者一个named pipe (FIFO)或者一个socket。
rename:rename系统调用使用,用于改名。
readlink:readlink系统调用使用,用于读取软链接文件指向的实际路径。
follow_link:VFS调用,用于跟踪获取一个软链接指向的inode。该函数返回一个指针cookie,该cookie会传递给put_link。
put_link:用于释放follow_link里申请的资源,cookie作为最后一个参数传入。它在NFS等文件系统上,page cache不是很稳定的情况下使用。
permission:VFS调用,用于检测访问权限。有可能在rcu-walk mode下被调用,那么该函数必须不能阻塞或者存储数据到inode。如果在rcu-walk mode下遇到问题,则返回-ECHILD,它将在ref-walk mode重新被调用。
setattr:VFS调用,用于设置文件的attr属性。它将被chmod等相关系统调用使用。
getattr:VFS调用,用于获取文件的attr属性。它将被stat等相关系统调用使用。
setxattr:VFS调用,用于设置文件的一个扩展attr属性。它将被setxattr系统调用使用。
getxattr:VFS调用,用于根据属性名称获取文件的一个扩展attr属性。它将被getxattr系统调用使用。
listxattr:VFS调用,用于列出给定文件的所有扩展属性。它将被listxattr系统调用使用。
removexattr:VFS调用,用于删除一个扩展attr属性。它将被removexattr系统调用使用。
update_time:VFS调用,用于更新inode的时间(如atime)或者i_version字段。如果该函数没有指定,则VFS将自己更新inode并调用mark_inode_dirty_sync。
atomic_open:该可选的函数,用于性能优化。它将lookup、可能的create操作以及open操作在一个接口里完成。只有negative dentry才会调用该函数。在dentry cache里的positive dentry直接通过f_op->open()函数来打开文件即可。
参考
Linux Documentation: VFS
后记
本篇主要介绍了VFS架构机制和作用,以及如何实现一个底层文件系统的注册和mount、super block和sb operations、inode和inode operations。
下一篇将继续介绍有关Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API:Linux VFS机制简析(二)