1. 前言
本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
在《kernel启动流程-start_kernel的执行_7.arch_call_rest_init》中提到kernel_init->do_basic_setup->do_initcalls会遍历执行所有的init函数,这其中会执行populate_rootfs函数,populate_rootfs会将initrd释放到rootfs的“/”目录,本文重点介绍start_kernel的cpio initrd解包的主要流程.
kernel版本:5.10
平台:arm64
2. rootfs挂载
rest_init
\--kernel_thread(kernel_init, NULL, CLONE_FS);
|--kernel_init
| |--kernel_init_freeable
| | |--do_basic_setup
| | | |--populate_rootfs
| | | | //情形1,通过释放根文件系统到kernel rootfs根目录/
| | | |--unpack_to_rootfs(__initramfs_start, __initramfs_size);
| | | |--if (!initrd_start || IS_ENABLED(CONFIG_INITRAMFS_FORCE)) goto done
| | | | //情形2,通过dts传递的内存地址释放根文件系统到kernel rootfs根目录/
| | | |--unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);
| | | //ramdisk_execute_command默认为/init, init_eaccess返回非0,因此会通过prepare_namespace执行挂载
| | | //因此对于initramfs的用户,需要通过类似"rdinit=linuxrc"的方式指定,并通过init_access检查通过才会
| | | //对于ramdisk用户,不需要通过类似"rdinit=linuxrc"的方式指定
| | |--if (init_eaccess(ramdisk_execute_command) != 0)
| | ramdisk_execute_command = NULL
| | //情形3, 挂载rootfs文件系统到kernel rootfs根目录/
| | prepare_namespace()
\--if (ramdisk_execute_command)
run_init_process(ramdisk_execute_command)
用户制作的根文件系统该如何与内核对接呢?主要包含三种方式:
-
情况1. 用户制作的根文件系统通过initramfs的方式与内核镜像打包在一起,这种情况下的rootfs内核会进行cpio压缩;
此时主要会通过rest_init->kernel_init->populate_rootfs将用户根文件系统释放到内核的rootfs中。 -
情况2. 用户制作的根文件系统通过cpio的方式单独处理,不与内核镜像打包在一起;
仍然会通过rest_init->kernel_init->populate_rootfs路径,只不过这里会判断一下initrd_start是否被初始化过,它主要来源于dts的chosen节点解析的linux,initrd-start,如果initrd_start变量不为空,那么代表用户将根文件系统存放到内存的某个区域, -
情况3. 用户制作的根文件系统以某种文件系统格式化,形成文件系统镜像,内核在启动时,进行挂载
prepare_namespace,主要针对格式化rootfs为某种文件系统的情况,主要通过挂载用户制作的rootfs根文件系统镜像
注:这里主要通过init_eaccess检查是否能访问到某个文件(如linuxrc),如果能访问到则表示已经通过populate_rootfs 释放到内核根目录/,则不会执行prepare_namespace,否则将执行prepare_namespace挂载用户根文件系统
3.populate_rootfs
此处我们主要以与内核打包在一起的initramfs为例进行说明
populate_rootfs的声明位于init/initramfs.c:
rootfs_initcall(populate_rootfs);
将initrd释放到rootfs,实际是将initrd下的目录和文件及链接在rootfs的根目录/下再创建一遍,之后把cpio initrd的文件内容拷贝过去。那这个动作是在什么时候发生的呢?就是在populatge_rootfs中
populate_rootfs
|--unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start)
|--while (!message && len)
decompress(buf, len, NULL, flush_buffer, NULL,...)//only once
write_buffer(buf, len)
由于本文采用cpio initrd,调用unpack_to_rootfs对cpio initrd进行解包。unpack_to_rootfs最重要的是会调用write_buffer函数,它通过一个大循环对cpio initrd中打包进去的每一个文件进行处理。while循环首次会将cpio initrd的压缩文件进行解压缩。之后将通过write_buffer对解压缩后的cpio initrd进行处理。
write_buffer函数维护了一个状态机,不同的状态具有不同的处理函数
对每一个文件的处理将依次经过如下几种状态(以普通文件为例):
- Start:文件处理开始
- Collect:通过读取cpio initrd中每个文件的cpio头来收集文件信息
- GotHeader:对每个文件的cpio头进行解析,并保存到全局变量
- GotName:通过上步获取的cpio头信息获取文件访问属性,对文件进行不同处理,如对于普通文件则在当前进程的当前目录(current为init_task,current->fs->pwd为rootfs的/目录)下创建此文件
- CopyFile:将initrd中文件的内容拷贝到新创建的文件
- Reset:重置,以准备下一个文件的处理
经过如上的步骤就完成了cpio initrd释放到rootfs的“/”目录,之后就可以通过/init来访问为init程序,并为之创建进程,也就是1号进程了。
4.GotName的处理
下面以GotName为例,以cpio initrd下的init文件为例说明释放过程, GotName对应do_name处理函数:
static int __init do_name(void)
{
......
clean_path(collected, mode);
if (S_ISREG(mode)) {//以常规文件为例
int ml = maybe_link();
if (ml >= 0) {
int openflags = O_WRONLY|O_CREAT;
if (ml != 1)
openflags |= O_TRUNC;
wfile = filp_open(collected, openflags, mode);
if (IS_ERR(wfile))
return 0;
wfile_pos = 0;
vfs_fchown(wfile, uid, gid);
vfs_fchmod(wfile, mode);
if (body_len)
vfs_truncate(&wfile->f_path, body_len);
state = CopyFile;
}
}
......
}
由于init是一个普通文件因此会调用sys_open(collected, openflags, mode),对于init也就是:
sys_open("init", O_WRONLY|O_CREAT, mode)
sys_open系统调用如下:
sys_open
\--do_sys_open
\--do_filp_open
\--path_openat
|--path_init
|--link_path_walk
\--do_last->
\--lookup_open->
|--lookup_dcache//创建dentry
\--vfs_create//创建inode
lookup_dcache 为init创建dentry;vfs_create调用根inode->i_op即ramfs_file_inode_operations->create为init文件创建inode