一、目的
本文主要讲述linux3.10文件系统初始化过程的第二阶段:加载initrd。
initrd是一个临时文件系统,由bootload负责加载到内存中,里面包含了基本的可执行程序和驱动程序。在linux初始化的初级阶段,它提供了一个基本的运行环境。当成功加载磁盘文件系统后,系统将切换到磁盘文件系统并卸载initrd。
如果是嵌入式设备,那么最终的文件系统就是initrd。
二、cpio文件格式
initrd常用的的文件格式是cpio,cpio格式记录了文件系统的结构和内容。
cpio格式具体定义如图1所示:
cpio格式的文件由段组成,最后一个段比较特殊,文件名为”TRAILER!!!”。
每个段都由文件头、文件名和文件体组成;文件名和文件体的长度由文件头中的name_len和body_len指定,并且文件名和文件体需要按指定字节对齐,所以尾部包含padding。
文件头共110个字节,头6个字节固定为070701,剩下字节的含义分别为:索引节点号、文件模式、用户id、组id、链接数、时间戳、文件体长度、主设备号、次设备号、设备号、文件名长度、保留字段。
其他详细情况请参见init/initramfs.c文件,这里不再描述。
图1
三、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!!!文件。
图2
四、解压initrd文件
initrd经过gunzip解压后,可以使用cpio工具解压cpio格式的文件。命令如下:
解压成功后,使用ls命令查看initrd文件内容如图3所示:
bin和sbin目录下包含基本的可执行程序;conf和etc目录下是配置文件;lib目录下是可执行程序使用的动态库;scripts目录下是脚本程序;init程序。initrd必须提供一个init程序,linux在加载完initrd后,会跳转到init程序,由init程序负责后面的初始化工作。
图3
五、总结
本文详细介绍了cpio格式的initrd文件,以及解压后各个目录的含义。initrd文件系统提供了init程序,在linux初始化阶段的后期会跳转到init程序,由该程序负责加载驱动程序和挂载磁盘文件系统以及其他的初始化工作。
一、目的
上文详细介绍了CPIO格式的initrd文件,本文从源代码角度分析加载并解析initrd文件的过程。
initrd文件和linux内核一般存储在磁盘空间中,在系统启动阶段由bootload负责把磁盘上的内核和initrd加载到指定的内存空间中;然后,再由内核读取和解析initrd文件,在VFS(目前只有rootfs的根目录)中新建目录、常规文件、符号链接文件以及特殊文件;这样VFS就从根目录"/"成长为一棵枝繁叶茂的大树了。
二、函数调用过程
initrd详细的加载过程在init/initramfs.c中实现的,为了更好的理解加载过程,我们给出了关键函数的调用关系图1。这里需要注意下,由于使用roofs_initcall()宏在initcallroofs段中注册了populate_rootfs()函数,因此在执行do_initcalls()函数时会隐示调用populate_rootfs()。
图1
三、initcall简介
linux在代码段中定义了一个特殊的段initcall,该段中存放的都是函数指针;linux初始化阶段调用do_initcalls()依次执行该段的函数。关于该段的详细信息可以参见vmlinux.lds.S链接脚本。
用户可以调用以下一组宏在initcall段中注册函数指针;initcall段分为initcall0-initcall7这8个等级,initcall0段的优先级最高,initcall7段的优先级最低,优先级高的段最先被执行;initcallrootfs段优先级介于5和6之间。
#define __define_initcall(fn, id) \\
179 static initcall_t __initcall_##fn##id __used \\
180 __attribute__((__section__(".initcall" #id ".init"))) = fn
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)
用户使用不同优先级的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);
四、加载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状态。
图2
五、总结
通过以上分析,程序就可以成功解析initrd文件,并使用sys_dir()、sys_open()、sys_mknod()、sys_symlink()等系统调用新建目录、常规文件、特殊文件和符号链接文件了。此时,VFS从只有根目录"/"成长为了一棵内容丰富的大树。
一、目的
linux把文件分为常规文件、目录文件、软链接文件、硬链接文件、特殊文件(设备文件、管道文件、socket文件等)几种类型,分别对应不同的新建函数sys_open()、sys_mkdir()、sys_symlink()、sys_link()、sys_mknod()。
系统初始化阶段成功加载initrd后,调用这些接口函数创建各种文件,因此这些函数在linux文件系统初始化过程中起到了重要作用,本文将详细描述这些接口函数的实现过程。
这些接口函数主要在fs/namei.c、fs/open.c文件中实现,可以在这两个文件中找到对应的源代码。
二、快速路径查找
在以上系统调用的实现中,需要解决一个关键问题:如何根据待新建文件的路径名,快速找到父目录的位置。得到父目录的位置后,才能创建待新建文件的目录,以及分配inode节点。
例如:新建/tmp/test.txt文件,首先需要根据”/tmp/test.txt”路径名查找到test.txt父目录tmp在VFS中的位置,然后基于tmp的位置才能为test.txt文件新建目录及分配inode节点。
由于快速路径查找使用比较频繁,所以对查找效率要求较高,否则会影响系统性能。do_path_lookup(intdfs, const char *name, unsigned int flags, struct nameidata*nd)函数负责实现该功能,其中name参数是路径名,nd参数是structnameidata结构体,nd的path成员记录了父目录的位置,last成员记录了待新建文件的文件名。例如:path记录了tmp在VFS中的位置,last记录”test.txt”文件名。
该函数的实现比较复杂,不方便对照源代码讲述,因此下面重点描述了该函数的主要操作。但是为了读者方便阅读源代码,给出如下关键函数的调用关系:do_path_lookup()->path_lookupat()->path_init()->link_path_walk()->walk_component()->lookup_fast()->__d_look_rcu()->__follow_mount_rcu()
do_path_lookup()在实现过程中,按照以下三种情形来操作:
1、路径名在单文件系统中:从根目录或当前目录开始,根据目录拓扑结构,递归查找父目录;
2、路径名在多文件系统中:在递归查找时,需要从当前文件系统的挂载点切换到最终文件系统的目录拓扑结构中,然后继续查找;
3、路径名在挂载点重复挂载多文件系统的情况:在递归查找时,需要从当前文件系统的挂载点切换到最终文件系统的目录拓扑结构中,然后继续查找;
为了理解以上三种情形的具体差异,举例说明:待新建文件路径名为”/usr/tmp/log/new.txt”。
情形1:
如下图所示,路径名只在单个文件系统ext3中存在,所以根据目录拓扑结构,递归查找父目录即可;查找完成后,使用nd.path记录父目录log的位置,nd.last记录了字符串常量”new.txt”。
情形2:
在情形2中,路径名横跨了两个文件系统,minix挂载在ext3的tmp目录上;当成功挂载minix后,设置挂载点tmp为已挂载状态,并且将tmp指向minix文件系统。
路径查找程序根据路径名”/usr/tmp/log/new.txt”查找到tmp目录时,发现tmp为已挂载状态,所以从挂载点tmp切换到minix根目录;然后,确认minix根目录不是已挂载状态后,在minix文件系统中继续查找剩下的路径名”log/new.txt”;查找完成后,使用nd记录查找结果。
情形3:
在情形3中,ext3的tmp目录重复挂载了两个文件系统minix和nfs;当成功挂载minix后,设置挂载点tmp为已挂载状态;当在同一挂载点tmp挂载nfs时,发现tmp已经是挂载状态,所以从挂载点tmp切换到minix根目录,在该根目录挂载nfs文件系统,并且将minix根目录设置为已挂载状态。linux支持在同一挂载点挂载多个文件系统的操作,但是只有最后被挂载的文件系统才是可见的,所以使用ls命令只能看到nfs文件系统的内容。
路径查找程序根据路径名”/usr/tmp/log/new.txt”查找到tmp目录时,发现tmp为已挂载状态,所以从挂载点tmp切换到minix根目录;然后发现minix根目录也是已挂载状态,所以继续切换到nfs根目录;最后,确认nfs根目录不是已挂载状态后,在nfs文件系统中继续查找剩下的路径名”log/new.txt”;查找完成后,使用nd记录查找结果。
在以上操作中,由__follow_mount_rcu()函数负责目录挂载状态检测和切换操作。
三、新建文件系统调用
do_path_lookup()函数返回的structnameidata *nd数据结构,记录了待新建文件父目录的位置,所以根据nd记录的信息,在VFS树中新建文件就变得相对简单了。
由于文件系统的操作比较复杂,因此不对源代码进行详解,主要介绍系统调用的主要功能(图中绿色部分),但是给出了关键函数调用路径,便于读者查阅细节内容。
3.1、新建常规文件系统调用sys_open()
注:这里重点介绍sys_open()的新建功能,忽略打开功能(打开功能比较复杂也与主题不符),所以读者不能片面认为sys_open()的功能只是新建文件。
关键函数调用路径如下:
主要功能总结:
1)get_unused_fd_flags()新建文件描述符;
2)do_filp_open()创建常规文件的file结构体、目录项、inode节点,并将三者关联起来;
2.1)get_empty_filp()创建常规文件的file结构体;
2.2)lookup_open()调用lookup_dcache()创建常规文件的目录项目;
2.3)lookup_open()调用vfs_create()创建常规文件的inode节点;
3)fd_install()将文件描述符指向file结构体。
具体操作流程如下图所示:
3.2、新建硬链接文件系统调用sys_link()
关键函数调用路径如下:
主要功能总结:
1)user_path_at()返回硬链接目标文件的目录项位置;
2)user_path_create()创建硬链接文件的目录项;
2.1)do_path_lookup()返回硬链接文件的父目录位置
2.2)lookup_hash()根据父目录位置,创建硬链接文件的目录项;
3)vfs_link()将硬链接文件指向硬链接目标文件的inode节点。
具体操作流程如下图所示:
3.3、新建目录文件系统调用sys_mkdir()
关键函数调用路径如下:
主要功能总结:
1)user_path_create()创建目录文件的目录项
1.1)do_path_lookup()返回目录文件的父目录位置;
1.2)lookup_hash()根据父目录位置,创建目录文件的目录项;
2)vfs_mkdir()创建目录文件的inode节点;
具体操作流程如下图所示:
3.4、新建软链接文件系统调用sys_symlink()
关键函数调用路径:
主要功能总结:
1)user_path_create()创建软链接文件的父目录位置;
1.1)do_path_lookup()返回软链接文件的父目录位置;
1.2)lookup_hash()根据父目录位置,创建软链接文件的目录项;
2)vfs_symlink()创建软链接文件的inode节点,并且inode节点记录了软链接目标文件的位置;简单来说,软链接文件的内容就是软链接目标文件的路径。
具体操作流程如下图所示:
3.5、新建特殊文件系统调用sys_mknod()
关键函数调用路径:
主要功能总结:
1)user_path_create()创建特殊文件的父目录位置;
1.1)do_path_lookup()返回特殊文件的父目录位置;
1.2)lookup_hash()根据父目录位置,创建特殊文件的目录项;
2)vfs_mknod()创建特殊文件的inode节点,初始化inode的i_rdev成员,并且将i_fop成员指向默认文件操作;
注:当用户使用sys_open()打开特殊文件时,会调用默认文件操作的open成员,把设备文件挂接到inode的设备链表中,并且将i_fop成员重新指向该设备的文件操作(即设备驱动)。
具体操作流程如下图所示:
通过以上介绍,可以看出快速路径查找函数do_path_lookup()在新建文件系统调用中起到了基础性的作用,因此有必要掌握该函数的用法。
四、VFS全景图
到目前为止,initrd的加载过程就全部结束了;最后,为了更清晰的理解VFS此时的全貌,给出如下VFS全景图。
从图中看出以下特点:
1、sysfs文件系统目前还没有挂载到rootfs的某个挂载点上,后续init程序会把sysfs挂载到rootfs的sys挂载点上;
2、系统打开了/dev/console设备,说明系统已经可以使用该设备,打印信息也可以正常输出;
3、rootfs文件系统的根目录下已经准备好了init程序,内核后续会启动该程序完成剩下的初始化操作。
五、总结
目前为止,linux成功加载了initrd文件后,在内核中构建了一个基于内存的文件系统rootfs;VFS再也不是只有根目录、结构简单的小树苗了;现在,它已经成长为拥有目录、常规文件、链接文件、设备文件等多种文件类型、结构复杂的参天大树了。