现在还做响应式网站吗,榆中建设投资有限公司网站,网站开发高级工程师专业,吉林网站备案《解析Linux中的VFS文件系统机制#xff08;根文件系统的由来#xff09;》 以及 《ARM Linux启动流程-根文件系统的加载》两篇文章都是好文章#xff0c;但是第一篇缺少了rootfs的由来#xff0c;让人觉得不能融会贯通#xff0c;缺点火候。
第一篇文章#xff1a;试图…《解析Linux中的VFS文件系统机制根文件系统的由来》 以及 《ARM Linux启动流程-根文件系统的加载》两篇文章都是好文章但是第一篇缺少了rootfs的由来让人觉得不能融会贯通缺点火候。
第一篇文章试图从一个比较高的角度来解释 Linux 下的 VFS 文件系统机制所以在叙述中更侧重于整个模块的主脉络而不拘泥于细节同时配有若干张插图以帮助读者理解。
第二篇文章提到在Kernel启动的初始阶段首先去创建虚拟的根文件系统(rootfs)接下来再去调用do_mount来加载真正的文件系统并将根文件系统切换到真正的文件系统也即真实的文件系统。
**
第一篇
**
解析 Linux 中的 VFS 文件系统机制根文件系统的由来
https://www.cnblogs.com/leaven/archive/2011/03/18/1988179.html
◆1. 摘要
本文阐述 Linux 中的文件系统部分源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux 下的文件系统主要可分为三大块一是上层的文件系统的系统调用二是虚拟文件系统 VFS(Virtual Filesystem Switch)三是挂载到 VFS 中的各实际文件系统例如 ext2jffs 等。本文侧重于通过具体的代码分析来解释 Linux 内核中 VFS 的内在机制在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。文章试图从一个比较高的角度来解释 Linux 下的 VFS 文件系统机制所以在叙述中更侧重于整个模块的主脉络而不拘泥于细节同时配有若干张插图以帮助读者理解。
相对来说VFS 部分的代码比较繁琐复杂希望读者在阅读完本文之后能对 Linux 下的 VFS 整体运作机制有个清楚的理解。建议读者在阅读本文前先尝试着自己阅读一下文件系统的源代码以便建立起 Linux 下文件系统最基本的概念比如至少应熟悉 super block, dentry, inodevfsmount 等数据结构所表示的意义这样再来阅读本文以便加深理解。
◆2. VFS 概述
VFS 是一种软件机制也许称它为 Linux 的文件系统管理者更确切点与它相关的数据结构只存在于物理内存当中。所以在每次系统初始化期间Linux 都首先要在内存当中构造一棵 VFS 的目录树(在 Linux 的源代码里称之为 namespace)实际上便是在内存中建立相应的数据结构。VFS 目录树在 Linux 的文件系统模块中是个很重要的概念希望读者不要将其与实际文件系统目录树混淆在笔者看来VFS 中的各目录其主要用途是用来提供实际文件系统的挂载点当然在 VFS 中也会涉及到文件级的操作本文不阐述这种情况。下文提到目录树或目录如果不特别说明均指 VFS 的目录树或目录。图 1 是一种可能的目录树在内存中的影像
图 1VFS 目录树结构
◆3. 文件系统的注册
这里的文件系统是指可能会被挂载到目录树中的各个实际文件系统所谓实际文件系统即是指VFS 中的实际操作最终要通过它们来完成而已并不意味着它们一定要存在于某种特定的存储设备上。比如在笔者的 Linux 机器下就注册有 “rootfs”、“proc”、“ext2”、“sockfs” 等十几种文件系统。
3.1 数据结构
在 Linux 源代码中每种实际的文件系统用以下的数据结构表示
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; }; 注册过程实际上将表示各实际文件系统的 struct file_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 定义如下
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 链表结构如下图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 中目录对应的数据结构是 struct dentry)。
在这个场景里do_kern_mount() 做的工作主要是
1调用 alloc_vfsmnt() 函数在内存里申请了一块该类型的内存空间struct vfsmount *mnt并初始化其部分成员变量。 调用 get_sb_nodev 函数在内存中分配一个超级块结构 (struct super_block) sb并初始化其部分成员变量将成员 s_instances 插入到 rootfs 文件系统类型结构中的 fs_supers 指向的双向链表中。 通过 rootfs 文件系统中的 read_super 函数指针调用 ramfs_read_super() 函数。还记得当初注册rootfs 文件系统时其成员 read_super 指针指向了 ramfs_read_super() 函数参见图2. ramfs_read_super() 函数调用 ramfs_get_inode() 在内存中分配了一个 inode 结构 (struct inode) 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 ramfs_read_super() 函数在分配和初始化了 inode 结构之后会调用 d_alloc_root() 函数来为 VFS的目录树建立起关键的根目录 (struct dentry)dentry并将 dentry 中的 d_sb 指针指向 sbd_inode 指针指向 inode。 将 mnt 中的 mnt_sb 指针指向 sbmnt_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); //mnt is returned by do_kern_mount() namespace-root mnt; init_task.namespace namespace; for_each_task§ { 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 目录树中增加新的节点。同时为配合路径搜索引入了下面一个数据结构
struct nameidata { struct dentry *dentry; struct vfsmount *mnt; struct qstr last; unsigned int flags; int last_type; }; 这个数据结构在路径搜索的过程中用来记录相关信息起着类似路标的作用。其中前两项中的 dentry记录的是要建目录的父目录的信息mnt 成员接下来会解释到。后三项记录的是所查找路径的最后一个节点(即待建目录或文件)的信息。 现在为建立目录 “/dev” 而调用 sys_mkdir(“/dev”, 0700)其中参数 0700 我们不去管它它只是限定将要建立的目录的某种模式。sys_mkdir 函数首先调用 path_lookup(“/dev”, LOOKUP_PARENT, nd)来对路径进行查找其中 nd 为 struct nameidata nd 声明的变量。在接下来的叙述中因为函数调用关系的繁琐为了突出过程主线将不再严格按照函数的调用关系来进行描述。
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.len3nd.last_typeLAST_NORM至于 nd 中 mnt 和 dentry 成员在这个场景里还是前面设置的值并无变化。这样一圈下来只是用 nd 记录下相关信息实际的目录建立工作并没有真正展开但是前面所做的工作却为接下来建立新的节点收集了必要的信息。
好到此为止真正建立新目录节点的工作将会展开这是由函数 lookup_create 来完成的调用这个函数时会传入两个参数lookup_create(nd, 1)其中参数 nd 便是前面提到的变量参数1表明要建立一个新目录。
这里的大体过程是新分配了一个 struct dentry 结构的内存空间用于记录 dev 目录所对应的信息该dentry 结构将会挂接到其父目录中也就是图 3 中 “/” 目录对应的 dentry 结构中由链表实现这一关系。接下来会再分配一个 struct inode 结构。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 的目录树中向其中某个目录(安装点 mount point)上挂载(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() 系统调用函数发起该函数原型声明如下
asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data); 其中参数 char *type 为标识将要安装的文件系统类型字符串对于 ext2 文件系统而言就是ext2。参数 flags 为安装时的模式标识数和接下来的 data 参数一样本文不将其做为重点。
为了帮助读者更好地理解这一过程笔者用一个具体的例子来说明我们准备将来自主硬盘第 2 分区(hda2)上的 ext2 文件系统安装到前面创建的 “/dev” 目录中。那么对于 sys_mount() 函数的调用便具体为
sys_mount(“hda2”,/dev ,“ext2”,…) 该函数在将这些来自用户内存空间(user space)的参数拷贝到内核空间后便调用 do_mount() 函数开始真正的安装文件系统的工作。同样为了便于叙述和讲清楚主流程接下来的说明将不严格按照具体的函数调用细节来进行。
do_mount() 函数会首先调用 path_lookup() 函数来得到安装点的相关信息如同创建目录过程中叙述的那样该安装点的信息最终记录在 struct nameidata 类型的一个变量当中为叙述方便记该变量为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() 函数返回的一 struct vfsmount 类型的变量加入到安装系统链表中同时 graft_tree() 还要将新分配的 struct vfsmount 类型的变量加入到一个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/windfly.cs 就是文件路径名path_lookup() 函数的责任就是对文件路径名中进行搜索直到找到目标文件所属目录所对应的 dentry 或者目标直接就是一个目录笔者不想在有限的篇幅里详细解释这个函数读者只要记住 path_lookup() 会返回一个目标目录即可。
上面的代码非常地不起眼以至于初次阅读文件系统的代码时经常会忽略掉它但是前文所提到从 VFS 的操作到实际文件系统操作的转化却是由它完成的对 VFS 中实现的文件系统的安装可谓功不可没。现在让我们仔细剖析一下该段代码 d_mountpoint(dentry) 的作用很简单它只是返回 dentry 中 d_mounted 成员变量的值。这里的dentry 仍然还是 VFS 目录树上的东西。如果 VFS 目录树上某个目录被安装过一次那么该值为 1。对VFS 中的一个目录可进行多次安装后面会有例子说明这种情况。在我们的例子中“/dev” 所对应的new_dentry 中 d_mounted1所以 while 循环中第一个条件满足。下面再来看__follow_down(nd-mnt, dentry)代
图 5安装 ext2 类型根文件系统到 /dev 目录上
码做了什么到此我们应该记住这里 nd 中的 dentry 成员就是图 5 中的 new_dentrynd 中的 mnt成员就是图 5 中的 mnt所以我们现在可以把 __follow_down(nd-mnt, dentry) 改写成__follow_down(mnt, new_dentry)接下来我们将 __follow_down() 函数的代码改写(只是去处掉一些不太相关的代码并且为了便于说明在部分代码行前加上了序号)如下
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 目录树下某一目录最近一次被 mount 时的安装区域块的指针在本例中最终会返回图 5 中的 e2_mnt。至于查找的原理这里粗略地描述一下。记得当我们在安装 ext2 文件系统到 “/dev” 时在后期会调用 graft_tree() 函数在这个函数里会把图 5 中的安装区域块指针 e2_mnt 挂到一 hash 表(Linux 2.4.20源代码中称之为 mount_hashtable)中的某一项而该项的键值就是由被安装点所对应的 dentry(本例中为 new_dentry)和 mount(本例中为 mnt)所共同产生所以自然地当我们知道 VFS 树中某一 dentry 被安装过(该 dentry 变成为一安装点)而要去查找其最近一次被安装的安装区域块指针时同样由该安装点所对应的 dentry 和 mount 来产生一键值以此值去索引 mount_hashtable自然可找到该安装点对应的安装区域块指针形成的链表的头指针然后遍历该链表当发现某一安装区域块指针记为 p满足以下条件时
(p-mnt_parent mnt p-mnt_mountpoint dentry) P 便为该安装点所对应的安装区域块指针。当找到该指针后便将 nd 中的 mnt 成员换成该安装区域块指针同时将 nd 中的 dentry 成员换成安装区域块中的 dentry 指针。在我们的例子中e2_mnt-mnt_root成员指向 e2_dentry也就是 ext2 文件系统的 “/” 目录。这样当 path_lookup() 函数搜索到 /dev时nd 中的 dentry 成员为 e2_dentry而不再是原来的 new_dentry同时 mnt 成员被换成 e2_mnt转化便在不知不觉中完成了。
现在考虑一下对某一安装点多次安装的情况同样作为例子我们假设在 “/dev” 上安装完一个 ext2文件系统后再在其上安装一个 ntfs 文件系统。在安装之前同样会对安装点所在的路径调用path_lookup() 函数进行搜索但是这次由于在 “/dev” 目录上已经安装过了 ext2 文件系统所以搜索到最后由 nd 返回的信息是nd.dentry e2_dentry, nd.mnt e2_mnt。由此可见在第二次安装时安装点已经由 dentry 变成了 e2_dentry。接下来同样地系统会再分配一个安装区域块假设该安装区域块的指针为 ntfs_mnt区域块中的 dentry 为 ntfs_dentry。ntfs_mnt 的父指针指向了e2_mntmnfs_mnt 中的 mnt_root 指向了代表 ntfs 文件系统根目录的 ntfs_dentry。然后系统通过 e2_dentry和 e2_mnt 来生成一个新的 hash 键值利用该值作为索引将 ntfs_mnt 加入到 mount_hashtable 中同时将 e2_dentry 中的成员 d_mounted 值设定为 1。这样安装过程便告结束。
读者可能已经知道对同一安装点上的最近一次安装会隐藏起前面的若干次安装下面我们通过上述的例子解释一下该过程
在先后将 ext2 和 ntfs 文件系统安装到 “/dev” 目录之后我们再调用 path_lookup() 函数来对/dev 进行搜索函数首先找到 VFS 目录树下的安装点 “/dev” 所对应的 dentry 和 mnt此时它发现dentry 成员中的 d_mounted 为 1于是它知道已经有文件系统安装到了该 dentry 上于是它通过 dentry 和 mnt 来生成一个 hash 值通过该值来对 mount_hashtable 进行搜索根据安装过程它应该能找到 e2_mnt 指针并返回之同时原先的 dentry 也已经被替换成 e2_dentry。回头再看一下前面已经提到的下列代码 while (d_mountpoint(dentry) __follow_down(nd-mnt, dentry)); 当第一次循环结束后, nd-mnt 已经是 e2_mnt而 dentry 则变成 e2_dentry。此时由于 e2_dentry 中的成员 d_mounted 值为 1所以 while 循环的第一个条件满足要继续调用 __follow_down() 函数这个函数前面已经剖析过当它返回后 nd-mnt 变成了 ntfs_mntdentry 则变成了 ntfs_dentry。由于此时 ntfs_dentry 没有被安装过其他文件所以它的成员 d_mounted 应该为 0循环结束。对 “/dev” 发起的 path_lookup() 函数最终返回了 ntfs 文件系统根目录所对应的 dentry。这就是为什么 “/dev” 本身和安装在其上的 ext2 都被隐藏的原因。如果此时对 “/dev” 目录进行一个 ls 命令将返回安装上去的 ntfs 文件系统根目录下所有的文件和目录。
◆7. 安装根文件系统
有了前面章节 5 的基础理解 Linux 下根文件系统的安装并不困难因为不管怎么样安装一个文件系统到 VFS 中某一安装点的过程原理毕竟都是一样的。
这个过程大致是首先要确定待安装的 ext2 文件系统的来源其次是确定 ext2 文件系统在 VFS中的安装点然后便是具体的安装过程。
关于第一问题Linux 2.4.20 的内核另有一大堆的代码去解决限于篇幅笔者不想在这里去具体说明这个过程大概记住它是要解决到哪里去找要安装的文件系统的就可以了这里我们不妨就认为要安装的根文件系统就来自于主硬盘的第一分区 hda1.
关于第二个问题Linux 2.4.20 的内核把来自于 hda1 上 ext2 文件系统安装到了 VFS 目录树中的/root 目录上。其实把 ext2 文件系统安装到 VFS 目录树下的哪个安装点并不重要(VFS 的根目录除外)只要是这个安装点在 VFS 树中是存在的并且内核对它没有另外的用途。如果读者喜欢尽可以自己在 VFS 中创建一个 “/Windows” 目录然后将 ext2 文件系统安装上去作为将来用户进程的根目录没有什么不可以的。问题的关键是要将进程的根目录和当前工作目录设定好因为毕竟只用用户进程才去关心现实的文件系统要知道笔者的这篇稿子可是要存到硬盘上去的。
在 Linux 下设定一个进程的当前工作目录是通过系统调用 sys_chdir() 进行的。初始化期间Linux 在将 hda1 上的 ext2 文件系统安装到了 “/root” 上后通过调用 sys_chdir(“/root”) 将当前进程也就是 init_task 进程的当前工作目录(pwd)设定为 ext2 文件系统的根目录。记住此时 init_task进程的根目录仍然是图 3 中的 dentry也就是 VFS 树的根目录这显然是不行的因为以后 Linux 世界中的所有进程都由这个 init_task 进程派生出来无一例外地要继承该进程的根目录如果是这样意味着用户进程从根目录搜索某一目录时实际上是从 VFS 的根目录开始的而事实上却是从 ext2 的根文件开始搜索的。这个矛盾的解决是靠了在调用完 mount_root() 函数后系统调用的下面两个函数
sys_mount(“.”, “/”, NULL, MS_MOVE, NULL); sys_chroot(“.”); 其主要作用便是将 init_task 进程的根目录转化成安装上去的 ext2 文件系统的根目录。有兴趣的读者可以自行去研究这一过程。
所以在用户空间下更多地情况是只能见到 VFS 这棵大树的一叶而且还是被安装过文件系统了的实际上对用户空间来说还是不可见。我想VFS 更多地被内核用来实现自己的功能并以系统调用的方式提供过用户进程使用至于在其上实现的不同文件系统的安装也只是其中的一个功能罢了。
◆8. 结束语
文件系统在整个 Linux 的内核中具有举足轻重的地位代码量也很复杂繁琐。但是因为其重要的地位要想对 Linux 的内核有比较深入的理解必须要能越过文件系统这一关。当然阅读其源代码便是其中最好的方法本文试图给那些已经尝试着去阅读但是目前尚有困惑的读者画一张 VFS 文件系统的草图希望能对读者有些许启发。但是想在如此有限的篇幅里去阐述清楚 Linux 中整个文件系统的来龙去脉是根本不现实的。而且本文也只是侧重于剖析 VFS 的机制对于象具体的文件读写为提高效率而引入的各种 buffer,hash 等内容以及文件系统的安全性方面都没有提到。毕竟本文只想帮助读者理清一个大体的脉络最终的理解与领悟还得靠读者自己去潜心研究源代码。最后对本文相关的任何问题或建议都欢迎用 email 和笔者联系。
**
第二篇
**
ARM Linux启动流程-根文件系统的加载
https://blog.csdn.net/silent123go/article/details/53470957 前言 在Kernel启动的初始阶段首先去创建虚拟的根文件系统(rootfs)接下来再去调用do_mount来加载真正的文件系统并将根文件系统切换到真正的文件系统也即真实的文件系统。 接下来结核内核代码(内核版本linux-3.14.28)讲解整个流程。
1、文件系统的分类
文件系统大体可以分为基于内存的文件系统(initrd)和非基于内存的文件系统(noinitrd)想要了解根文件系统的挂载流程首先要了解各种文件的特性及使用方法。 rootfs: 一个基于内存的文件系统是linux在初始化时加载的第一个文件系统。 realfs: 用户最终使用的真正的文件系统。 initramfs: 在内核镜像中附加一个cpio包这个cpio包中包含了一个小型的文件系统当内核启动时内核将这个cpio包解开并且将其中包含的文件系统释放到rootfs中内核中的一部分初始化代码会放到这个文件系统中作为用户层进程来执行。这样带来的明显的好处是精简了内核的初始化代码而且使得内核的初始化过程更容易定制。Linux 2.6.12内核的initramfs还没有什么实质性的东西一个包含完整功能的initramfs的实现可能还需要一个缓慢的过程。 cpio-initrd: cpio格式的initrd。一般作为最终的根文件系统。 image-initrd: 专指传统的文件镜像格式的initrd如ext2格式。可以作为最终的根文件系统也可以作为过渡由Image-initrd里的init来加载最终的根文件系统。 noinitrd: 如jffs2yaffs2等格式的根文件系统作为最终的根文件系统。
2、initrd的处理流程
initrd有CPIO-initrd和Image-initrd两种格式取决于制作initrd文件系统映像的工具和方法。initramfs是内核自动生成的一个简单的CPIO-initrd。 initramfs的处理流程: 1.如果内核支持initrd但是并没有配置CONFIG_INITRAMFS_SOURCE选项的话内核在编译的时候会自动生成一个最小的cpio包附在内核中(这个cpio包的内容与由default_rootfs生成的一样)除非你使用了ramdisk作为文件系统否则内核按initramfs文件系统启动。 2.将initramfs的内容释放到rootfs中。 3.挂载真实的文件系统。 cpio-initrd 的处理流程 1.bootloader 把内核以及 initrd 文件系统分别加载到内存的特定位置。然后启动内核并告诉内核initrd在内存的位置。 2.内核判断initrd的文件格式如果是cpio格式。 3.将initrd的内容释放到rootfs中。即这时候rootfs就是真正的根文件系统。 4.执行initrd中的/init文件执行到这一点内核的工作全部结束完全交给/init文件处理。 image-initrd的处理流程 1.bootloader 把内核以及 initrd 文件系统分别加载到内存的特定位置。然后启动内核并告诉内核initrd在内存的位置 2.内核判断initrd的文件格式如果不是cpio格式将其作为image-initrd处理。 3.内核将initrd的内容保存在rootfs下的/initrd.image文件中。 4.内核将/initrd.image的内容读入/dev/ram0设备中也就是读入了一个内存盘中。 5.接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。 6.执行initrd上的/linuxrc文件linuxrc通常是一个脚本文件负责加载内核访问根文件系统必须的驱动 以及加载根文件系统。 7.如果/dev/ram0被指定为真正的根文件系统那么内核跳至最后一步即第9步正常启动。 8.否则, 将真实根文件系统如/dev/mtdblock3或nfs挂载到rootfs下。 9.在常规根文件系统上进行正常启动过程 执行/sbin/init。 二者比较 、cpio-initrd的处理流程更加简单并没有使用额外的ramdisk而是将其内容直接输入到rootfs中其实rootfs本身也是一个基于内存的文件系统。这样就省掉了ramdisk的挂载、卸载等步骤。cpio-initrd不再象image-initrd那样作为linux内核启动的一个中间步骤而是作为内核启动的终点内核将控制权交给cpio-initrd的/init文件后内核的任务就结束了所以在/init文件中我们可以做更多的工作而不比担心同内核后续处理的衔接问题。 、而对于image-initrd如果最终的真实根文件系统不在Root_RAM0比如在/dev/mtdblock3或nfs则内核在执行完image-initrd 里的/linuxrc进程后还要进行一些收尾工作。并挂载最终执行真正的根文件系统和执行最终真正根文件系统里的init。如果最终的真实根文件系统在Root_RAM0则挂载最终执行真正的根文件系统和执行最终真正根文件系统里的init。
3、整体流程解读
3.1 根文件系统的注册 首先不得不从老掉牙的Linux系统的函数start_kernel()说起。函数start_kernel()中会去调用vfs_caches_init()来初始化VFS。 void __init vfs_caches_init(unsigned long mempages) { … //创建一个rootfs这是个虚拟的rootfs即内存文件系统后面还会指向真实的文件系统 mnt_init(); } void __init mnt_init(void) { … //创建虚拟根文件系统调用register_filesystem(rootfs_fs_type)注册rootfs即根文件系统 init_rootfs(); /********************************************************************************挂载根文件系统”/”其实这只是个空目录是后面挂载实际根文件系统的根节点。 *init_mount_tree会调用 vfs_kern_mount(“rootfs”, 0, “rootfs”, NULL)为 VFS 建立根目*录“/”而一旦有了根那么这棵数就可以发展壮大。同时挂载前面已经注册了的 rootfs 文件系统到*根目录“/”下。最后调用set_fs_pwd和set_fs_root切换进程的根目录和当前目录为”/”.这也就是根*目录的由来 ********************************************************************************/init_mount_tree(); }这个流程如下图所示
3. 根文件系统的初始化
3.2.1 noinitrd初始化流程 针对noinitrd的情况初始化一个简单的rootfs。主要往里面创建两个目录/dev和/root还有一个结点/dev/console。 /* * Create a simple rootfs that is similar to the default initramfs */ static int __init default_rootfs(void) { int err; err sys_mkdir((const char __user __force *) /dev, 0755);
if (err 0)goto out;err sys_mknod((const char __user __force *) /dev/console,S_IFCHR | S_IRUSR | S_IWUSR,new_encode_dev(MKDEV(5, 1)));
if (err 0)goto out;err sys_mkdir((const char __user __force *) /root, 0700);
if (err 0)goto out;return 0;out: printk(KERN_WARNING “Failed to create a rootfs\n”); return err; } rootfs_initcall(default_rootfs);
3.2.2initrd初始化流程 当内核支持initrd时rootfs_initcall调用initramfs.c中的populate_rootfs()函数。 针对initrd的情况在kernel启动之前uboot会把initrd映像(即真实根文件系统)拷贝到外部sram的指定位置。 如果是cpio-initrd则直接填充到rootfs根目录下这时rootfs即从vfs变成真实的根文件系统。 如果是Image-initrd则Image-initrd里面的内容保存到/initrd.image里面。 unpack_to_rootfs顾名思义就是解压包到rootfs文件系统中。
static int __init populate_rootfs(void) { char err unpack_to_rootfs(__initramfs_start, __initramfs_size); if (err) panic(“%s”, err); / Failed to decompress INTERNAL initramfs / /********************************* *如果内核支持initrd但是并没有配置CONFIG_INITRAMFS_SOURCE选项的话, *initrd_start为。 ***********/ if (initrd_start) { / *支持ramdisk的话必须定义宏CONFIG_BLK_DEV_RAM ************************/ #ifdef CONFIG_BLK_DEV_RAM int fd; printk(KERN_INFO “Trying to unpack rootfs image as initramfs…\n”); err unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start); if (!err) { free_initrd(); goto done; } else { clean_rootfs(); unpack_to_rootfs(__initramfs_start, __initramfs_size); } printk(KERN_INFO “rootfs image is not initramfs (%s)” “; looks like an initrd\n”, err); fd sys_open(“/initrd.image”, O_WRONLY|O_CREAT, 0700); if (fd 0) { sys_write(fd, (char *)initrd_start, initrd_end - initrd_start); sys_close(fd); free_initrd(); } done: #else printk(KERN_INFO “Unpacking initramfs…\n”); err unpack_to_rootfs((char )initrd_start, initrd_end - initrd_start); if (err) printk(KERN_EMERG “Initramfs unpacking failed: %s\n”, err); free_initrd(); #endif / * Try loading default modules from initramfs. This gives * us a chance to load before device_initcalls. */ load_default_modules(); } return 0; } rootfs_initcall(populate_rootfs);
检测根文件系统中是否存在ramdisk_execute_command文件。 这个值由uboot传给内核的参数中rdinit指定如果未指定则采用默认的/init。如果ramdisk_execute_command文件不存在则执行prepare_namespace()挂载根文件系统。 如果是cpio-initrdpopulate_rootfs已经成功解压cpio-initrd到rootfs中这种情况下rootfs就是真实的根文件系统所以这时一般会存在ramdisk_execute_command。 如果是Image-initrd或者noinitrd的情况一般不会存在ramdisk_execute_command所以执行prepare_namespace()挂载根文件系统。 start_kernel-rest_init-kernel_init-kernel_init_freeable
static noinline void __init kernel_init_freeable(void) { /* * Wait until kthreadd is all set-up. */ wait_for_completion(kthreadd_done);
/* Now the scheduler is fully set up and can do blocking allocations */
gfp_allowed_mask __GFP_BITS_MASK;/** init can allocate pages on any node*/
set_mems_allowed(node_states[N_MEMORY]);
/** init can run on any cpu.*/
set_cpus_allowed_ptr(current, cpu_all_mask);cad_pid task_pid(current);smp_prepare_cpus(setup_max_cpus);do_pre_smp_initcalls();
lockup_detector_init();smp_init();
sched_init_smp();do_basic_setup();/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) /dev/console, O_RDWR, 0) 0)pr_err(Warning: unable to open an initial console.\n);(void) sys_dup(0);
(void) sys_dup(0);
/** check if there is an early userspace init. If yes, let it do all* the work*/if (!ramdisk_execute_command)ramdisk_execute_command /init;if (sys_access((const char __user *) ramdisk_execute_command, 0) ! 0) {ramdisk_execute_command NULL;prepare_namespace();
}/** Ok, we have completed the initial bootup, and* were essentially up and running. Get rid of the* initmem segments and start the user-mode stuff..*//* rootfs is available now, try loading default modules */
load_default_modules();}
挂载真实的根文件系统并把真实的根文件系统的根目录作为进程的根目录。本函数的具体流程请看注释。
void __init prepare_namespace(void) { int is_floppy;
/************************* *对于将根文件系统存在usb或者scsi的情况 *kernel需要等待这些耗费时间比较久的驱动 *加载完毕所以这里存在一个delay。 *************************/ if (root_delay) { printk(KERN_INFO “Waiting %d sec before mounting root device…\n”, root_delay); ssleep(root_delay); }
/** wait for the known devices to complete their probing** Note: this is a potential source of long boot delays.* For example, it is not atypical to wait 5 seconds here* for the touchpad of a laptop to initialize.*/
/********************
*等待根文件系统所在的设备的探测函数的完成。
********************/
wait_for_device_probe();md_run_setup();/*******************************saved_root_name是uboot传进来的参数root/dev/mtdblock3******************************/
if (saved_root_name[0]) {root_device_name saved_root_name;if (!strncmp(root_device_name, mtd, 3) ||!strncmp(root_device_name, ubi, 3)) {mount_block_root(root_device_name, root_mountflags);goto out;}/**********************ROOT_DEV存放saved_root_name的设备节点号。*********************/ROOT_DEV name_to_dev_t(root_device_name);if (strncmp(root_device_name, /dev/, 5) 0)root_device_name 5;
}/*************************************挂载Image-initrd如果bootargs指定了noinitrd*那么initrd_load()是空操作。*************************************/
if (initrd_load())goto out;/* wait for any asynchronous scanning to complete */
if ((ROOT_DEV 0) root_wait) {printk(KERN_INFO Waiting for root device %s...\n,saved_root_name);while (driver_probe_done() ! 0 ||(ROOT_DEV name_to_dev_t(saved_root_name)) 0)msleep(100);async_synchronize_full();
}is_floppy MAJOR(ROOT_DEV) FLOPPY_MAJOR;if (is_floppy rd_doload rd_load_disk(0))ROOT_DEV Root_RAM0;/***********************把真实的根文件系统挂在到rootfs的/root目录下。**********************/
mount_root();out: devtmpfs_mount(“dev”); /**************************************** *将真实根文件系统从当前目录移动到rootfs的根目录后 *并进入根目录。 *然后将当前目录设置为系统的根目录即作为当前进程的根目录。 *所以最终把虚拟的文件系统切换到了真实的根文件系统。 ****************************************/ sys_mount(“.”, “/”, NULL, MS_MOVE, NULL); sys_chroot(“.”); }
initrd_load()是针对Image-initrd的函数注意前面已经把Image-initrd解压到了/initrd.image里面。
int __init initrd_load(void) { /*********************************** mount_initrd的默认值为1如果uboot传给kernel 的参数指明noinitrd则mount_initrd被置成0。 / if (mount_initrd) { create_dev(“/dev/ram”, Root_RAM0); / * Load the initrd data into /dev/ram0. Execute it as initrd * unless /dev/ram0 is supposed to be our actual root device, * in that case the ram disk is just set up here, and gets * mounted in the normal path. / /**** *rd_load_image函数将/initrd.image的内容释放到/dev/ram设备节点。 *如果根文件系统设备号不是Root_RAM0即给内核指定的参数不是/dev/ram *则会调用handle_initrd()。但是一般我们给内核指定的参数是/dev/ram。 ******************************************/ if (rd_load_image(“/initrd.image”) ROOT_DEV ! Root_RAM0) { sys_unlink(“/initrd.image”); handle_initrd(); return 1; } } sys_unlink(“/initrd.image”); return 0; }
执行/linuxrc脚本确定真实的根文件系统接着调用mount_root将真实的根文件系统挂载到rootfs的/root目录下。
static void __init handle_initrd(void) { struct subprocess_info *info; static char *argv[] { “linuxrc”, NULL, }; extern char *envp_init[]; int error; /***********************************real_root_dev为一个全局变量用来保存真实根文件系统的设备号。**********************************/
real_root_dev new_encode_dev(ROOT_DEV);/***********************************************/dev/root.old的设备号是Root_RAM0而前面已经把Image-initrd释放到了*Root_RAM0所以/dev/root.old下的内容就是真实的根文件系统Image-initrd。**********************************************/
create_dev(/dev/root.old, Root_RAM0);
/* mount initrd on rootfs /root */
/************************************
*将真实的根文件系统挂载到rootfs的/root目录下。
************************************/
mount_block_root(/dev/root.old, root_mountflags ~MS_RDONLY);
sys_mkdir(/old, 0700);
sys_chdir(/old);/* try loading default modules from initrd */
load_default_modules();/** In case that a resume from disk is carried out by linuxrc or one of* its children, we need to tell the freezer not to wait for us.*/
current-flags | PF_FREEZER_SKIP;info call_usermodehelper_setup(/linuxrc, argv, envp_init,GFP_KERNEL, init_linuxrc, NULL, NULL);
if (!info)return;
call_usermodehelper_exec(info, UMH_WAIT_PROC);current-flags ~PF_FREEZER_SKIP;/* move initrd to rootfs /old */
sys_mount(.., ., NULL, MS_MOVE, NULL);
/* switch root and cwd back to / of rootfs */
sys_chroot(..);if (new_decode_dev(real_root_dev) Root_RAM0) {sys_chdir(/old);return;
}sys_chdir(/);
/*************************************
*执行完linuxrc后真实的根文件系统已经确定则执行
*mount_root将真实的根文件系统挂载到rootfs的/root目录下。
**************************************/
ROOT_DEV new_decode_dev(real_root_dev);
mount_root();printk(KERN_NOTICE Trying to move old root to /initrd ... );
error sys_mount(/old, /root/initrd, NULL, MS_MOVE, NULL);
if (!error)printk(okay\n);
else {int fd sys_open(/dev/root.old, O_RDWR, 0);if (error -ENOENT)printk(/initrd does not exist. Ignored.\n);elseprintk(failed\n);printk(KERN_NOTICE Unmounting old root\n);sys_umount(/old, MNT_DETACH);printk(KERN_NOTICE Trying to free ramdisk memory ... );if (fd 0) {error fd;} else {error sys_ioctl(fd, BLKFLSBUF, 0);sys_close(fd);}printk(!error ? okay\n : failed\n);
}}
———————————————— 版权声明本文为CSDN博主「o倚楼听风雨o」的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明。 原文链接https://blog.csdn.net/silent123go/article/details/53470957