文章来自一口linux博客(linux文件系统初始化过程(1)---概述_linux struct dentry-优快云博客),这里主要拿来做笔记使用
一.概述
术语表:
struct task:进程
struct mnt_namespace:命名空间
struct mount:挂载点
struct vfsmount:挂载项
struct file:文件
struct super_block:超级块
struct dentry:目录
struct inode:索引节点
1.1、目的
linux文件系统主要分为三个部分:文件系统调用;虚拟文件系统(VFS);挂载到VFS的实际文件系统。
其中,VFS是核心,linux文件系统的本质就是在内存中创建一棵VFS树。当根目录被创建后,用户就可以使用系统调用在VFS上创建文件、删除文件、挂载各种文件系统等操作。
该系列文章主要分析linux3.10文件系统初始化过程,分为三个阶段:
1、挂载根文件系统(rootfs);
2、加载initrd;
3、挂载磁盘文件系统;
1.2、常用数据结构
linux文件系统中重要的数据结构有:文件、挂载点、超级块、目录项、索引节点等。每个数据结构的具体实现请参见源代码,这里不再描述。
为了直观的表示数据结构之间的关系,请参见图1:图中含有两个文件系统(红色和绿色表示的部分),并且绿色文件系统挂载在红色文件系统tmp目录下。一般来说,每个文件系统在VFS层都是由挂载点、超级块、目录和索引节点组成;当挂载一个文件系统时,实际也就是创建这四个数据结构的过程,因此这四个数据结构的地位很重要,关系也很紧密。由于VFS要求实际的文件系统必须提供以上数据结构,所以不同的文件系统在VFS层可以互相访问。
如果进程打开了某个文件,还会创建file(文件)数据结构,这样进程就可以通过file来访问VFS的文件系统了。
另外,该图只给出了主要的关系结构,忽略了部分细节。
1.3、函数调用关系
图2描述了文件系统初始化过程中主要的函数调用关系。linux文件系统初始化过程主要分为三个阶段:
1、vfs_caches_init()负责挂载rootfs文件系统,并创建了第一个挂载点目录:'/';
2、rest_init()负责加载initrd文件,扩展VFS树,创建基本的文件系统目录拓扑;
3、init程序负责挂载磁盘文件系统,并将文件系统的根目录从rootfs切换到磁盘文件系统;
linux文件系统初始化过程主要分为三个阶段:挂载rootfs,提供第一个挂载点''/;加载initrd,扩展VFS树;执行init程序,完成linux系统的初始化。下面会详细介绍每个阶段的主要内容。
二、挂载rootfs文件系统
rootfs是基于内存的文件系统,所有操作都在内存中完成;也没有实际的存储设备,所以不需要设备驱动程序的参与。基于以上原因,linux在启动阶段使用rootfs文件系统,当磁盘驱动程序和磁盘文件系统成功加载后,linux系统会将系统根目录从rootfs切换到磁盘文件系统。
2.1、主要函数调用过程
图1描述了挂载rootfs的函数调用关系(图中红色部分),便于后面的分析。
从图中发现,在挂载rootfs前会先挂载sysfs,这样做的原因是确保sysfs能够完整的记录下设备驱动模型。
sysfs_init()完成注册和挂载sysfs文件系统的功能;init_rootfs()负责注册rootfs,init_mount_tree()负责挂载rootfs,并将init_task的命名空间与之联系起来。
2.2、linux文件系统初始化
vfs_cache_init()首先建立并初始化目录hash表dentry_hashtable和索引节点hash表inode_hashtable;然后设置内核可以打开的最大文件数;最后调用mnt_init()完成sysfs和rootfs文件系统的注册和挂载。
linux使用哈希表存储目录和索引节点,以提高目录和索引节点的查找效率;dentry_hashtable是目录哈希表,inode_hashtable是索引节点哈希表。
2.3、挂载sysfs文件系统
sysfs用来记录和展示linux驱动模型,sysfs先于rootfs挂载是为全面展示linux驱动模型做好准备。
mnt_init()调用sysfs_init()注册并挂载sysfs文件系统,然后调用kobject_create_and_add()创建"fs"目录。
2735 err = sysfs_init();
2736 if (err)
2737 printk(KERN_WARNING "%s: sysfs_init error: %d\n",
2738 __func__, err);
2739 fs_kobj = kobject_create_and_add("fs", NULL);
2740 if (!fs_kobj)
2741 printk(KERN_WARNING "%s: kobj create error\n", __func__);
下面详细介绍sysfs文件系统的挂载过程:
1、sysfs_init()调用register_filesystem()注册文件系统类型sysfs_fs_type,并加入到全局单链表file_systems中。sysfs_fs_type定义如下,.mount成员函数负责超级块、根目录和索引节点的创建和初始化工作。
err = register_filesystem(&sysfs_fs_type);
if (!err) {
sysfs_mnt = kern_mount(&sysfs_fs_type);
if (IS_ERR(sysfs_mnt)) {
printk(KERN_ERR "sysfs: could not mount!\n");
err = PTR_ERR(sysfs_mnt);
sysfs_mnt = NULL;
unregister_filesystem(&sysfs_fs_type);
goto out_err;
}
static struct file_system_type sysfs_fs_type = {
.name = "sysfs",
.mount = sysfs_mount,
.kill_sb = sysfs_kill_sb,
.fs_flags = FS_USERNS_MOUNT,
};
2、sysfs_init()->kern_mount()->vfs_kern_mount()创建并初始化struct mount挂载点,并使用全局变量sysfs_mnt保存该挂载点的挂载项(mnt成员)。
783 mnt = alloc_vfsmnt(name);
784 if (!mnt)
785 return ERR_PTR(-ENOMEM);
3、kern_mount()调用sysfs_fs_type的.mount成员sysfs_mount()创建并初始化超级块、根目录'/'、根目录的索引节点等数据结构;并且把超级块添加到全局单链表super_blocks中,把索引节点添加到hash表inode_hashtable和超级块的inode链表中。
目前,我们可以得出一个重要结论:kern_mount()主要完成挂载点、超级块、根目录和索引节点的创建和初始化操作,可以看成是一个原子操作,这个函数以后会频繁使用。
790 root = mount_fs(type, flags, name, data);
1091 struct dentry *
1092 mount_fs(struct file_system_type *type, int flags, const char *name, void*data)
1093 {
1094 struct dentry *root;
...
1108
1109 root = type->mount(type, flags, name, data);
107 static struct dentry *sysfs_mount(struct file_system_type *fs_type,
108 int flags, const char *dev_name, void *data)
109 { ...
112 struct super_block *sb;
...
125 sb = sget(fs_type, sysfs_test_super, sysfs_set_super, flags, info);
...
130 if (!sb->s_root) {
131 error = sysfs_fill_super(sb, data, flags & MS_SILENT ? 1 : 0);
4、vfs_kern_mount()初始化挂载点的根目录和超级块。
796 mnt->mnt.mnt_root = root;
797 mnt->mnt.mnt_sb = root->d_sb;
798 mnt->mnt_mountpoint = mnt->mnt.mnt_root;
799 mnt->mnt_parent = mnt;
2.4、mnt_init()调用kobject_create_and_add()创建"fs"目录。
通过以上步骤,sysfs文件系统在VFS中的视图如图2所示:挂载点指向超级块和根目录;超级块处在super_blocks单链表中,并且链接起所有属于该文件系统的索引节点;根目录'/'和目录"fs"指向各自的索引节点;为了提高查找效率,索引节点保存在hash表中。
2.5、挂载rootfs文件系统
mnt_init()调用init_rootfs()注册rootfs,然后调用init_mount_tree()挂载rootfs。
下面详细介绍rootfs文件系统的挂载过程:
1、mnt_init()调用init_rootfs()注册文件系统类型rootfs_fs_type,并加入到全局单链表file_systems中。
rootfs_fs_type定义如下,mount成员函数负责超级块、根目录和索引节点的建立和初始化工作。
265 static struct file_system_type rootfs_fs_type = {
266 .name = "rootfs",
267 .mount = rootfs_mount,
268 .kill_sb = kill_litter_super,
269 };
2、init_mount_tree()调用vfs_kern_mount()挂载rootfs文件系统,详细的挂载过程与sysfs文件系统类似,不再赘述。
3、init_mount_tree()调用create_mnt_ns()创建命名空间,并设置该命名空间的挂载点为rootfs的挂载点,同时将rootfs的挂载点链接到该命名空间的双向链表中。
2459 static struct mnt_namespace *create_mnt_ns(struct vfsmount *m)
2460 {
2461 struct mnt_namespace *new_ns = alloc_mnt_ns(&init_user_ns);
2462 if (!IS_ERR(new_ns)) {
2463 struct mount *mnt = real_mount(m);
2464 mnt->mnt_ns = new_ns;
2465 new_ns->root = mnt;
2466 list_add(&mnt->mnt_list, &new_ns->list);
2467 }
4、init_mount_tree()设置init_task的命名空间,同时调用set_fs_pwd()和set_fs_root()设置init_task任务的当前目录和根目录为rootfs的根目录'/'。
2696 ns = create_mnt_ns(mnt);
2697 if (IS_ERR(ns))
2698 panic("Can't allocate initial namespace");
2699
2700 init_task.nsproxy->mnt_ns = ns;
2701 get_mnt_ns(ns);
2702
2703 root.mnt = mnt;
2704 root.dentry = mnt->mnt_root;
2705
2706 set_fs_pwd(current->fs, &root);
2707 set_fs_root(current->fs, &root);
通过以上分析,我们发现sysfs和rootfs的区别在于:虽然系统同时挂载了sysfs和rootfs文件系统,但是只有rootfs处于init_task进程的命名空间内,也就是说系统当前实际使用的是rootfs文件系统。
此时,sysfs和rootfs在VFS中的视图如图3所示:为了突出主要关系,省略了挂载点指向超级块和根目录。
从图中看出,rootfs处于进程的命名空间中,并且进程的fs_struct数据结构的root和pwd都指向了rootfs的根目录'/',所以用户实际使用的是rootfs文件系统。另外,rootfs为VFS提供了'/'根目录,所以文件操作和文件系统的挂载操作都可以在VFS上进行了。
linux文件系统在初始化时,同时挂载了sysfs和rootfs文件系统,但是只有rootfs处于进程的命名空间中,且进程的root目录和pwd目录都指向rootfs的根目录。至此,linux的VFS已经准备好了根目录(rootfs的根目录'/'),此时用户可以使用系统调用对VFS树进行扩展。
三、加载initrd
initrd是一个临时文件系统,由bootload负责加载到内存中,里面包含了基本的可执行程序和驱动程序。在linux初始化的初级阶段,它提供了一个基本的运行环境。当成功加载磁盘文件系统后,系统将切换到磁盘文件系统并卸载initrd。
如果是嵌入式设备,那么最终的文件系统就是initrd。
3.1、cpio文件格式
initrd常用的的文件格式是cpio,cpio格式记录了文件系统的结构和内容。
cpio格式具体定义如图1所示:
cpio格式的文件由段组成,最后一个段比较特殊,文件名为”TRAILER!!!”。
每个段都由文件头、文件名和文件体组成;文件名和文件体的长度由文件头中的name_len和body_len指定,并且文件名和文件体需要按指定字节对齐,所以尾部包含padding。
文件头共110个字节,头6个字节固定为070701,剩下字节的含义分别为:索引节点号、文件模式、用户id、组id、链接数、时间戳、文件体长度、主设备号、次设备号、设备号、文件名长度、保留字段。
其他详细情况请参见init/initramfs.c文件,这里不再描述。
3.2、initrd文件实例
为了更直观的理解cpio格式的initrd文件,下面看一个实例。
在ubuntu环境中,boot目录下存放着经过压缩的cpio格式文件initrd。
将boot目录下的initrd文件拷贝到任意目录下,重名为为initrd.gz,并且使用gunzip解压。
这样我们就得到了一个cpio格式的initrd文件,使用vi查看文件内容如图2所示(由于文件太大,只展示了部分内容):
简单分析后显示该文件包含了script/nfs-top目录、script/nfs-top/ORDER文件、script/nfs-top/udev文件、run目录、标志cpio结束的TRAILER!!!文件。
3.3、解压initrd文件
initrd经过gunzip解压后,可以使用cpio工具解压cpio格式的文件。命令如下:
root: cpio-idmv < initrd
解压成功后,使用ls命令查看initrd文件内容如图3所示:
bin和sbin目录下包含基本的可执行程序;conf和etc目录下是配置文件;lib目录下是可执行程序使用的动态库;scripts目录下是脚本程序;init程序。initrd必须提供一个init程序,linux在加载完initrd后,会跳转到init程序,由init程序负责后面的初始化工作。
initrd文件系统提供了init程序,在linux初始化阶段的后期会跳转到init程序,由该程序负责加载驱动程序和挂载磁盘文件系统以及其他的初始化工作。
四、源代码角度分析加载并解析initrd文件的过程
initrd文件和linux内核一般存储在磁盘空间中,在系统启动阶段由bootload负责把磁盘上的内核和initrd加载到指定的内存空间中;然后,再由内核读取和解析initrd文件,在VFS(目前只有rootfs的根目录)中新建目录、常规文件、符号链接文件以及特殊文件;这样VFS就从根目录"/"成长为一棵枝繁叶茂的大树了。
4.1、函数调用过程
initrd详细的加载过程在init/initramfs.c中实现的,为了更好的理解加载过程,我们给出了关键函数的调用关系图1。这里需要注意下,由于使用roofs_initcall()宏在initcallroofs段中注册了populate_rootfs()函数,因此在执行do_initcalls()函数时会隐示调用populate_rootfs()。
4.2、initcall简介
linux在代码段中定义了一个特殊的段initcall,该段中存放的都是函数指针;linux初始化阶段调用do_initcalls()依次执行该段的函数。关于该段的详细信息可以参见vmlinux.lds.S链接脚本。
用户可以调用以下一组宏在initcall段中注册函数指针;initcall段分为initcall0-initcall7这8个等级,initcall0段的优先级最高,initcall7段的优先级最低,优先级高的段最先被执行;initcallrootfs段优先级介于5和6之间。
187 #define early_initcall(fn) __define_initcall(fn, early)
196 #define pure_initcall(fn) __define_initcall(fn, 0)
198 #define core_initcall(fn) __define_initcall(fn, 1)
199 #define core_initcall_sync(fn) __define_initcall(fn, 1s)
200 #define postcore_initcall(fn) __define_initcall(fn, 2)
201 #define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
202 #define arch_initcall(fn) __define_initcall(fn, 3)
203 #define arch_initcall_sync(fn) __define_initcall(fn, 3s)
204 #define subsys_initcall(fn) __define_initcall(fn, 4)
205 #define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
206 #define fs_initcall(fn) __define_initcall(fn, 5)
207 #define fs_initcall_sync(fn) __define_initcall(fn, 5s)
208 #define rootfs_initcall(fn) __define_initcall(fn, rootfs)
209 #define device_initcall(fn) __define_initcall(fn, 6)
210 #define device_initcall_sync(fn) __define_initcall(fn, 6s)
211 #define late_initcall(fn) __define_initcall(fn, 7)
212 #define late_initcall_sync(fn) __define_initcall(fn, 7s)
#define __define_initcall(fn, id) \
179 static initcall_t __initcall_##fn##id __used \
180 __attribute__((__section__(".initcall" #id ".init"))) = fn
用户使用不同优先级的initcall宏可以很方便的在linux代码中注册函数指针;将这些函数指针存储在相应的initcall段中;最终,由do_initcalls()按照优先级依次执行段中的函数,具体的代码实现如下:
715 static initcall_t *initcall_levels[] __initdata = {
716 __initcall0_start,
717 __initcall1_start,
718 __initcall2_start,
719 __initcall3_start,
720 __initcall4_start,
721 __initcall5_start,
722 __initcall6_start,
723 __initcall7_start,
724 __initcall_end,
725 };
678 int __init_or_module do_one_initcall(initcall_t fn)
679 {
681 int ret;
686 ret = fn();
}
739 static void __init do_initcall_level(int level)
740 {
742 initcall_t *fn;
...
751 for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
752 do_one_initcall(*fn);
753 }
754
755 static void __init do_initcalls(void)
756 {
757 int level;
758
759 for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
760 do_initcall_level(level);
761 }
回到加载initrd这个话题中,在init/initram.c的最后使用rootfs_initcall宏注册了populate_rootfs()函数;基于以上分析,我们知道这里就是加载initrd文件的入口,下面就开始分析该函数的功能。
627 rootfs_initcall(populate_rootfs);
4.3、加载initrd文件
系统启动阶段,bootload将initrd加载到内存起始地址为initrd_start,结束地址为initrd_end的内存中。
populate_rootfs()调用unpack_to_rootfs()从内存中读取并解析initrd文件;根据CPIO的格式我们知道initrd文件是由很多个段组成,且段中又是由文件头、文件名和文件体组成,因此该解析程序可以使用了状态机原理处理initrd文件。
解析程序定义了以下8种状态:Start(初始状态)、Collect(获取符号链接文件信息状态)、GotHeader(获取文件头信息状态)、SkipIt(跳过该段状态)、GotName(获取文件名并新建文件状态)、CopyFile(写文件状态)、GotSymlink(新建符号链接文件状态)、Reset(终止状态)。
376 static __initdata int (*actions[])(void) = {
377 [Start] = do_start,
378 [Collect] = do_collect,
379 [GotHeader] = do_header,
380 [SkipIt] = do_skip,
381 [GotName] = do_name,
382 [CopyFile] = do_copy,
383 [GotSymlink] = do_symlink,
384 [Reset] = do_reset,
385 };
为了直观理解initrd文件的解析过程,下面给出状态机跳转图2。
从图中可以看出将文件分为符号链接和非符号链接两种情况处理,这是因为符号链接文件是一种特殊的文件,只有第一个符号链接文件的inode存储的是真实数据,而其他符号链接文件inode中存储的是第一个符号链接文件的路径名,因此需要把第一个符号链接文件的路径名缓存起来,缓存的数据结构是hash表,所以在处理符号链接文件时多了一些hash表的操作,因此分为了符号链接文件和非符号链接文件这两种情况来处理。
initrd文件的详细解析过程如下:
1、S0:初始状态,初始化一些全局变量;
2、S1:获取符号链接文件的文件头和文件体;
3、S2:根据CPIO格式的定义,获取文件头信息;
4、S3:跳过当前CPIO格式的段,继续处理下一个段;
5、S4:获取文件名,并在VFS中新建文件;
6、S5:将文件内容写入到新建文件中;
7、S6:新建符号链接文件;
8、S7:处理完当前CPIO格式的段,继续一个段的处理。
从图中还可以看出,由于目录文件和特殊文件没有文件内容,因此跳过了S5状态,直接进入S3状态。
通过以上分析,程序就可以成功解析initrd文件,并使用sys_dir()、sys_open()、sys_mknod()、sys_symlink()等系统调用新建目录、常规文件、特殊文件和符号链接文件了。此时,VFS从只有根目录"/"成长为了一棵内容丰富的大树。
五、执行init程序
内核加载完initrd文件后,为挂载磁盘文件系统做好了必要的准备工作,包括挂载了sysfs、proc文件系统,加载了磁盘驱动程序驱动程序等。接下来,内核跳转到用户空间的init程序,由init完成创建磁盘设备文件、加载磁盘文件系统、从rootfs切换到磁盘根文件系统等工作。
由于在不同的linux发行版中,init的实现方式差异很大,不能将所有的发行版都分析一遍,因此本文选取ubuntu12.04发行版来描述如何从rootfs切换到磁盘根文件系统。
5.1、创建磁盘设备文件
init程序使用udev工具动态的创建磁盘设备文件。udev的工作原理是根据sysfs中的设备信息,在/dev目录下创建相应的设备文件,因此需要提前准备好sysfs文件系统。
首先,创建必要的挂载点目录/dev、/root、/sys、/proc等;然后,将VFS中的sysfs挂载到rootfs的/sys目录下,将tmpfs挂载到/dev目录下(/dev的文件系统类型为tmpfs);最后,为了输出打印信息,创建了/dev/console、/dev/null两个特殊的设备文件。
这些必要信息准备好后,就可以启动udev后台进程,由udev根据sysfs动态的创建磁盘设备文件。Udev启动代码在scripts/init-top/udev中。
5.2、挂载磁盘文件系统
磁盘文件系统的挂载一般有两种方式:本地方式和网络方式。根据BOOT变量的值,init选择执行本地加载或者网络加载,如果是本地加载则执行/scripts/local脚本;如果是网络加载则执行/scripts/nfs脚本。个人pc一般都是本地加载,数据中心的服务器一般是nfs加载。
最后,由init程序调用/scripts/local脚本挂载磁盘文件系统。
5.3、切换根文件系统
成功挂载磁盘文件系统后,需要将rootfs下的/sys、/proc、/dev等重要的目录都迁移到磁盘文件系统下。
最后,通过调用/sbin/run-init程序将内核的根文件系统从rootfs切换到磁盘文件系统的根目录。
5.4、最终VFS视图
到此为止,内核文件系统初始化过程就全部完成了,下面给出最终的VFS视图(由于文件系统过大,因此只给出其中关键的拓扑结构):
init程序的主要工作就是加载磁盘文件系统,将rootfs下重要的目录迁移到磁盘文件系统下,最后将内核根目录从rootfs切换到磁盘文件系统的根目录。