4.2.2 解压initramfs到rootfs
在挂载了rootfs后,内核将Bootloader加载到内存中的initramfs中的文件解压到rootfs中。而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具实现挂载真正的根文件系统。
一旦配置内核支持initramfs,那么内核将编译文件initramfs.c,脚本如下所示:
而在文件initramfs.c中,我们可看到如下代码:
宏rootfs_initcall告诉编译器将函数populate_rootfs链接在段".initcall"部分。在内核初始化时,函数do_basic_setup调用do_initcalls,而do_initcalls执行段".initcall"中包含的函数,所以initramfs在此时被解压到rootfs中。
populate_rootfs解压initramfs的代码如下所示:
根据populate_rootfs的代码可见:
1)populate_rootfs首先调用unpack_to_rootfs将内核内置的initramfs解压到rootfs中;
2)接下来,如果变量initrd_start不为0,那么说明还有一个外部的initramfs通过Bootloader加载了,内核将这个外部的initramfs也释放到rootfs中。其中CONFIG_BLK_DEV_RAM是对应于使用ramdisk机制的情况,我们不关心这种情况。initrd_start是initramfs被加载到内存中的起始位置。initramfs通常作为一个独立的外部文件存在,并通过Bootloader加载到内存。
事实上,内核也允许将initramfs和内核映像构建在一起,统一通过内核加载,使用的方法是:首先将initramfs的内容保存到一个目录;然后将这个目录指定给kbuild,kbuild将使用自带的辅助程序gen_init_cpio将其压缩为initramfs;最后链接到内核映像的段".init.ramfs"中。这种方法的配置方式如图4-3所示。
图 4-3 指定initramfs的源文件所在目录
但是,即使我们没有指定将initramfs包含进内核映像中,内核也会构建一个内置的initramfs。这也是我们看到populate_rootfs代码中,第一个unpack_to_rootfs是无条件执行的原因。
接下来,我们就具体看看这个内置的initramfs的创建过程。
首先,内核的链接脚本告诉链接器将段".init.ramfs"中包含的内容链接到内核映像的"Init code and data"部分,链接脚本如下所示:
在".init.begin"和".init.end"之间的部分是内核初始化时使用的代码,在内核初始化完成后,将再无用处,因此,内核初始化完成后,这部分的代码将被释放。内核内置的initramfs就被包含在这里。我们先来看一下宏INIT_DATA_SECTION以及INIT_RAM_FS的定义:
由上可见,段".init.ramfs"被链接到了内核映像的"Init code and data"部分;函数unpack_to_rootfs中使用的符号__initramfs_start指向段".init.ramfs"的开头。
段".init.ramfs"的具体内容在文件initramfs_data.S中,代码如下:
通过伪指令".section.init.ramfs",链接器将initramfs链接进段".init.ramfs"。其中的INITRAMFS_IMAGE在Makefile中定义:
initramfs可以采用不同压缩方式,suffix_y是对应的后缀。比如,如果使用的是gzip压缩方式,则后缀为".gz";如果使用的是bzip2压缩方式,则后缀是".bz2";等等。当然也可以不必压缩,因为内核最终会被压缩。
显然,initramfs_data.S就是initramfs的内容(initramfs_data.cpio)的封装。另外在这个文件中定义了代表initramfs大小的符号__initramfs_size,这个符号也是函数populate_rootfs解压内置的initramfs时需要的。
接下来,我们就来看看具体的initramfs的内容initramfs_data.cpio,其创建规则的脚本如下所示:
关注创建initramfsdata.cpio规则的命令,其中if_changed这个表达式在讨论内核的构建时我们已见过多次,其核心就是执行命令cmd$1,这里对应的就是cmd_initfs,该命令定义如下:
其中涉及的三个参数的定义如下:
其中:
❑initramfs是scripts目录下的脚本gen_initramfs_list.sh。
❑ramfs-input指定了创建initramfs的输入。如果配置内核时指定了构成initramfs的源所在的目录,则使用这个源目录下的文件创建initramfs;否则,只传递一个参数"-d"给脚本gen_initramfs_list.sh,该脚本则用内核默认的内容创建initramfs。
❑ramfs-args这个参数只有在用户自己指定了构建initramfs的文件时才有效,默认是"-u 0-g 0",是告诉内核将这些文件的用户ID和组ID都设置为root。
综上所述,当指定了initramfs的源目录时,假设源目录为/vita/initramfs,那么构建initramfs的命令展开后大致如下:
脚本gen_initramfs_list.sh将/vita/initramfs目录下的文件的UID和GID全部设置为0,即root用户和组的ID,然后将目录下的内容打包为initramfs_data.cpio。
当不指定initramfs的源目录时,创建内置的initramfs的命令展开后大致如下:
通过命令行参数"-d",即"default initramfs",告诉脚本gen_initramfs_list.sh创建一个默认的initramfs,其包含的内容如下:
这个默认的initramfs非常简单,仅包括一个/dev目录,一个/dev/console设备节点以及一个/root目录。
最后还要指出的一点是,事实上,即使配置内核不支持initramfs,内核在内部依然会构建一个最小的initramfs。根据init目录下的Makefile,当配置内核不支持initramfs时,内核链接init下的noinitramfs.c,如下脚本所示:
文件noinitramfs.c的代码如下:
由代码"rootfs_initcall(default_rootfs)"可见,在没有配置内核支持initramfs的情况下,内核初始化时,依然会执行default_rootfs。而该函数将在rootfs中创建/dev、/root目录以及/dev/console节点。
可见,无论在什么情况下,内核都将确保有一个initramfs。那么内核为什么要这么做呢?因为第一个进程如果打不开控制台设备(/dev/console),那么其将异常终止,最终导致内核panic。所以,这个默认的initramfs确保了内核不会因为第一个进程打不开控制台设备而panic。从某种意义上,也可以将其看作内核虚拟文件系统的一个Bootstrap,也就是说,如果没人给内核提供一个最小的文件系统的内容,那么内核就自己创建一个。