VFSVirtual Filesystem Switch.docx
- 文档编号:11814616
- 上传时间:2023-04-02
- 格式:DOCX
- 页数:14
- 大小:119.44KB
VFSVirtual Filesystem Switch.docx
《VFSVirtual Filesystem Switch.docx》由会员分享,可在线阅读,更多相关《VFSVirtual Filesystem Switch.docx(14页珍藏版)》请在冰豆网上搜索。
VFSVirtualFilesystemSwitch
1.摘要
本文阐述Linux中的文件系统部分,源代码来自基于IA32的2.4.20内核。
总体上说Linux下的文件系统主要可分为三大块:
一是上层的文件系统的系统调用,二是虚拟文件系统VFS(VirtualFilesystemSwitch),三是挂载到VFS中的各实际文件系统,例如ext2,jffs等。
本文侧重于通过具体的代码分析来解释Linux内核中VFS的内在机制,在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。
文章试图从一个比较高的角度来解释Linux下的VFS文件系统机制,所以在叙述中更侧重于整个模块的主脉络,而不拘泥于细节,同时配有若干张插图,以帮助读者理解。
相对来说,VFS部分的代码比较繁琐复杂,希望读者在阅读完本文之后,能对Linux下的VFS整体运作机制有个清楚的理解。
建议读者在阅读本文前,先尝试着自己阅读一下文件系统的源代码,以便建立起Linux下文件系统最基本的概念,比如至少应熟悉superblock,dentry,inode,vfsmount等数据结构所表示的意义,这样再来阅读本文以便加深理解。
回页首
2.VFS概述
VFS是一种软件机制,也许称它为Linux的文件系统管理者更确切点,与它相关的数据结构只存在于物理内存当中。
所以在每次系统初始化期间,Linux都首先要在内存当中构造一棵VFS的目录树(在Linux的源代码里称之为namespace),实际上便是在内存中建立相应的数据结构。
VFS目录树在Linux的文件系统模块中是个很重要的概念,希望读者不要将其与实际文件系统目录树混淆,在笔者看来,VFS中的各目录其主要用途是用来提供实际文件系统的挂载点,当然在VFS中也会涉及到文件级的操作,本文不阐述这种情况。
下文提到目录树或目录,如果不特别说明,均指VFS的目录树或目录。
图1是一种可能的目录树在内存中的影像:
图1:
VFS目录树结构
回页首
3.文件系统的注册
这里的文件系统是指可能会被挂载到目录树中的各个实际文件系统,所谓实际文件系统,即是指VFS中的实际操作最终要通过它们来完成而已,并不意味着它们一定要存在于某种特定的存储设备上。
比如在笔者的Linux机器下就注册有"rootfs"、"proc"、"ext2"、"sockfs"等十几种文件系统。
3.1数据结构
在Linux源代码中,每种实际的文件系统用以下的数据结构表示:
structfile_system_type{
constchar*name;
intfs_flags;
structsuper_block*(*read_super)(structsuper_block*,void*,int);
structmodule*owner;
structfile_system_type*next;
structlist_headfs_supers;
};
注册过程实际上将表示各实际文件系统的structfile_system_type数据结构的实例化,然后形成一个链表,内核中用一个名为file_systems的全局变量来指向该链表的表头。
3.2注册rootfs文件系统
在众多的实际文件系统中,之所以单独介绍rootfs文件系统的注册过程,实在是因为该文件系统VFS的关系太过密切,如果说ext2/ext3是Linux的本土文件系统,那么rootfs文件系统则是VFS存在的基础。
一般文件系统的注册都是通过module_init宏以及do_initcalls()函数来完成(读者可通过阅读module_init宏的声明及arch\i386\vmlinux.lds文件来理解这一过程),但是rootfs的注册却是通过init_rootfs()这一初始化函数来完成,这意味着rootfs的注册过程是Linux内核初始化阶段不可分割的一部分。
init_rootfs()通过调用register_filesystem(&rootfs_fs_type)函数来完成rootfs文件系统注册的,其中rootfs_fs_type定义如下:
structfile_system_typerootfs_fs_type={\
name:
"rootfs",\
read_super:
ramfs_read_super,\
fs_flags:
FS_NOMOUNT|FS_LITTER,\
owner:
THIS_MODULE,\
}
注册之后的file_systems链表结构如下图2所示:
图2:
file_systems链表结构
回页首
4.VFS目录树的建立
既然是树,所以根是其赖以存在的基础,本节阐述Linux在初始化阶段是如何建立根结点的,即"/"目录。
这其中会包括挂载rootfs文件系统到根目录"/"的具体过程。
构造根目录的代码是在init_mount_tree()函数(fs\namespace.c)中。
首先,init_mount_tree()函数会调用do_kern_mount("rootfs",0,"rootfs",NULL)来挂载前面已经注册了的rootfs文件系统。
这看起来似乎有点奇怪,因为根据前面的说法,似乎是应该先有挂载目录,然后再在其上挂载相应的文件系统,然而此时VFS似乎并没有建立其根目录。
没关系,这是因为这里我们调用的是do_kern_mount(),这个函数内部自然会创建我们最关心也是最关键的根目录(在Linux中,目录对应的数据结构是structdentry)。
在这个场景里,do_kern_mount()做的工作主要是:
1)调用alloc_vfsmnt()函数在内存里申请了一块该类型的内存空间(structvfsmount*mnt),并初始化其部分成员变量。
2)调用get_sb_nodev()函数在内存中分配一个超级块结构(structsuper_block)sb,并初始化其部分成员变量,将成员s_instances插入到rootfs文件系统类型结构中的fs_supers指向的双向链表中。
3)通过rootfs文件系统中的read_super函数指针调用ramfs_read_super()函数。
还记得当初注册rootfs文件系统时,其成员read_super指针指向了ramfs_read_super()函数,参见图2.
4)ramfs_read_super()函数调用ramfs_get_inode()在内存中分配了一个inode结构(structinode)inode,并初始化其部分成员变量,其中比较重要的有i_op、i_fop和i_sb:
inode->i_op=&ramfs_dir_inode_operations;
inode->i_fop=&dcache_dir_ops;
inode->i_sb=sb;
这使得将来通过文件系统调用对VFS发起的文件操作等指令将被rootfs文件系统中相应的函数接口所接管。
图3
5)ramfs_read_super()函数在分配和初始化了inode结构之后,会调用d_alloc_root()函数来为VFS的目录树建立起关键的根目录(structdentry)dentry,并将dentry中的d_sb指针指向sb,d_inode指针指向inode。
6)将mnt中的mnt_sb指针指向sb,mnt_root和mnt_mountpoint指针指向dentry,而mnt_parent指针则指向自身。
这样,当do_kern_mount()函数返回时,以上分配出来的各数据结构和rootfs文件系统的关系将如上图3所示。
图中mnt、sb、inode、dentry结构块下方的数字表示它们在内存里被分配的先后顺序。
限于篇幅的原因,各结构中只给出了部分成员变量,读者可以对照源代码根据图中所示按图索骥,以加深理解。
最后,init_mount_tree()函数会为系统最开始的进程(即init_task进程)准备它的进程数据块中的namespace域,主要目的是将do_kern_mount()函数中建立的mnt和dentry信息记录在了init_task进程的进程数据块中,这样所有以后从init_task进程fork出来的进程也都先天地继承了这一信息,在后面用sys_mkdir在VFS中创建一个目录的过程中,我们可以看到这里为什么要这样做。
为进程建立namespace的主要代码如下:
namespace=kmalloc(sizeof(*namespace),GFP_KERNEL);
list_add(&mnt->mnt_list,&namespace->list);//mntisreturnedbydo_kern_mount()
namespace->root=mnt;
init_task.namespace=namespace;
for_each_task(p){
get_namespace(namespace);
p->namespace=namespace;
}
set_fs_pwd(current->fs,namespace->root,namespace->root->mnt_root);
set_fs_root(current->fs,namespace->root,namespace->root->mnt_root);
该段代码的最后两行便是将do_kern_mount()函数中建立的mnt和dentry信息记录在了当前进程的fs结构中。
以上讲了一大堆数据结构的来历,其实最终目的不过是要在内存中建立一颗VFS目录树而已,更确切地说,init_mount_tree()这个函数为VFS建立了根目录"/",而一旦有了根,那么这棵数就可以发展壮大,比如可以通过系统调用sys_mkdir在这棵树上建立新的叶子节点等,所以系统设计者又将rootfs文件系统挂载到了这棵树的根目录上。
关于rootfs这个文件系统,读者如果看一下前面图2中它的file_system_type结构,会发现它的一个成员函数指针read_super指向的是ramfs_read_super,单从这个函数名称中的ramfs,读者大概能猜测出这个文件所涉及的文件操作都是针对内存中的数据对象,事实上也的确如此。
从另一个角度而言,因为VFS本身就是内存中的一个数据对象,所以在其上的操作仅限于内存,那也是非常合乎逻辑的事。
在接下来的章节中,我们会用一个具体的例子来讨论如何利用rootfs所提供的函树为VFS增加一个新的目录节点。
VFS中各目录的主要用途是为以后挂载文件系统提供挂载点。
所以真正的文件操作还是要通过挂载后的文件系统提供的功能接口来进行。
回页首
5.VFS下目录的建立
为了更好地理解VFS,下面我们用一个实际例子来看看Linux是如何在VFS的根目录下建立一个新的目录"/dev"的。
要在VFS中建立一个新的目录,首先我们得对该目录进行搜索,搜索的目的是找到将要建立的目录其父目录的相关信息,因为"皮之不存,毛将焉附"。
比如要建立目录/home/ricard,那么首先必须沿目录路径进行逐层搜索,本例中先从根目录找起,然后在根目录下找到目录home,然后再往下,便是要新建的目录名ricard,那么前面讲得要先对目录搜索,在该例中便是要找到ricard这个新目录的父目录,也就是home目录所对应的信息。
当然,如果搜索的过程中发现错误,比如要建目录的父目录并不存在,或者当前进程并无相应的权限等等,这种情况系统必然会调用相关过程进行处理,对于此种情况,本文略过不提。
Linux下用系统调用sys_mkdir来在VFS目录树中增加新的节点。
同时为配合路径搜索,引入了下面一个数据结构:
structnameidata{
structdentry*dentry;
structvfsmount*mnt;
structqstrlast;
unsignedintflags;
intlast_type;
};
这个数据结构在路径搜索的过程中用来记录相关信息,起着类似"路标"的作用。
其中前两项中的dentry记录的是要建目录的父目录的信息,mnt成员接下来会解释到。
后三项记录的是所查找路径的最后一个节点(即待建目录或文件)的信息。
现在为建立目录"/dev"而调用sys_mkdir("/dev",0700),其中参数0700我们不去管它,它只是限定将要建立的目录的某种模式。
sys_mkdir函数首先调用path_lookup("/dev",LOOKUP_PARENT,&nd);来对路径进行查找,其中nd为structnameidatand声明的变量。
在接下来的叙述中,因为函数调用关系的繁琐,为了突出过程主线,将不再严格按照函数的调用关系来进行描述。
path_lookup发现"/dev"是以"/"开头,所以它从当前进程的根目录开始往下查找,具体代码如下:
nd->mnt=mntget(current->fs->rootmnt);
nd->dentry=dget(current->fs->root);
记得在init_mount_tree()函数的后半段曾经将新建立的VFS根目录相关信息记录在了init_task进程的进程数据块中,那么在这个场景里,nd->mnt便指向了图3中mnt变量,nd->dentry便指向了图3中的dentry变量。
然后调用函数path_walk接着往下查找,找到最后通过变量nd返回的信息是nd.last.name="dev",nd.last.len=3,nd.last_type=LAST_NORM,至于nd中mnt和dentry成员,在这个场景里还是前面设置的值,并无变化。
这样一圈下来,只是用nd记录下相关信息,实际的目录建立工作并没有真正展开,但是前面所做的工作却为接下来建立新的节点收集了必要的信息。
好,到此为止真正建立新目录节点的工作将会展开,这是由函数lookup_create来完成的,调用这个函数时会传入两个参数:
lookup_create(&nd,1);其中参数nd便是前面提到的变量,参数1表明要建立一个新目录。
这里的大体过程是:
新分配了一个structdentry结构的内存空间,用于记录dev目录所对应的信息,该dentry结构将会挂接到其父目录中,也就是图3中"/"目录对应的dentry结构中,由链表实现这一关系。
接下来会再分配一个structinode结构。
Inode中的i_sb和dentry中的d_sb分别都指向图3中的sb,这样看来,在同一文件系统下建立新的目录时并不需要重新分配一个超级块结构,因为毕竟它们都属于同一文件系统,因此一个文件系统只对应一个超级块。
这样,当调用sys_mkdir成功地在VFS的目录树中新建立一个目录"/dev"之后,在图3的基础上,新的数据结构之间的关系便如图4所示。
图4中颜色较深的两个矩形块new_inode和new_entry便是在sys_mkdir()函数中新分配的内存结构,至于图中的mnt,sb,dentry,inode等结构,仍为图3中相应的数据结构,其相互之间的链接关系不变(图中为避免过多的链接曲线,忽略了一些链接关系,如mnt和sb,dentry之间的链接,读者可在图3的基础上参看图4)。
需要强调一点的是,既然rootfs文件系统被mount到了VFS树上,那么它在sys_mkdir的过程中必然会参与进来,事实上在整个过程中,rootfs文件系统中的ramfs_mkdir、ramfs_lookup等函数都曾被调用过。
图4:
在VFS树中新建一目录"dev"
回页首
6.在VFS树中挂载文件系统
在本节中,将描述在VFS的目录树中向其中某个目录(安装点mountpoint)上挂载(mount)一个文件系统的过程。
这一过程可简单描述为:
将某一设备(dev_name)上某一文件系统(file_system_type)安装到VFS目录树上的某一安装点(dir_name)。
它要解决的问题是:
将对VFS目录树中某一目录的操作转化为具体安装到其上的实际文件系统的对应操作。
比如说,如果将hda2上的根文件系统(假设文件系统类型为ext2)安装到了前一节中新建立的"/dev"目录上(此时,"/dev"目录就成为了安装点),那么安装成功之后应达到这样的目的,即:
对VFS文件系统的"/dev"目录执行"ls"指令,该条指令应能列出hda2上ext2文件系统的根目录下所有的目录和文件。
很显然,这里的关键是如何将对VFS树中"/dev"的目录操作指令转化为安装在其上的ext2这一实际文件系统中的相应指令。
所以,接下来的叙述将抓住如何转化这一核心问题。
在叙述之前,读者不妨自己设想一下Linux系统会如何解决这一问题。
记住:
对目录或文件的操作将最终由目录或文件所对应的inode结构中的i_op和i_fop所指向的函数表中对应的函数来执行。
所以,不管最终解决方案如何,都可以设想必然要通过将对"/dev"目录所对应的inode中i_op和i_fop的调用转换到hda2上根文件系统ext2中根目录所对应的inode中i_op和i_fop的操作。
初始过程由sys_mount()系统调用函数发起,该函数原型声明如下:
asmlinkagelongsys_mount(char*dev_name,char*dir_name,char*type,
unsignedlongflags,void*data);
其中,参数char*type为标识将要安装的文件系统类型字符串,对于ext2文件系统而言,就是"ext2"。
参数flags为安装时的模式标识数,和接下来的data参数一样,本文不将其做为重点。
为了帮助读者更好地理解这一过程,笔者用一个具体的例子来说明:
我们准备将来自主硬盘第2分区(hda2)上的ext2文件系统安装到前面创建的"/dev"目录中。
那么对于sys_mount()函数的调用便具体为:
sys_mount("hda2","/dev","ext2",…);
该函数在将这些来自用户内存空间(userspace)的参数拷贝到内核空间后,便调用do_mount()函数开始真正的安装文件系统的工作。
同样,为了便于叙述和讲清楚主流程,接下来的说明将不严格按照具体的函数调用细节来进行。
do_mount()函数会首先调用path_lookup()函数来得到安装点的相关信息,如同创建目录过程中叙述的那样,该安装点的信息最终记录在structnameidata类型的一个变量当中,为叙述方便,记该变量为nd。
在本例中当path_lookup()函数返回时,nd中记录的信息如下:
nd.entry=new_entry;nd.mnt=mnt;这里的变量如图3和4中所示。
然后,do_mount()函数会根据调用参数flags来决定调用以下四个函数之一:
do_remount()、do_loopback()、do_move_mount()、do_add_mount()。
在我们当前的例子中,系统会调用do_add_mount()函数来向VFS树中安装点"/dev"安装一个实际的文件系统。
在do_add_mount()中,主要完成了两件重要事情:
一是获得一个新的安装区域块,二是将该新的安装区域块加入了安装系统链表。
它们分别是调用do_kern_mount()函数和graft_tree()函数来完成的。
这里的描述可能有点抽象,诸如安装区域块、安装系统链表等,不过不用着急,因为它们都是笔者自己定义出来的概念,等一下到后面会有专门的图表解释,到时便会清楚。
do_kern_mount()函数要做的事情,便是建立一新的安装区域块,具体的内容在前面的章节VFS目录树的建立中已经叙述过,这里不再赘述。
graft_tree()函数要做的事情便是将do_kern_mount()函数返回的一structvfsmount类型的变量加入到安装系统链表中,同时graft_tree()还要将新分配的structvfsmount类型的变量加入到一个hash表中,其目的我们将会在以后看到。
这样,当do_kern_mount()函数返回时,在图4的基础上,新的数据结构间的关系将如图5所示。
其中,红圈区域里面的数据结构便是被称做安装区域块的东西,其中不妨称e2_mnt为安装区域块的指针,蓝色箭头曲线即构成了所谓的安装系统链表。
在把这些函数调用后形成的数据结构关系理清楚之后,让我们回到本章节开始提到的问题,即将ext2文件系统安装到了"/dev"上之后,对该目录上的操作如何转化为对ext2文件系统相应的操作。
从图5上看到,对sys_mount()函数的调用并没有直接改变"/dev"目录所对应的inode(即图中的new_inode变量)结构中的i_op和i_fop指针,而且"/dev"所对应的dentry(即图中的new_dentry变量)结构仍然在VFS的目录树中,并没有被从其中隐藏起来,相应地,来自hda2上的ext2文件系统的根目录所对应的e2_entry也不是如当初笔者所想象地那样将VFS目录树中的new_dentry取而代之,那么这之间的转化到底是如何实现的呢?
请读者注意下面的这段代码:
while(d_mountpoint(dentry)&&__follow_down(&nd->mnt,&dentry));
这段代码在link_path_walk()函数中被调用,而link_path_walk()最终又会被path_lookup()函数调用,如果读者阅读过Linux关于文件系统部分的代码,应该知道path_lookup()函数在整个Linux繁琐的文件系统代码中属于一个重要的基础性的函数。
简单说来,这个函数用于解析文件路径名,这里的文件路径名和我们平时在应用程序中所涉及到的概念相同,比如在Linux的应用程序中open或read一个文件/home/windfly.cs时,这里的/home/windf
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- VFSVirtual Filesystem Switch