VFS序
这篇导读是后面才回来写的,是因为有些问题觉得提前说出来会让读者更容易理解,自己以后回来复习的时候更好理解。我先提出新人最开始好奇的问题(以自己为例,以前这些问题都是最开始自己非常好奇的 ),然后简单(让人听懂)的回答,之后在后的文章补充。
FAQ
1. 我们读写普通文件(path/to/file),跟读写设备节点(/dev/sda)有区别吗?
没区别,首先明确一点,在文件系统的眼里,这些都是文件,只是不同的类型,我们设备的节点
在关机之后如果文件系统不会删除,那就跟普通文件,那么区别到底在哪里?
处理方式不一样。
open:
if( file is normal file ) {
file->ops = &normal_ops;
} else if( device file ) {
file->ops = &device_ops;
}
ops 就是一系列的函数指针,当我们打开一个文件的时候
文件系统的open函数初始化的
/*
* NOTE:
* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
* can be called without the big kernel lock held in all filesystems.
*/
struct file_operations {
...
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 (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
...
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
...
};对于普通文件,read/write 最终就是从硬盘中读取数据,而对于设备文件,则是从“设备”读取文件,其实读者发现了,硬盘不就是一个设备,所以两者没有任何区别,只是数据的获取方式不一样。
2. 文件操作函数怎么注册的?
冤有头,债有主,文件系统的模块提供的,也可以说是驱动提供的,思考一问题,这不就是一个函数指针的数组吗,只要操作系统可以找到不就好了,最简单的办法,在操作系统里面增加一个map
当然,Linux 有一个相对复杂的注册机制,但是本质就是获得了文件操作函数,所以可以使得一个文件可以正确被打开。
3. 操作系统如何知道一个文件的文件系统
任意一个目录下,都可能挂载一个不同文件系统,那么到底操作系统如何知道的呢?其实也就是说,如何为一个文件获取一个正确的 file_operations。
还是一样的,冤有头债有主,考虑一个所谓的目录结构,是怎么出现的。
举个例子,假设我们在 /mnt/ 目录下,mount 了一个 ext2 文件系统,来自一个全新的硬盘,里面什么信息都没有,刚刚被我们格式化为 ext2 。
这里是一个树结构,当我们在一个目录下挂载之后,就会出现这个情况。现在我们设计一个简单的结构来表示它。
当我们 mount 的时候就会这样,首先先搜索找到 "mnt" 的 file 结构
如此一来,就实现了挂载,当然这是简化版,关键我们理解,我们其实通过了一个新建了子根目录,并且利用了一下结构,来让操作系统明白,这里有新的文件系统。
考虑前一条的切换当前目录的,我们写一个简单的demo来处理。
其实上面的函数非常的简单,当然忽略了非常多的错误处理,搜索file结构的过程其实是数据结构的知识,取决于我们如何设计它们的结构,关键在于,判断新的文件系统挂载于一个新目录的时候,我们的处理就是得到那个挂载文件系统的 根目录 的相关结构,这里就真正实现了跨越了文件系统,但是对上层则是透明的。
现在来考虑下一条指令
关键是什么,是调用了 ext2 提供的文件操作函数的指针,这也是为什么一定要切换到 ext2 的根目录的原因所在,就是因为之后对当前目录的所有操作,都是和具体的文件系统挂钩的,只有正确切换了,才能实现 VFS
文件系统的跨越,本质就是为了隔离不同文件系统的区别,抽象出一个通用的接口,因为不同的文件系统有不同的处理方式,就比如上面的 mkdir,最终可能是,写一个具体的数据到了硬盘,具体怎么写,都是 file system-specific 的,内核不可能知晓,所以得文件系统本身挂载的时候,就要求文件系统的 操作函数指针 必须得注册在内核,提供一个通用的接口。
实现跨越文件系统,就是在路径搜索的过程,比如上面设置一个 mount point 来判断,虽然实现起来可能会很复杂,比如权限的判断,但是本质绝对一样。
实际上就是面向对象( OOP )的思想,内核的 VFS 就是一个 caller ,至于到底怎么操作,那是取决于文件系统本身。
这里仅仅介绍了 mkdir,实际上,对于 open,write,read...等操作,都是一个通用的接口。
现在,我们只需要把文件系统给内核,然后挂载到相关的目录下,如此,这个目录下创建的文件,当我们 read 它的时候,必然是返回这句话。
相信到此,读者已经有点理解,其实所谓写一个新的文件系统,就是自己写一个配合内核规范( VFS调用规范 )的函数,然后注册到内核,这样内核就可以成功调用。
如果有读者,写过一些 callback 函数的实现,其实很类似,这些思想都是一样的。
END
具体到了真正 linux 下的实现,非常的复杂,因为经过长时间的演变,有时候为了适应具体的要求,衍生出了非常复杂的数据结构,总体来说有 inode dentry address_space vfs_mount等,但是它们的思想和早期没有区别,都是为了隔离,是不同文件系统对于上层的用户是透明的。
我们 read 一个设备文件和一个 proc 文件系统的文件,或者一个真正意义上的文件,处理都是一样的,根本的原因,就是挂载文件系统的时候,注册了与其对应的函数指针,VFS 最终落实到了这些函数的调用。
所以在其他的文章,我也说过了,文件其实只是沟通的媒介,真正的实现是落实在一个具体的函数的。是否要与 块设备沟通,又或者是只是在内存中,都由这个注册的函数来决定。
Last updated