Linux VFS机制简析(一)

时间:2023-02-11 17:24:08

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 <linux/fs.h>
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_keys_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_inodedestroy_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_readquota_write:用于读写文件系统的quota文件。

nr_cached_objectsfree_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机制简析(二)