1 、前a 自从诞生以来QLinux p不断完善和普及,目前它已l成Z通用操作pȝ之一Q用得非常q泛Q它?Windows、UNIX 一起占据了操作pȝ领域几乎所有的市场份额。特别是在高性能计算领域QLinux 已经成ؓ一个占dC的操作系l,?005q?月全球TOP500 计算ZQ有 301 台部|的?Linux 操作pȝ。因此,研究和?Linux 已经成ؓ开发者的不可回避的问题了? 下面我们介绍一?Linux 内核中文?Cache 理的机制。本文以 2.6 pd内核为基准,主要讲述工作原理、数据结构和法Q不涉及具体代码? 2 操作pȝ和文?Cache 理 操作pȝ是计机上最重要的系lYӞ它负责管理各U物理资源,q向应用E序提供各种抽象接口以便其用这些物理资源。从应用E序的角度看Q操作系l提供了一个统一的虚拟机Q在该虚拟机中没有各U机器的具体l节Q只有进E、文件、地址I间以及q程间通信{逻辑概念。这U抽象虚拟机使得应用E序的开发变得相对容易:开发者只需与虚拟机中的各种逻辑对象交互Q而不需要了解各U机器的具体l节。此外,q些抽象的逻辑对象使得操作pȝ能够很容易隔dƈ保护各个应用E序? 对于存储讑֤上的数据Q操作系l向应用E序提供的逻辑概念是"文g"。应用程序要存储或访问数据时Q只需L者写"文g"的一l地址I间卛_Q而这个地址I间与存储设备上存储块之间的对应关系则由操作pȝl护? ?Linux 操作pȝ中,当应用程序需要读取文件中的数据时Q操作系l先分配一些内存,数据从存储讑֤d到这些内存中Q然后再数据分发给应用E序Q当需要往文g中写数据Ӟ操作pȝ先分配内存接收用h据,然后再将数据从内存写到磁盘上。文?Cache 理指的是对这些由操作pȝ分配Qƈ用来存储文g数据的内存的理?Cache 理的优劣通过两个指标衡量Q一?Cache 命中率,Cache 命中时数据可以直接从内存中获取,不再需要访问低速外设,因而可以显著提高性能Q二是有?Cache 的比率,有效 Cache 是指真正会被讉K到的 Cache ,如果有效 Cache 的比率偏低,则相当部分磁盘带宽会被浪费到d无用 Cache 上,而且无用 Cache 会间接导致系l内存紧张,最后可能会严重影响性能? 下面分别介绍文g Cache 理?Linux 操作pȝ中的C和作用、Linux 中文?Cache相关的数据结构、Linux 中文?Cache 的预d替换、Linux 中文?Cache 相关 API 及其实现? 2?文g Cache 的地位和作用 文g Cache 是文件数据在内存中的副本Q因此文?Cache 理与内存管理系l和文gpȝ都相养I一斚w文g Cache 作ؓ物理内存的一部分Q需要参与物理内存的分配回收q程Q另一斚w文g Cache 中的数据来源于存储设备上的文Ӟ需要通过文gpȝ与存储设备进行读写交互。从操作pȝ的角度考虑Q文?Cache 可以看做是内存管理系l与文gpȝ之间的联pȝ带。因此,文g Cache 理是操作系l的一个重要组成部分,它的性能直接影响着文gpȝ和内存管理系l的性能? ?描述?Linux 操作pȝ中文?Cache 理与内存管理以及文件系l的关系C意图。从图中可以看到Q在 Linux 中,具体文gpȝQ如 ext2/ext3、jfs、ntfs {,负责在文?Cache和存储设备之间交换数据,位于具体文gpȝ之上的虚拟文件系lVFS负责在应用程序和文g Cache 之间通过 read/write {接口交换数据,而内存管理系l负责文?Cache 的分配和回收Q同时虚拟内存管理系l?VMM)则允许应用程序和文g Cache 之间通过 memory map的方式交换数据。可见,?Linux pȝ中,文g Cache 是内存管理系l、文件系l以及应用程序之间的一个联pLU? ![]() ?Linux 的实CQ文?Cache 分ؓ两个层面Q一?Page CacheQ另一?Buffer CacheQ每一?Page Cache 包含若干 Buffer Cache。内存管理系l和 VFS 只与 Page Cache 交互Q内存管理系l负责维护每?Page Cache 的分配和回收Q同时在使用 memory map 方式讉K时负责徏立映;VFS 负责 Page Cache 与用L间的数据交换。而具体文件系l则一般只?Buffer Cache 交互Q它们负责在外围存储讑֤?Buffer Cache 之间交换数据。Page Cache、Buffer Cache、文件以及磁盘之间的关系如图 2 所C,Page l构?buffer_head 数据l构的关pd?3 所C。在上述两个图中Q假定了 Page 的大是 4KQ磁盘块的大是 1K。本文所讲述的,主要是指?Page Cache 的管理? ?Linux 内核中,文g的每个数据块最多只能对应一?Page Cache ,它通过两个数据l构来管理这?Cache ,一个是 radix treeQ另一个是双向链表。Radix tree 是一U搜索树QLinux 内核利用q个数据l构来通过文g内偏Ud速定?Cache ,?4 ?radix tree的一个示意图Q该 radix tree 的分叉ؓ4(22)Q树高ؓ4Q用来快速定?位文件内偏移。Linux(2.6.7) 内核中的分叉?64(26)Q树高ؓ 6(64位系l?或?11(32位系l?Q用来快速定?32 位或?64 位偏U,radix tree 中的每一个叶子节Ҏ向文件内相应偏移所对应的CacheV? 另一个数据结构是双向链表QLinux内核为每一片物理内存区?zone)l护active_list?inactive_list两个双向链表Q这两个list主要用来实现物理内存的回收。这两个链表上除了文件Cache之外Q还包括其它匿名 (Anonymous)内存Q如q程堆栈{? ![]() ![]() ![]() 4 、文件Cache的预d替换
Linux内核中文仉ȝ法的具体q程是这LQ对于每个文件的W一个读hQ系l读入所h的页面ƈd紧随其后的少数几个页?不少于一个页面,通常是三个页?Q这时的预读UCؓ同步预读。对于第二次读请求,如果所读页面不在Cache中,即不在前ơ预ȝgroup中,则表明文件访问不是顺序访问,pȝl箋采用同步预读Q如果所读页面在Cache中,则表明前ơ预d中,操作pȝ把预读group扩大一倍,q让底层文gpȝd group中剩下尚不在Cache中的文g数据块,q时的预ȝ为异步预诅R无论第二次读请求是否命中,pȝ都要更新当前预读group的大。此外,pȝ中定义了一个windowQ它包括前一ơ预ȝgroup和本ơ预ȝgroup。Q何接下来的读h都会处于两种情况之一Q第一U情冉|所h的页面处于预读window中,q时l箋q行异步预读q更新相应的window和groupQ第二种情况是所h的页面处于预读window之外Q这时系l就要进行同步预dƈ重置相应的window和group。图5是Linux内核预读机制的一个示意图Q其中a是某ơ读操作之前的情况,b是读操作所h面不在window中的情况Q而c是读操作所h面在window中的情况? Linux内核中文件Cache替换的具体过E是q样的:刚刚分配的Cachew入到inactive_list头部Qƈ其状态设|ؓactiveQ当内存不够需要回收CacheӞpȝ首先从尾部开始反向扫描active_listq将状态不是referenced的项铑օ?inactive_list的头部,然后pȝ反向扫描inactive_listQ如果所扫描的项的处于合适的状态就回收该项Q直到回收了_数目?CacheVCache替换法如图6的算法描qC码所C? ![]() Mark_Accessed(b) {
if b.state==(UNACTIVE && UNREFERENCE) b.state = REFERENCE else if b.state == (UNACTIVE && REFERENCE) { b.state = (ACTIVE && UNREFERENCE) Add X to tail of active_list } else if b.state == (ACTIVE && UNREFERENCE) b.state = (ACTIVE && REFERENCE) } Reclaim() { if active_list not empty and scan_num ? Linux的Cache替换法描述 5 、文件Cache相关API及其实现 Linux内核中与文gCache操作相关的API有很多,按其使用方式可以分成两类Q一cL以拷贝方式操作的相关接口Q如read/write/sendfile{,其中sendfile?.6pd的内怸已经不再支持Q另一cL以地址映射方式操作的相x口,?mmap{? W一U类型的API在不同文件的Cache之间或者Cache与应用程序所提供的用L间buffer之间拯数据Q其实现原理如图7所C? ![]() W二U类型的APICacheҎ到用户I间Q得应用程序可以像使用内存指针一栯问文ӞMemory map讉KCache的方式在内核中是采用h面机制实现的,其工作过E如?所C?/div>
![]() 首先Q应用程序调用mmapQ图?Q,陷入到内怸后调用do_mmap_pgoffQ图?Q。该函数从应用程序的地址I间中分配一D区域作为映的内存地址Qƈ使用一个VMAQvm_area_structQ结构代表该区域Q之后就q回到应用程序(图中3Q。当应用E序讉Kmmap所q回的地址指针Ӟ图中4Q,׃虚实映射未建立Q会触发~页中断Q图?Q。之后系l会调用~页中断处理函数Q图?Q,在缺中断处理函CQ内栔R过相应区域的VMAl构判断区域属于文g映射Q于是调用具体文件系l的接口d相应的Page Cache(图中7??Q,q填写相应的虚实映射表。经q这些步骤之后,应用E序可以正常访问相应的内存区域了?
6 、小l? 文gCache理是Linux操作pȝ的一个重要组成部分,同时也是研究领域一个很热门的研I方向。目前,Linux内核在这个方面的工作集中在开发更有效的Cache替换法上,如LIRS(其变UClockPro)、ARC{?span class=postbody> |
本文阐述 Linux 中的文gpȝ部分Q源代码来自Z IA32 ?2.4.20 内核。M上说 Linux 下的文gpȝ主要可分Z大块Q一是上层的文gpȝ的系l调用,二是虚拟文gpȝ VFS(Virtual Filesystem Switch)Q三是挂载到 VFS 中的各实际文件系l,例如 ext2Qjffs {。本文侧重于通过具体的代码分析来解释 Linux 内核?VFS 的内在机Ӟ在这q程中会涉及C层文件系l调用和下层实际文gpȝ的如何挂载。文章试图从一个比较高的角度来解释 Linux 下的 VFS 文gpȝ机制?/p>
1. 摘要
本文阐述 Linux 中的文gpȝ部分Q源代码来自Z IA32 ?2.4.20 内核。M上说 Linux 下的文gpȝ主要可分Z大块Q一是上层的文gpȝ的系l调用,二是虚拟文gpȝ VFS(Virtual Filesystem Switch)Q三是挂载到 VFS 中的各实际文件系l,例如 ext2Qjffs {。本文侧重于通过具体的代码分析来解释 Linux 内核?VFS 的内在机Ӟ在这q程中会涉及C层文件系l调用和下层实际文gpȝ的如何挂载。文章试图从一个比较高的角度来解释 Linux 下的 VFS 文gpȝ机制Q所以在叙述中更侧重于整个模块的主脉l,而不拘惔于细节,同时配有若干张插图,以帮助读者理解?/p>
相对来说QVFS 部分的代码比较繁琐复杂,希望读者在阅读完本文之后,能对 Linux 下的 VFS 整体q作机制有个清楚的理解。徏议读者在阅读本文前,先尝试着自己阅读一下文件系l的源代码,以便建立?Linux 下文件系l最基本的概念,比如臛_应熟?super block, dentry, inodeQvfsmount {数据结构所表示的意义,q样再来阅读本文以便加深理解?/p>
2. VFS 概述
VFS 是一UY件机Ӟ也许U它?Linux 的文件系l管理者更切点,与它相关的数据结构只存在于物理内存当中。所以在每次pȝ初始化期_Linux 都首先要在内存当中构造一?VFS 的目录树(?Linux 的源代码里称之ؓ namespace)Q实际上便是在内存中建立相应的数据结构。VFS 目录树在 Linux 的文件系l模块中是个很重要的概念Q希望读者不要将其与实际文gpȝ目录树淆,在笔者看来,VFS 中的各目录其主要用途是用来提供实际文gpȝ的挂载点Q当然在 VFS 中也会涉及到文gU的操作Q本文不阐述q种情况。下文提到目录树或目录,如果不特别说明,均指 VFS 的目录树或目录。图 1 是一U可能的目录树在内存中的影像Q?/p>
?1QVFS 目录树结?/p>
3. 文gpȝ的注?/p>
q里的文件系l是指可能会被挂载到目录树中的各个实际文件系l,所谓实际文件系l,x指VFS 中的实际操作最l要通过它们来完成而已Qƈ不意味着它们一定要存在于某U特定的存储讑֤上。比如在W者的 Linux 机器下就注册?"rootfs"?proc"?ext2"?sockfs" {十几种文gpȝ?/p>
3.1 数据l构
?Linux 源代码中Q每U实际的文gpȝ用以下的数据l构表示Q?/p>
struct file_system_type {
const char *name;
int fs_flags;
struct super_block *(*read_super) (struct super_block *, void *, int);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
};
注册q程实际上将表示各实际文件系l的 struct file_system_type 数据l构的实例化Q然后Ş成一个链表,内核中用一个名?file_systems 的全局变量来指向该链表的表头?/p>
3.2 注册 rootfs 文gpȝ
在众多的实际文gpȝ中,之所以单独介l?rootfs 文gpȝ的注册过E,实在是因文gpȝ VFS 的关pdq密切,如果?ext2/ext3 ?Linux 的本土文件系l,那么 rootfs 文gpȝ则是 VFS 存在的基。一般文件系l的注册都是通过 module_init 宏以?do_initcalls() 函数来完?读者可通过阅读module_init 宏的声明?archi386vmlinux.lds 文g来理解这一q程)Q但?rootfs 的注册却是通过 init_rootfs() q一初始化函数来完成Q这意味着 rootfs 的注册过E是 Linux 内核初始化阶D不可分割的一部分?/p>
init_rootfs() 通过调用 register_filesystem(&rootfs_fs_type) 函数来完?rootfs 文gpȝ注册的,其中rootfs_fs_type 定义如下Q?/p>
struct file_system_type rootfs_fs_type = {
name: "rootfs",
read_super: ramfs_read_super,
fs_flags: FS_NOMOUNT|FS_LITTER,
owner: THIS_MODULE,
}
注册之后?file_systems 链表l构如下?所C:
?2: file_systems 链表l构
4. VFS 目录树的建立
既然是树Q所以根是其赖以存在的基Q本节阐q?Linux 在初始化阶段是如何徏立根l点的,?"/"目录。这其中会包括挂?rootfs 文gpȝ到根目录 "/" 的具体过E。构造根目录的代码是?init_mount_treeQ) 函数 Qfs amespace.cQ?中?/p>
首先Qinit_mount_tree() 函数会调?do_kern_mount("rootfs", 0, "rootfs", NULL) 来挂载前面已l注册了?rootfs 文gpȝ。这看v来似乎有点奇怪,因ؓҎ前面的说法,g是应该先有挂载目录,然后再在其上挂蝲相应的文件系l,然而此?VFS gq没有徏立其根目录。没关系Q这是因里我们调用的?do_kern_mount()Q这个函数内部自然会创徏我们最兛_也是最关键的根目录(?Linux 中,目录对应的数据结构是 struct dentry)?/p>
在这个场景里Qdo_kern_mount() 做的工作主要是:
1Q调?alloc_vfsmnt() 函数在内存里甌了一块该cd的内存空_struct vfsmount *mntQ,q初始化光分成员变量?/p>
2) 调用 get_sb_nodevQ) 函数在内存中分配一个超U块l构 (struct super_block) sbQƈ初始化其部分成员变量Q将成员 s_instances 插入?rootfs 文gpȝcdl构中的 fs_supers 指向的双向链表中?/p>
3) 通过 rootfs 文gpȝ中的 read_super 函数指针调用 ramfs_read_super() 函数。还记得当初注册rootfs 文gpȝӞ其成?read_super 指针指向?ramfs_read_super() 函数Q参见图2.
4) ramfs_read_super() 函数调用 ramfs_get_inode() 在内存中分配了一?inode l构 (struct inode) inodeQƈ初始化其部分成员变量Q其中比较重要的?i_op、i_fop ?i_sbQ?/p>
inode->i_op = &ramfs_dir_inode_operations;
inode->i_fop = &dcache_dir_ops;
inode->i_sb = sb;
q得将来通过文gpȝ调用?VFS 发v的文件操作等指o被 rootfs 文gpȝ中相应的函数接口所接管?/p>
?
5) ramfs_read_super() 函数在分配和初始化了 inode l构之后Q会调用 d_alloc_root() 函数来ؓ VFS的目录树建立起关键的根目?(struct dentry)dentryQƈ?dentry 中的 d_sb 指针指向 sbQd_inode 指针指向 inode?/p>
6) ?mnt 中的 mnt_sb 指针指向 sbQmnt_root ?mnt_mountpoint 指针指向 dentryQ?mnt_parent指针则指向自w?/p>
q样Q当 do_kern_mount() 函数q回Ӟ以上分配出来的各数据l构?rootfs 文gpȝ的关pd如上?3 所C。图?mnt、sb、inode、dentry l构块下方的数字表示它们在内存里被分配的先后序。限于篇q的原因Q各l构中只l出了部分成员变量,读者可以对照源代码Ҏ图中所C按囄骥,以加q解?/p>
最后,init_mount_tree() 函数会ؓpȝ最开始的q程(?init_task q程)准备它的q程数据块中的namespace 域,主要目的是将 do_kern_mount() 函数中徏立的 mnt ?dentry 信息记录在了 init_task q程的进E数据块中,q样所有以后从 init_task q程 fork 出来的进E也都先天地l承了这一信息Q在后面用sys_mkdir ?VFS 中创Z个目录的q程中,我们可以看到q里Z么要q样做。ؓq程建立 namespace 的主要代码如下:
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
list_add(&mnt->mnt_list, &namespace->list); //mnt is returned by do_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 信息记录在了当前q程?fsl构中?/p>
以上讲了一大堆数据l构的来历,其实最l目的不q是要在内存中徏立一?VFS 目录树而已Q更切地说Q?init_mount_tree() q个函数?VFS 建立了根目录 "/"Q而一旦有了根Q那么这|可以发展壮大,比如可以通过pȝ调用 sys_mkdir 在这|上徏立新的叶子节点等Q所以系l设计者又?rootfs 文gpȝ挂蝲Cq棵树的根目录上。关?rootfs q个文gpȝQ读者如果看一下前面图 2 中它的file_system_type l构Q会发现它的一个成员函数指?read_super 指向的是 ramfs_read_superQ单从这个函数名UC?ramfsQ读者大概能猜测个文件所涉及的文件操作都是针对内存中的数据对象,事实上也的确如此。从另一个角度而言Q因?VFS 本n是内存中的一个数据对象,所以在其上的操作仅限于内存Q那也是非常合乎逻辑的事。在接下来的章节中,我们会用一个具体的例子来讨论如何利?rootfs所提供的函树ؓ VFS 增加一个新的目录节炏V?/p>
VFS 中各目录的主要用途是Z后挂载文件系l提供挂载点。所以真正的文g操作q是要通过挂蝲后的文gpȝ提供的功能接口来q行?/p>
5. VFS 下目录的建立
Z更好地理?VFSQ下面我们用一个实际例子来看看 Linux 是如何在 VFS 的根目录下徏立一个新的目?"/dev" 的?/p>
要在 VFS 中徏立一个新的目录,首先我们得对该目录进行搜索,搜烦的目的是扑ֈ要建立的目录其父目录的相关信息Q因?皮之不存Q毛焉?。比如要建立目录 /home/ricardQ那么首先必L目录路径q行逐层搜烦Q本例中先从根目录找P然后在根目录下找到目?homeQ然后再往下,便是要新建的目录?ricardQ那么前面讲得要先对目录搜烦Q在该例中便是要扑ֈ ricard q个新目录的父目录,也就?home 目录所对应的信息?/p>
当然Q如果搜索的q程中发现错误,比如要徏目录的父目录q不存在Q或者当前进Eƈ无相应的权限{等Q这U情늳l必然会调用相关q程q行处理Q对于此U情况,本文略过不提?/p>
Linux 下用pȝ调用 sys_mkdir 来在 VFS 目录树中增加新的节点。同时ؓ配合路径搜烦Q引入了下面一个数据结构:
struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr last;
unsigned int flags;
int last_type;
};
q个数据l构在\径搜索的q程中用来记录相关信息,LcM"路标"的作用。其中前两项中的 dentry记录的是要徏目录的父目录的信息,mnt 成员接下来会解释到。后三项记录的是所查找路径的最后一个节?卛_建目录或文g)的信息?现在为徏立目?"/dev" 而调?sys_mkdir("/dev", 0700)Q其中参?0700 我们不去它Q它只是限定要建立的目录的某种模式。sys_mkdir 函数首先调用 path_lookup("/dev", LOOKUP_PARENT, &nd)Q来对\径进行查找,其中 nd ?struct nameidata nd 声明的变量。在接下来的叙述中,因ؓ函数调用关系的繁琐,ZH出q程ȝQ将不再严格按照函数的调用关pLq行描述?/p>
path_lookup 发现 "/dev" 是以 "/" 开_所以它从当前进E的根目录开始往下查找,具体代码如下Q?/p>
nd->mnt = mntget(current->fs->rootmnt);
nd->dentry = dget(current->fs->root);
记得?init_mount_tree() 函数的后半段曄新建立?VFS 根目录相关信息记录在?init_task q程的进E数据块中,那么在这个场景里Qnd->mnt 便指向了?3 ?mnt 变量Qnd->dentry 便指向了?3 中的 dentry 变量?/p>
然后调用函数 path_walk 接着往下查找,扑ֈ最后通过变量 nd q回的信息是 nd.last.name="dev"Qnd.last.len=3Qnd.last_type=LAST_NORMQ至?nd ?mnt ?dentry 成员Q在q个场景里还是前面设|的|q无变化。这样一圈下来,只是?nd 记录下相关信息,实际的目录徏立工作ƈ没有真正展开Q但是前面所做的工作却ؓ接下来徏立新的节Ҏ集了必要的信息?/p>
好,到此为止真正建立新目录节点的工作会展开Q这是由函数 lookup_create 来完成的Q调用这个函数时会传入两个参敎ͼlookup_create(&nd, 1)Q其中参?nd 便是前面提到的变量,参数1表明要徏立一个新目录?/p>
q里的大体过E是Q新分配了一?struct dentry l构的内存空_用于记录 dev 目录所对应的信息,该dentry l构会挂接到其父目录中Q也是?3 ?"/" 目录对应?dentry l构中,由链表实现这一关系。接下来会再分配一?struct inode l构。Inode 中的 i_sb ?dentry 中的 d_sb 分别都指向图 3 中的 sbQ这L来,在同一文gpȝ下徏立新的目录时q不需要重新分配一个超U块l构Q因为毕竟它们都属于同一文gpȝQ因此一个文件系l只对应一个超U块?/p>
q样Q当调用 sys_mkdir 成功地在 VFS 的目录树中新建立一个目?"/dev" 之后Q在?3 的基上,新的数据l构之间的关pM如图 4 所C。图 4 中颜色较q两个矩Ş?new_inode ?new_entry 便是在sys_mkdir() 函数中新分配的内存结构,至于图中?mnt,sb,dentry,inode {结构,仍ؓ?3 中相应的数据l构Q其怺之间的链接关pM?图中为避免过多的链接曲线Q忽略了一些链接关p,?mnt ?sb,dentry之间的链接,读者可在图 3 的基上参看图 4)?/p>
需要强调一点的是,既然 rootfs 文gpȝ?mount C VFS 树上Q那么它?sys_mkdir 的过E中必然会参与进来,事实上在整个q程中,rootfs 文gpȝ中的 ramfs_mkdir、ramfs_lookup {函数都曾被调用q?/p>
?4: ?VFS 树中新徏一目录 "dev"
6. ?VFS 树中挂蝲文gpȝ
在本节中Q将描述?VFS 的目录树中向其中某个目录(安装?mount point)上挂?mount)一个文件系l的q程?/p>
q一q程可简单描qCؓQ将某一讑֤(dev_name)上某一文gpȝ(file_system_type)安装到VFS目录树上的某一安装?dir_name)。它要解决的问题是:对 VFS 目录树中某一目录的操作{化ؓ具体安装到其上的实际文gpȝ的对应操作。比如说Q如果将 hda2 上的Ҏ件系l?假设文gpȝcd?ext2)安装C前一节中新徏立的 "/dev" 目录?此时Q?/dev" 目录成Z安装?Q那么安装成功之后应辑ֈq样的目的,卻I?VFS 文gpȝ?"/dev" 目录执行 "ls" 指oQ该条指令应能列?hda2 ?ext2 文gpȝ的根目录下所有的目录和文件。很昄Q这里的关键是如何将?VFS 树中 "/dev" 的目录操作指令{化ؓ安装在其上的 ext2 q一实际文gpȝ中的相应指o。所以,接下来的叙述抓住如何{化这一核心问题。在叙述之前Q读者不妨自p想一?Linux pȝ会如何解册一问题。记住:对目录或文g的操作将最l由目录或文件所对应?inode l构中的 i_op ?i_fop 所指向的函数表中对应的函数来执行。所以,不管最l解x案如何,都可以设惛_然要通过对 "/dev" 目录所对应?inode ?i_op ?i_fop 的调用{换到 hda2 上根文gpȝ ext2 中根目录所对应?inode ?i_op ?i_fop 的操作?/p>
初始q程?sys_mount() pȝ调用函数发vQ该函数原型声明如下Q?/p>
asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type,
unsigned long flags, void * data);
其中Q参?char *type 为标识将要安装的文gpȝcd字符Ԍ对于 ext2 文gpȝ而言Q就?ext2"。参?flags 为安装时的模式标识数Q和接下来的 data 参数一P本文不将其做为重炏V?/p>
Z帮助读者更好地理解q一q程Q笔者用一个具体的例子来说明:我们准备来自主盘W?2 分区(hda2)上的 ext2 文gpȝ安装到前面创建的 "/dev" 目录中。那么对?sys_mount() 函数的调用便具体为:
sys_mount("hda2","/dev ","ext2",…)Q?/code>
该函数在这些来自用户内存空?user space)的参数拷贝到内核I间后,便调?do_mount() 函数开始真正的安装文gpȝ的工作。同PZ便于叙述和讲清楚LE,接下来的说明不严格按照具体的函数调用细节来q行?/p>
do_mount() 函数会首先调?path_lookup() 函数来得到安装点的相关信息,如同创徏目录q程中叙q的那样Q该安装点的信息最l记录在 struct nameidata cd的一个变量当中,为叙q方便,记该变量为nd。在本例中当 path_lookup() 函数q回Ӟnd 中记录的信息如下Qnd.entry = new_entry; nd.mnt = mnt; q里的变量如?3 ?4 中所C?/p>
然后Qdo_mount() 函数会根据调用参?flags 来决定调用以下四个函C一Qdo_remount()?do_loopback()、do_move_mount()、do_add_mount()?/p>
在我们当前的例子中,pȝ会调?do_add_mount() 函数来向 VFS 树中安装?"/dev " 安装一个实际的文gpȝ。在 do_add_mount() 中,主要完成了两仉要事情:一是获得一个新的安装区域块Q二是将该新的安装区域块加入了安装系l链表。它们分别是调用 do_kern_mount() 函数?graft_tree() 函数来完成的。这里的描述可能有点抽象Q诸如安装区域块、安装系l链表等Q不q不用着急,因ؓ它们都是W者自己定义出来的概念Q等一下到后面会有专门的图表解释,到时便会清楚?/p>
do_kern_mount() 函数要做的事情,便是建立一新的安装区域块,具体的内容在前面的章?VFS 目录树的建立中已l叙q过Q这里不再赘q?/p>
graft_tree() 函数要做的事情便是将 do_kern_mount() 函数q回的一 struct vfsmount cd的变量加入到安装pȝ链表中,同时 graft_tree() q要新分配?struct vfsmount cd的变量加入到一个hash表中Q其目的我们会在以后看到?/p>
q样Q当 do_kern_mount() 函数q回Ӟ在图 4 的基上,新的数据l构间的关系如?5 所C。其中,U圈区域里面的数据结构便是被U做安装区域块的东西Q其中不妨称 e2_mnt 为安装区域块的指针,蓝色头曲线x成了所谓的安装pȝ链表?/p>
在把q些函数调用后Ş成的数据l构关系理清楚之后,让我们回到本章节开始提到的问题Q即?ext2 文gpȝ安装C "/dev " 上之后,对该目录上的操作如何转化为对 ext2 文gpȝ相应的操作。从?5上看刎ͼ?sys_mount() 函数的调用ƈ没有直接改变 "/dev " 目录所对应?inode (卛_中的 new_inode变量)l构中的 i_op ?i_fop 指针Q而且 "/dev " 所对应?dentry(卛_中的 new_dentry 变量)l构仍然?VFS 的目录树中,q没有被从其中隐藏v来,相应圎ͼ来自 hda2 上的 ext2 文gpȝ的根目录所对应?e2_entry 也不是如当初W者所惌地那样将 VFS 目录树中?new_dentry 取而代之,那么q之间的转化到底是如何实现的呢?
误者注意下面的q段代码Q?/p>
while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry));
q段代码?link_path_walk() 函数中被调用Q?link_path_walk() 最l又会被 path_lookup() 函数调用Q如果读者阅读过 Linux 关于文gpȝ部分的代码,应该知道 path_lookup() 函数在整?Linux J琐的文件系l代码中属于一个重要的基础性的函数。简单说来,q个函数用于解析文g路径名,q里的文件\径名和我们^时在应用E序中所涉及到的概念相同Q比如在 Linux 的应用程序中 open ?read 一个文?/home/windfly.cs Ӟq里?/home/windfly.cs 是文g路径名,path_lookup() 函数的责d是对文g路径名中q行搜烦Q直到找到目标文件所属目录所对应?dentry 或者目标直接就是一个目录,W者不惛_有限的篇q里详细解释q个函数Q读者只要记?path_lookup() 会返回一个目标目录即可?/p>
上面的代码非常地不v|以至于初ơ阅L件系l的代码时经怼忽略掉它Q但是前文所提到?VFS 的操作到实际文gpȝ操作的{化却是由它完成的Q对 VFS 中实现的文gpȝ的安装可谓功不可没。现在让我们仔细剖析一下该D代码: d_mountpoint(dentry) 的作用很单,它只是返?dentry ?d_mounted 成员变量的倹{这里的dentry 仍然q是 VFS 目录树上的东ѝ如?VFS 目录树上某个目录被安装过一ơ,那么该gؓ 1。对VFS 中的一个目录可q行多次安装Q后面会有例子说明这U情c在我们的例子中Q?/dev" 所对应的new_dentry ?d_mounted=1Q所?while 循环中第一个条件满뀂下面再来看__follow_down(&nd->mnt, &dentry)?/p>
?5Q安?ext2 cdҎ件系l到 "/dev " 目录?/p>

码做了什么?到此我们应该CQ这?nd 中的 dentry 成员是?5 中的 new_dentryQnd 中的 mnt成员是?5 中的 mntQ所以我们现在可以把 __follow_down(&nd->mnt, &dentry) 改写成__follow_down(&mnt, &new_dentry)Q接下来我们?__follow_down() 函数的代码改?只是d掉一些不太相关的代码Qƈ且ؓ了便于说明,在部分代码行前加上了序号)如下Q?/p>
static inline int __follow_down(struct vfsmount **mnt, struct dentry **dentry)
{
struct vfsmount *mounted;
[1] mounted = lookup_mnt(*mnt, *dentry);
if (mounted) {
[2] *mnt = mounted;
[3] *dentry = mounted->mnt_root;
return 1;
}
return 0;
}
代码行[1]中的 lookup_mnt() 函数用于查找一?VFS 目录树下某一目录最q一ơ被 mount 时的安装区域块的指针Q在本例中最l会q回?5 中的 e2_mnt。至于查扄原理Q这里粗略地描述一下。记得当我们在安?ext2 文gpȝ?"/dev" Ӟ在后期会调用 graft_tree() 函数Q在q个函数里会把图 5 中的安装区域块指?e2_mnt 挂到一 hash ?Linux 2.4.20源代码中UC?mount_hashtable)中的某一,而该的键值就是由被安装点所对应?dentry(本例中ؓ new_dentry)?mount(本例中ؓ mnt)所共同产生Q所以自然地Q当我们知道 VFS 树中某一 dentry 被安装过(?dentry 变成Z安装?Q而要L扑օ最q一ơ被安装的安装区域块指针Ӟ同样p安装Ҏ对应?dentry ?mount 来生一键|以此值去索引 mount_hashtableQ自然可扑ֈ该安装点对应的安装区域块指针形成的链表的头指针,然后遍历该链表,当发现某一安装区域块指针,Cؓ pQ满以下条件时Q?/p>
(p->mnt_parent == mnt && p->mnt_mountpoint == dentry)
P 便ؓ该安装点所对应的安装区域块指针。当扑ֈ该指针后Q便?nd 中的 mnt 成员换成该安装区域块指针Q同时将 nd 中的 dentry 成员换成安装区域块中?dentry 指针。在我们的例子中Qe2_mnt->mnt_root成员指向 e2_dentryQ也是 ext2 文gpȝ?"/" 目录。这P?path_lookup() 函数搜烦?"/dev"Ӟnd 中的 dentry 成员?e2_dentryQ而不再是原来?new_dentryQ同?mnt 成员被换?e2_mntQ{化便在不知不觉中完成了?/p>
现在考虑一下对某一安装点多ơ安装的情况Q同样作Z子,我们假设?"/dev" 上安装完一?ext2文gpȝ后,再在其上安装一?ntfs 文gpȝ。在安装之前Q同样会对安装点所在的路径调用path_lookup() 函数q行搜烦Q但是这ơ由于在 "/dev" 目录上已l安装过?ext2 文gpȝQ所以搜索到最后,?nd q回的信息是Qnd.dentry = e2_dentry, nd.mnt = e2_mnt。由此可见,在第二次安装Ӟ安装点已l由 dentry 变成?e2_dentry。接下来Q同样地Q系l会再分配一个安装区域块Q假设该安装区域块的指针?ntfs_mntQ区域块中的 dentry ?ntfs_dentry。ntfs_mnt 的父指针指向了e2_mntQmnfs_mnt 中的 mnt_root 指向了代?ntfs 文gpȝ根目录的 ntfs_dentry。然后,pȝ通过 e2_dentry?e2_mnt 来生成一个新?hash 键|利用该g为烦引,?ntfs_mnt 加入?mount_hashtable 中,同时?e2_dentry 中的成员 d_mounted D定ؓ 1。这P安装q程便告l束?/p>
读者可能已l知道,对同一安装点上的最q一ơ安装会隐藏起前面的若干ơ安装,下面我们通过上述的例子解释一下该q程Q?/p>
在先后将 ext2 ?ntfs 文gpȝ安装?"/dev" 目录之后Q我们再调用 path_lookup() 函数来对"/dev" q行搜烦Q函数首先找?VFS 目录树下的安装点 "/dev" 所对应?dentry ?mntQ此时它发现dentry 成员中的 d_mounted ?1Q于是它知道已经有文件系l安装到了该 dentry 上,于是它通过 dentry ?mnt 来生成一?hash |通过该值来?mount_hashtable q行搜烦Q根据安装过E,它应该能扑ֈ e2_mnt 指针q返回之Q同时原先的 dentry 也已l被替换?e2_dentry。回头再看一下前面已l提到的下列代码Q?while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)); 当第一ơ@环结束后, nd->mnt 已经?e2_mntQ?dentry 则变?e2_dentry。此时由?e2_dentry 中的成员 d_mounted gؓ 1Q所?while 循环的第一个条件满I要l调?__follow_down() 函数Q这个函数前面已l剖析过Q当它返回后 nd->mnt 变成?ntfs_mntQdentry 则变成了 ntfs_dentry。由于此?ntfs_dentry 没有被安装过其他文gQ所以它的成?d_mounted 应该?0Q@环结束。对 "/dev" 发v?path_lookup() 函数最l返回了 ntfs 文gpȝ根目录所对应?dentry。这是Z?"/dev" 本n和安装在其上?ext2 都被隐藏的原因。如果此时对 "/dev" 目录q行一?ls 命oQ将q回安装上去?ntfs 文gpȝ根目录下所有的文g和目录?/p>
7. 安装Ҏ件系l?/p>
有了前面章节 5 的基Q理?Linux 下根文gpȝ的安装ƈ不困难,因ؓ不管怎么P安装一个文件系l到 VFS 中某一安装点的q程原理毕竟都是一L?/p>
q个q程大致是:首先要确定待安装?ext2 文gpȝ的来源,其次是确?ext2 文gpȝ?VFS中的安装点,然后便是具体的安装过E?/p>
关于W一问题QLinux 2.4.20 的内核另有一大堆的代码去解决Q限于篇q,W者不惛_q里d体说明这个过E,大概C它是要解军_哪里L要安装的文gpȝ的就可以了,q里我们不妨p安装的根文gpȝ来自于ȝ盘的W一分区 hda1.
关于W二个问题,Linux 2.4.20 的内核把来自?hda1 ?ext2 文gpȝ安装C VFS 目录树中?/root" 目录上。其实,?ext2 文gpȝ安装?VFS 目录树下的哪个安装点q不重要(VFS 的根目录除外)Q只要是q个安装点在 VFS 树中是存在的Qƈ且内核对它没有另外的用途。如果读者喜Ƣ,可以自己在 VFS 中创Z?"/Windows" 目录Q然后将 ext2 文gpȝ安装上去作ؓ来用户q程的根目录Q没有什么不可以的。问题的关键是要进E的根目录和当前工作目录讑֮好,因ؓ毕竟只用用户q程才去兛_现实的文件系l,要知道笔者的q篇E子可是要存到硬盘上ȝ?/p>
?Linux 下,讑֮一个进E的当前工作目录是通过pȝ调用 sys_chdir() q行的。初始化期间QLinux 在将 hda1 上的 ext2 文gpȝ安装C "/root" 上后Q通过调用 sys_chdir("/root") 当前进E,也就?init_task q程的当前工作目?pwd)讑֮?ext2 文gpȝ的根目录。记住此?init_taskq程的根目录仍然是图 3 中的 dentryQ也是 VFS 树的根目录,q显然是不行的,因ؓ以后 Linux 世界中的所有进E都p?init_task q程z出来Q无一例外地要l承该进E的根目录,如果是这P意味着用户q程从根目录搜烦某一目录Ӟ实际上是?VFS 的根目录开始的Q而事实上却是?ext2 的根文g开始搜索的。这个矛盄解决是靠了在调用?mount_root() 函数后,pȝ调用的下面两个函敎ͼ
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
其主要作用便是将 init_task q程的根目录转化成安装上ȝ ext2 文gpȝ的根目录。有兴趣的读者可以自行去研究q一q程?/p>
所以在用户I间下,更多地情冉|只能见到 VFS q棵大树的一Ӟ而且q是被安装过文gpȝ了的Q实际上对用L间来说还是不可见。我惻IVFS 更多地被内核用来实现自己的功能,q以pȝ调用的方式提供过用户q程使用Q至于在其上实现的不同文件系l的安装Q也只是其中的一个功能Ş了?/p>
8. l束?/p>
文gpȝ在整?Linux 的内怸h举轻重的地位,代码量也很复杂繁琐。但是因为其重要的地位,要想?Linux 的内核有比较深入的理解,必须要能过文gpȝq一兟뀂当焉d源代码便是其中最好的ҎQ本文试囄那些已经试着去阅读,但是目前有困惑的读者画一?VFS 文gpȝ的草图,希望能对读者有些许启发。但是想在如此有限的幅里去阐述清楚 Linux 中整个文件系l的来龙去脉Q是Ҏ不现实的。而且本文也只是侧重于剖析 VFS 的机Ӟ对于象具体的文gdQؓ提高效率而引入的各种 buffer,hash {内容以及文件系l的安全性方面,都没有提到。毕竟,本文只想帮助读者理清一个大体的脉络Q最l的理解与领悟,q得靠读者自己去潜心研究源代码。最后,Ҏ文相关的M问题或徏议,都欢q用 email 和笔者联pR?/p>