PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
环境说明
无
前言
VFS(Virtual File System)是一种软件抽象,主要还是为了连接用户态、内核态和实际文件系统本身。例如:我们可以write一个字符串到磁盘ext4 fs上的某个文件。
在linux里面,有各种各样的文件系统,它们实际存放的位置可能是硬盘(ext4fs等)、RAM(sysfs, procfs, devtmpfs等)、网络(nfs等)等等存储介质。
VFS的基础知识
这里主要介绍VFS的基础知识,以及我们常见的文件操作怎么对应到VFS里面,主要以ext4 fs与vfs的关联为例子.
Directory Entry Cache (dcache)
在vfs里面提供了一种缓存机制,缓存struct dentry项,这个dcache在内核里面表示了一个文件系统的整个文件目录信息(从根目录开始的一个目录信息),但是作为一种cache数据结构,一般会存在cache miss然后创建对应struct dentry。这样就可以很快的查找到我们传入的一个路径的文件对于的struct dentry项。
vfs通过我们常见的open接口操作文件时,一般是用路径来标识一个文件名的,那么怎么将我们传入的名字转换成对应的目录/文件信息呢?答案就是上面提到的dcache数据结构,通过查询dcache得到目录/文件信息,这个部分的内容也是open系统调用常常做的事情。
下面是dcache项的基本定义:
c
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_spinlock_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
... ...
struct super_block *d_sb; /* The root of the dentry tree */
... ...
};
还记得我们在《Linux 文件系统(一) --- ext4文件系统简介》( https://www.cnblogs.com/Iflyinsky/p/18162137 )中提到,目录也是一种文件嘛?文件又是用inode来表示的,那么struct dentry就可以得到对应的struct inode信息了。
struct inode 对象
大家应该都听过一句话,在unix里面,一切皆文件。那么对于vfs来说,一个独立的文件就是struct inode对象.
c
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
... ...
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
... ...
}
其实这个struct inode和struct ext4_inode有许多相似的属性,他们也有些许关联。
对于struct inode来说,很重要的就是struct inode_operations,这个代表着我们可以对这个inode进行的操作,其结构大概如下:
c
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
... ...
int (*create) (struct mnt_idmap *, struct inode *,struct dentry *,
umode_t, bool);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
... ...
}
根据父目录的对应的lookup函数,我们可以查找对应的文件inode信息。此外,还有创建删除inode节点的方法,这些方法在文件系统装载的时候实现。
struct file 对象
我们上面讲的struct dentry和struct inode是和对应的文件系统存储的数据是息息相关的。但是实际我们操作文件的第一步是打开文件,对于一个打开的文件,在内核里面使用struct file来标识,其结构如下:
c
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
/*
* f_{lock,count,pos_lock} members can be highly contended and share
* the same cacheline. f_{lock,mode} are very frequently used together
* and so share the same cacheline as well. The read-mostly
* f_{path,inode,op} are kept on a separate cacheline.
*/
struct file {
... ...
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
... ...
}
对于一个file对象来说,除了上面提到的struct dentry和struct inode关联外,还有一个重要的结构是:struct file_operations,对于这个结构来说,大家应该非常的熟悉,包含了open/close/read/write等等接口:
c
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 *);
... ...
int (*open) (struct inode *, struct file *);
... ...
到这里,其实我们vfs我们常见用到的基本就讲完了,串起来是说就是open一个文件,创建一个struct file对象,关联一个struct dentry和struct inode,这时就可以对文件进行操作了.
VFS的深入知识
FileSystem 的注册和取消注册
我们前面提到了,vfs最终会对接到实际文件系统本身,那么VFS支持哪些FS呢?在linux的/proc/filesystems文件中,存放了当前注册到vfs的所有支持的FS。
下面我们介绍文件系统的注册和取消注册:
c
/*
* Filesystem context for holding the parameters used in the creation or
* reconfiguration of a superblock.
*
* Superblock creation fills in ->root whereas reconfiguration begins with this
* already set.
*
* See Documentation/filesystems/mount_api.rst
*/
struct fs_context {
const struct fs_context_operations *ops;
struct mutex uapi_mutex; /* Userspace access mutex */
struct file_system_type *fs_type;
void *fs_private; /* The filesystem's context */
void *sget_key;
struct dentry *root; /* The root and superblock */
... ...
}
struct file_system_type {
const char *name; //文件系统名字,例如ext4
int fs_flags;
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_spec *parameters;
struct dentry *(*mount) (struct file_system_type *, int,
const char *, void *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct hlist_head fs_supers;
... ...
};
#include <linux/fs.h>
extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);
在register_filesystem/unregister_filesystem函数里面,主要是对内核里面的file_system_type变量进行链表操作,注册就是增加链表节点,取消注册就是删除链表节点。
FileSystem的挂载和卸载
这里我们先举个例子,对于linux来说,内核启动后第一个挂载的文件系统,也就是挂载在/根目录的文件系统。如果大家观察过一些linux的启动相关信息,有个常见的问题就是:
txt
Error: root fs cannot be detected。
这个问题就是内核没有找到适合的根文件系统来加载,一般来说失败后会自动进入一个叫做initramfs的文件系统,方便进行诊断。
对于用户挂载和卸载文件系统来说,一般我们是使用mount/umount命令,其和内核启动后挂载第一个文件系统的操作类似,其实我们执行mount命令的时候,对调用对应的file_system_type的mount函数(看上文中的file_system_type有一个mount函数)来完成挂载的操作。
首先我们来看看mount命令是怎么到file_system_type中的mount函数的。我们来看看调用序列:
- mount命令调用userspace 中的mount函数
- 接着调用sys_mount
- 接着调用do_mount
- 接着调用path_mount
- 接着调用do_new_mount,在这里我们会根据参数,获取或者创建struct file_system_type对象。(linux kernel v4.20.17)
- 接着调用vfs_kern_mount。(linux kernel v4.20.17)
- 接着调用mount_fs,在这里面通过struct file_system_type对象调用ext4_mount函数。(linux kernel v4.20.17)
上面的操作完毕, 我们得到一个struct vfsmount 和 struct mount 对象,这个对象代表了一个文件系统的挂载基本信息。struct mount 中的mnt_instance指向的是ext4 fs 的root dentry中的super_block存放的链表。意思就是,创建好的一个文件系统,其挂载点信息可以在root dentry中找到。
c
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
struct mnt_idmap *mnt_idmap;
} __randomize_layout;
struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
struct dentry *mnt_mountpoint;
struct vfsmount mnt;
... ...
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
... ...
}
下面我们来讲讲ext4的file_system_type定义中,ext4_mount的调用。
注意,file_system_type的mount接口在新的内核版本中被废弃了,因为有新的mount api实现,所以在5.17-rc1中,ext4_mount这个函数无了。下面是linux kernel中这个提交的commit内容:
txt
ext4: switch to the new mount api
Add the necessary functions for the fs_context_operations. Convert and
rename ext4_remount() and ext4_fill_super() to ext4_get_tree() and
ext4_reconfigure() respectively and switch the ext4 to use the new api.
One user facing change is the fact that we no longer have access to the
entire string of mount options provided by mount(2) since the mount api
does not store it anywhere. As a result we can't print the options to
the log as we did in the past after the successful mount.
Signed-off-by: Lukas Czerner <lczerner@redhat.com>
Reviewed-by: Carlos Maiolino <cmaiolino@redhat.com>
Link: https://lore.kernel.org/r/20211027141857.33657-13-lczerner@redhat.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
commit-id:cebe85d570cf84804e848332d6721bc9e5300e07
下面,我们仍用ext4_mount的函数内容来讲解。对于挂载来说,一般会做如下操作(对于磁盘来说):
- 根据mount命令的参数,ext4_mount调用mount_bdev函数。
- 根据mount_bdev函数的dev_name获取struct block_device对象。
- 通过struct block_device对象,初始化并创建struct super_block对象,将对象放到super_blocks链表中。
- 返回此文件系统对于的根dentry的引用(例如ext4fs: 根据block group0中的inode table[2]获取根节点,并创建dentry),这个时候我们就可以解析整个文件系统的所有文件目录了。
从上面的说明我们可以知道,一个struct super_block对象,代表一个挂载的文件系统。其定义如下:
c
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize;
loff_t s_maxbytes; /* Max file size */
struct file_system_type *s_type;
const struct super_operations *s_op;
... ...
}
从上面的结构来看,最重要的就是s_op了,这个代表对一个文件系统的一些基本操作方法。此外,对于s_list来说,很明显的表达了struct super_block会被存储到一个链表里面,在linux里面,是存放在 static LIST_HEAD(super_blocks) 变量中的。
c
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*free_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
... ...
}
ext4的各种操作实现
上面我们提到了struct super_operations、struct inode_operations、struct file_operations这三个重要的操作,对于挂载的ext4fs来说,其实现在ext4中实现,并对应赋值给对应的指针。他们定义分别如下:
c
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.iopoll = iocb_bio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = ext4_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
const struct inode_operations ext4_file_inode_operations = {
.setattr = ext4_setattr,
.getattr = ext4_file_getattr,
.listxattr = ext4_listxattr,
.get_inode_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
.fiemap = ext4_fiemap,
.fileattr_get = ext4_fileattr_get,
.fileattr_set = ext4_fileattr_set,
};
static const struct super_operations ext4_sops = {
.alloc_inode = ext4_alloc_inode,
.free_inode = ext4_free_in_core_inode,
.destroy_inode = ext4_destroy_inode,
.write_inode = ext4_write_inode,
.dirty_inode = ext4_dirty_inode,
.drop_inode = ext4_drop_inode,
.evict_inode = ext4_evict_inode,
.put_super = ext4_put_super,
.sync_fs = ext4_sync_fs,
.freeze_fs = ext4_freeze,
.unfreeze_fs = ext4_unfreeze,
.statfs = ext4_statfs,
.show_options = ext4_show_options,
.shutdown = ext4_shutdown,
#ifdef CONFIG_QUOTA
.quota_read = ext4_quota_read,
.quota_write = ext4_quota_write,
.get_dquots = ext4_get_dquots,
#endif
};
从上面来,还没有挂载的时候,对于一个ext4fs的各种操作就已经实现了,挂载只是将这些操作实现对应赋值而已。
这里多说一句,其他的fs也会有对应operations的实现。例如:我们常见的驱动开发的时候,file_operations的填充可以说是基操。
总结
总的来说,vfs提供了对各种fs的操作的封装。mount命令可以将特定文件系统绑定到vfs。当我们mount一个fs时,可以得到这个fs的root dentry,super_block,mount等结构信息。
我们根据一个fs的root dentry信息,可以解析出其目录下的所有文件目录结构,从而达到访问特定文件系统、特定设备的文件的目的。
参考文献
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。