1.概览
VFS全称为 virtual file systemfile 即虚拟文件系统,它被设计出来的目的则是为了将文件系统抽象化及标准化。抽象的过程其实就是归纳总结的过程,需要提炼出一套适用于所有文件系统的共性来,既然是适用所有文件系统的那么也就可以称之为标准了。使用虚拟文件系统可以为用户层屏蔽掉各式各样的文件系统,例如基于内存的procfs、sysfs,基于磁盘的Ext文件系统家族以及来自于微软的NTFS、VFAT,还有来自华为的EROFS等。总之一句话,抽象而出的VFS就是为了屏蔽具体不同文件系统用的。举个没有VFS的拷贝操作,
cp /nfsfs/data1 /ext4/data
如果没有VFS,那么开发cp命令的时候就需要根据不同的文件系统来调用其内部实现的拷贝方法。那么使用VFS后,不论是基于nfs的还是ext4的拷贝方法,对于cp命令开发而言都是同一个系统调用了。
2.关键概念
下面还是需要介绍下VFS中重要的几个概念的。越是抽象的东西其涵盖的内容也就越多,因此理解起来相对更难。所以在理解抽象事物的时候,尽量将其和具象的事物建立联系。
2.1 dentry object
dentry代表一个路径中的一项,例如下面的路径就对应3个dentry
/tmp/mydata
3个dentry依次对应根目录/,根目录下的tmp文件夹,tmp文件夹下的文件mydata,每一个dentry都会记录着和他们相连的dentry。一个目录是需要对应文件(inode)的,否则他就是废路径,反之他就是积极的。其在内核在的定义如下
//include\linux\dcache.h
struct dentry {
unsigned int d_flags; /* protected by d_lock */
...
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
...
};
2.2 inode object
inode就是具体的文件了,用来记录一个文件的信息。常见的文件、文件夹、FIFO在硬盘中都是由它代表的。如果文件系统时基于内存的那么它就存在于内存如(procfs、sysfs),位置则跟随文件系统的适用对象,这里指代的是它的主体。还有一种情况,则是内存和硬盘中都会存在的,例如使用vim打开基于磁盘的文件系统的一个文件,那么它会被读入内存,如果这个文件被改变了,相应的kernel也需要将内存中的数据刷入硬盘中去,所以这个过程是设计内存和硬盘的。一个inode是可以对于多个文件路径(dentry)的。其在内核在的定义如下
//include\linux\fs.h
struct inode {
umode_t i_mode;
struct super_block *i_sb;
...
};
2.3 superblock(SB)
代表已被挂载的文件系统,在没有挂载该文件系统前是不会实例化的。这个数据结构一般存储在硬盘中,用来记录被挂载文件系统的信息,可以理解成一个放置文件的容器。其在内核在的定义如下
//include\linux\path.h
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
//include\linux\fs.h
struct super_block {
struct list_head s_list; /* Keep this first */
...
struct file_system_type *s_type;
struct dentry *s_root;
...
};
2.4 file
file用于描述被进程打开的inode的信息,只有进程打开一个inode的时候才会生成。并且所有被该进程打开的文件都会生成对应的file存在任务数据结构的数组中。在应用编程的过程中,open返回的fd实际上就是这个数组的下标。注意数组的0/1/2对应的file是固定的,其分别对应标准输出、标准输入、标准错误。该数据也是存在于内存的。其在内核在的定义如下
//include\linux\fs.h
struct file {
...
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
};
2.5 他们之间的关系图
下面是来自于ULK3中的关系图,画的很好供享用
3. 构建自定义的文件系统
下面是基于VFS实现的自定义类型的文件系统,暂且取名为flagstaff吧,下面是挂载好flagstaff文件系统的目录结构
board:/ # ls -lh /dev/flagstafffs/
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 binder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 flagstaff-control
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 hwbinder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 vndbinder
上面的示意图中,flagstaff文件系统的挂载点为目录/dev/flagstaffs。
3.1 注册 flagstaff 文件系统
注意了,对于使用了GKI的kernel,注册文件系统类型是不允许编译成模块的,必须加入到内核中去
+obj-y += \
+ virtualFileSystemTest.o
下面是 flagstaff 文件系统的类型定义
#define FLAGSTAFF_FS_NAME "flagstaff"
static struct file_system_type flagstaff_fs_type = {
.name = FLAGSTAFF_FS_NAME,
.mount = flagstaff_fs_mount,
.kill_sb = flagstaff_fs_kill_super,
.fs_flags = FS_USERNS_MOUNT,
};
vfs_test_drv_init
register_filesystem(&flagstaff_fs_type);
在vfs中,结构体 file_system_type 用来表示一种类型的文件系统,其中name就是对应它的类型,使用接口register_filesystem来对其注册,注册的过程主要将其挂到全局文件系统链表中去,此时是不会调用其内部函数的。其内部的mount函数指针在在用户挂载文件系统的时候会被调用,此处的实现如下
static struct dentry *flagstaff_fs_mount (
struct file_system_type *fst,
int flags,
const char *name,
void *data){
VFS_TEST_INFO();
return mount_nodev(fst, flags, data, flagstaff_fs_fill_super);
}
实际上就是返回了一个dentry,也就是挂载点下面的一个文件,当然也可能是文件夹,因为linux中一切皆文件嘛。
kill_sb和mount函数指针则是配对的,用户挂载文件系统时mount被调用,那么用户反向挂载时,kill_sb则被调用,此处的实现如下
static void flagstaff_fs_kill_super (struct super_block *sb)
{
VFS_TEST_INFO();
}
直接打印供调试。
3.2 flagstaff 文件系统的挂载
可以使用命令mount手动挂载flagstaff文件系统方便测试,其命令如下
// <type> <device> <dir>
mount -t flagstaff flagstaff /dev/flagstafffs/
其中的 type 就是file_system_type.name,device的名字可以随意取。dir就是文件系统要被挂载的文件夹路径,被挂载成功后其内部文件结构会被 flagstaff 的文件系统替代掉。
当用户使用mount命令挂载flagstaff文件系统后,flagstaff_fs_mount就会被调用,那么紧接着mount_nodev也会被调用,其实现如下
//fs\super.c
struct dentry *mount_nodev(struct file_system_type *fs_type,
int flags, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
...
fill_super(s, data, flags & SB_SILENT ? 1 : 0);
s->s_flags |= SB_ACTIVE;
return dget(s->s_root);
}
从mount_nodev的实现可知,最终fill_super也会被调用,也就是我们传入的flagstaff_fs_fill_super函数,我们在其内部来构建前面展示的目录结构。
3.2.1 文件系统的super_block的构造
flagstaff_fs_fill_super
//Block size in bytes
sb->s_blocksize = PAGE_SIZE;
//Block size in number of bits
sb->s_blocksize_bits = PAGE_SHIFT;
//Mount flags
sb->s_iflags &= ~SB_I_NODEV;/* Ignore devices on this fs */
sb->s_iflags |= SB_I_NOEXEC;/* Ignore executables on this fs */
//Filesystem magic number
sb->s_magic = FLAGSTAFF_SUPER_MAGIC;
//Superblock methods
sb->s_op = &flagstaff_fs_super_ops;
//Timestamp’s granularity (in nanoseconds)
sb->s_time_gran = 1;
//Pointer to superblock information of a specific filesystem
sb->s_fs_info = kzalloc(sizeof(FLAGSTAFF_FS_NAME), GFP_KERNEL);
sprintf((char*)sb->s_fs_info, "%s", FLAGSTAFF_FS_NAME);
s_fs_info指向文件系统的私有数据,这块数据不同的文件系统之间是不同的。对于 flagstaff 文件系统则存放的是其自身的文件名,仅作为测试用。
3.2.2 根文件的构建
文件系统被构建后,根文件实际上是不可见的,它只是用来挂载其他文件的。下面是其实现代码
flagstaff_fs_fill_super
struct inode *inode = NULL;
inode = new_inode(sb);
//inode number
inode->i_ino = 1;
inode->i_fop = &simple_dir_operations;
///File type and access rights
inode->i_mode = S_IFDIR | 0755;
/*
i_mtime:Time of last file write
i_atime:Time of last file access
i_ctime:Time of last inode change
*/
inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode);
inode->i_op = &flagstaff_fs_dir_inode_operations;
sb->s_root = d_make_root(inode);
上面说过inode代表实际的文件,我们可以通过接口 new_inode从super_block中分配一个信息的文件,然后初始化它。其中i_fop就是进程打开一个文件后可做的所有操作的实现了,例如 open/read/write/poll等。对于i_op则是用来操作inode本身的。d_make_root 则是为inode生成一个dentry也就是一个路径节点,其中inode和dentry是可以一对多的。
3.2.3 构建 flagstaff-control 文件
create_flagstaff_ctl_file
inode = new_inode(sb);
inode->i_ino = 2;
inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode);
inode->i_mode = 0666;
inode->i_fop = &flagstaff_fs_ctl_fops;
dentry = d_alloc_name(root, "flagstaff-control");
d_add(dentry, inode);
i_ino代表着其在文件系统下的文件标号,跟文件为1,第二个文件自然为2。使用flagstaff_fs_ctl_fops为inode构造i_fop,即进程操作文件时的调用。d_alloc_name用来分配一个自定义文件名的路径节点dentry,然后将dentry和实际的inode通过d_add进行关联后就会显示到文件系统中了。
static ssize_t flagstaff_fs_ctl_read (struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
VFS_TEST_INFO("file:%s", get_name_by_file(file));
return 0;
}
static ssize_t flagstaff_fs_ctl_write (struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
VFS_TEST_INFO("file:%s", get_name_by_file(file));
return size;
}
static const struct file_operations flagstaff_fs_ctl_fops = {
.owner = THIS_MODULE,
.read = flagstaff_fs_ctl_read,
.write = flagstaff_fs_ctl_write,
};
在内部也只是简单的打印被打开的文件名,进程进行读写操作则触发,对于log如下
board:/ # cat /dev/flagstafffs/flagstaff-control &&echo 1 > /dev/flagstafffs/flagstaff-control
[ 224.942102] vfsTest:flagstaff_fs_ctl_read,61:file:flagstaff-control
[ 224.944527] vfsTest:flagstaff_fs_ctl_write,67:file:flagstaff-control
get_name_by_file的实现也很简单,通过file可以获得该文件的文件名,实现如下
static const char* get_name_by_file(struct file *flp)
{
struct path *file_path = NULL;
struct dentry *dentry = NULL;
const char* name = NULL;
file_path = &flp->f_path;
dentry = file_path->dentry;
//Filename
name = dentry->d_iname;
return name;
}
代码中省略了对指针的检查,在实际产品代码中则要加上。
3.2.4 构建 binder hwbinder vndbinder等文件
这几个文件都是通过自定义的文件构建函数(create_file_by_name)构造的,因为其类型是一模一样的,其实现如下
static void create_file_by_name(struct super_block *sb, const char* file_name)
{
struct dentry *dentry;
struct dentry *root;
static int count = 0;
struct inode *inode = NULL;
root = sb->s_root;
inode = new_inode(sb);
/*
* 1 for root inode
* 2 for flagstaff_fs_ctl inode
* So,the stared index is 3.
*/
inode->i_ino = (count++) + 3;
inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode);
inode->i_mode = 0666;
inode->i_fop = &flagstaff_fs_fops;
dentry = lookup_one_len(file_name, root, (size_t)strlen(file_name));
d_instantiate(dentry, inode);
}
构造文件的套路都是类似的,大致流程如下
a)从super_block中分配一个inode也就是真正的文件。
b)填充inode中的i_ino、i_fop等重要的成员。
c)为inode准备一个dentry用于在路径上的显示。
d)将dentry和对应的inode进行关联,如此dentry才有意义。
lookup_one_len会根据文件名来查找有效的dentry,如果不存在那么就返回一个为初始化的dentry。d_instantiate的功能和d_add 类似,用于将dentry和inode进行关联,其实主要就是初始化dentry.d_inode
d_instantiate(dentry, inode)
__d_instantiate(entry, inode);
__d_set_inode_and_type(dentry, inode, add_flags)
dentry->d_inode = inode;
自此整个flagstaff文件系统的文件结构就构造好了,其结构如下
board:/ # ls -lh /dev/flagstafffs/
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 binder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 flagstaff-control
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 hwbinder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 vndbinder
4.完整源码
https://gitee.com/solo-king/linux-kernel-base-usage/blob/master/flagstaff/virtualFileSystemTest.c
5.测试命令
board:/ # remount&&mkdir /dev/flagstafffs/
remount succeeded
board:/ # mount -t flagstaff flagstaff /dev/flagstafffs/ &&ls -lh /dev/flagstafffs/
[ 221.854103] vfsTest:flagstaff_fs_mount,205:
[ 221.854158] vfsTest:flagstaff_fs_fill_super,161:
total 0
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 binder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 flagstaff-control
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 hwbinder
-rw-rw-rw- 1 root root 0 2022-04-08 16:22 vndbinder
board:/ # cat /dev/flagstafffs/flagstaff-control &&echo 1 > /dev/flagstafffs/flagstaff-control
[ 224.942102] vfsTest:flagstaff_fs_ctl_read,61:file:flagstaff-control
[ 224.944527] vfsTest:flagstaff_fs_ctl_write,67:file:flagstaff-control
board:/ # cat /dev/flagstafffs/binder && echo 1 > /dev/flagstafffs/binder
[ 228.964707] vfsTest:flagstaff_fs_read,42:file:binder
[ 228.967398] vfsTest:flagstaff_fs_write,48:file:binder
board:/ # cat /dev/flagstafffs/vndbinder && echo 1 > /dev/flagstafffs/vndbinder
[ 232.261555] vfsTest:flagstaff_fs_read,42:file:vndbinder
[ 232.263734] vfsTest:flagstaff_fs_write,48:file:vndbinder
board:/ # cat /dev/flagstafffs/hwbinder && echo 1 > /dev/flagstafffs/hwbinder
[ 235.514905] vfsTest:flagstaff_fs_read,42:file:hwbinder
[ 235.516466] vfsTest:flagstaff_fs_write,48:file:hwbinder
board:/ #umount /dev/flagstafffs/
[ 238.433477] vfsTest:flagstaff_fs_kill_super,211: