Linux内核启动之根文件系统挂载
一、基本概念介绍
1.1 rootfs
什么是根文件系统?理论上说一个嵌入式设备如果内核能运行起来,且不需要用户进程的话(估计这种情况很少),是不需要文件系统的。
文件系统简单的说就是一种目录结构,由于linux
操作系统的设备在系统中是以文件的形式存在,将这些文件分类管理以及提供和内核交互的接口,就形成了一定的目录结构也就是文件系统。文件系统是为用户反映系统的一种形式,为用户提供一个检测控制系统的接口。
而根文件系统,是一种特殊的文件系统。那么根文件系统和普通的文件系统有什么区别呢?
- 当我们运行一个
linux
操作系统时,我们可以发现整个操作系统看起来就是一颗目录树,最顶层的目录为根目录/
,所有文件都组织在这个目录树,这个最顶层的文件系统就称为根文件系统,也就是就是内核启动时挂载的第一个文件系统; - 所有已安装的文件系统(即普通的文件系统)都作为根文件系统树的树叶出现在系统中,当我们新增一个设备(普通文件系统)时,需要将其挂载到目录树中的某个目录才能进行访问,这个目录也被称作挂载点;
由于根文件系统是启动时挂载的第一个文件系统,那么根文件系统就要包括linux
启动时所必须的目录和关键性的文件,例如linux
启动时都需要有用户进程init
对应的文件,在linux
挂载分区时一定要会找/etc/fstab
这个挂载文件等,根文件系统中还包括了许多的应用程序,如 /bin
目录下的命令等。任何linux
启动时所必须的文件的文件系统都可以称为根文件系统。
1.1.1 虚拟rootfs
虚拟rootfs
由内核自己创建和加载,仅仅存在于内存之中,其文件系统是tmpfs
类型或者ramfs
类型。
1.1.2 真实rootfs
真实rootfs
则是指根文件系统存在于存储设备上,内核在启动过程中会在虚拟rootfs
上挂载这个存储设备,然后将/
目录节点切换到这个存储设备上,这样存储设备上的文件系统就会被作为根文件系统使用。
1.2 initrd
initrd
总的来说目前有两种格式:image
格式和cpio
格式。
当系统启动的时候,uboot
会把initrd
文件读到内存中,然后把initrd
文件在内存中的起始地址和大小传递给内核;
- 可以通过
bootargs
参数initrd
指定其地址范围; - 也可以通过备树
dts
里的chosen
节点的中的linux,initrd-start
和linux,initrd-end
属性指定其地址范围;
内核在启动初始化过程中会解压缩initrd
文件,然后将解压后的initrd
挂载为根目录,然后执行根目录中的某个可执行文件;
cpio
格式的initrd
为/init
;image
格式的initrd
为/linuxrc
;
我们可以在这个脚本中加载真实文件系统。这样,就可以mount
真正的根目录,并切换到这个根目录中来。
注意:我们制作不同格式的initrd
时,启动脚本是不同:
- 如果制作的是
cpio-initrd
则需要包含/init
; - 如果制作的是
image-initrd
则需要包含/linuxrc
。
1.2.1 image-initrd
image-initrd
是将一块内存当作物理磁盘,然后在上面载入文件系统,比如我们在《Rockchip RK3399 - busybox 1.36.0
制作根文件系统》制作的ramdisk
文件系统就是就属于这一种。
image-initrd
格式镜像制作,进入到要制作的文件系统的根目录;
dd if=/dev/zero of=../initrd.img bs=512k count=5
mkfs.ext2 -F -m0 ../initrd.img
mount -t ext2 -o loop ../initrd.img /mnt
cp -r * /mnt
umount /mnt
gzip -9 ../initrd.img
1.2.1.1 内核ramdisk
配置
为了能够使用ramdisk
,内核必须要支持ramdisk
,即:在编译内核时,要选中如下配置;
Device Drivers --->
[*] Block devices --->
<*> RAM block device support
(1) Default number of RAM disks
(131072) Default RAM disk size (kbytes)
配置完成,会在.config
生成如下配置:
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=131072
同时为了让内核有能力在内核加载阶段就能装入ramdisk
,并运行其中的内容,要选中:
General setup --->
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
会在配置文件中定义CONFIG_BLK_DEV_INITRD
。
1.2.1.2 启动参数
在uboot
启动内核时指定根文件系统的位置;修改uboot
启动参数bootargs
中的root
属性为root=/dev/ram0
,表示根目录挂载点为/dev/ram0
块设备;
假设ramdisk.gz
文件被加载到内存指定位置0x42000000
。
我们可以修改bootargs
加入如下配置:
initrd=0x42000000,0x14000000
initrd
参数格式为:地址,长度,这里设备RAM
起始地址为0x42000000
,只要是在内核RAM
物理地址空间内即可。长度这里只要比ramdisk.gz
压缩包大小大就可以了。
此外我们还可以通过备树dts
里的chosen
节点中的linux,initrd-start
和linux,initrd-end
属性指定其地址范围;
chosen {
linux,initrd-start=0x42000000
linux,initrd-end=0x56000000
}
1.2.1.3 挂载方式
1)当系统启动的时候,uboot
会把image-initrd
文件读到内存中;
2)接着内核判断initrd
的文件格式,如果不是cpio
格式,将其作为image-initrd
处理;
3)内核将image-initrd
保存在rootfs
下的/initrd.image
中, 并将其读入/dev/ram0
中,根据root
是否等于/dev/ram0
做不同的处理;
root == /dev/ram0
:root=/dev/ram
是告诉内核我们最终要挂载的文件系统实际就是被拷贝的内存里的这个文件系统,不要让内核再去费力去挂载其他的文件系统了;- 进行正常启动过程,执行
/sbin/init
;
root != /dev/ram0
-
执行
initrd
上的/linuxrc
文件,linuxrc
通常是一个脚本文件,负责加载内核访问真正根文件系统必须的驱动,以及加载真正根文件系统; -
/linuxrc
执行完毕,真正的根文件系统被挂载,执行权交给内核; -
如果真正的根文件系统存在
/initrd
目录,那么/dev/ram0
将从/
移动到/initrd
;否则/dev/ram0
将被卸载; -
在真正的根文件系统上进行正常启动过程 ,执行
/sbin/init
;
-
1.2.2 cpio-initrd
特指使用cpio
格式创建的initrd
映像,和编译进内核的initramfs
格式是一样的,只不过它是独立存在的,也被称为外部initramfs
,
cpio-initrd
格式镜像制作,进入到要制作的文件系统的根目录;
find . | cpio -c -o > ../initrd.img
gzip ../initrd.img
1.2.2.1 内核配置
需要在make menuconfig
中配置以下选项就可以了:
General setup --->
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
1.2.2.2 启动参数
同image-initrd
启动参数。
1.2.2.3 挂载方式
1)当系统启动的时候,uboot
会把cpio-initrd
文件读到内存中;
2)接着内核判断initrd
的文件格式,如果是cpio
格式;
3)内核将cpio-initrd
释放到rootfs
,结束内核对cpio-initrd
的操作(rootfs
本身也是一个基于内存的文件系统。这样就省掉了ramdisk
的挂载、卸载等);
4)执行cpio-initrd
中的/init
文件,执行到这一点,内核的工作全部结束,完全交给/init
文件处理。也就是其实到了最后一步,内核就已经完成了自己所有的工作,直接移交给initrd
的/init
。
1.2.3 比较
两种格式镜像比较:
cpio-initrd
的制作方法比image-initrd
简单;cpio-initrd
的内核处理流程相比image-initrd
更简单,因为:- 根据上面的流程对比可知,
cpio-initrd
格式的镜像是释放到rootfs
中的,不需要额外的文件系统支持, 而image-initrd
格式的镜像先是被挂载成虚拟文件系统,而后被卸载,基于具体的文件系统; image-initrd
内核在执行完/linuxrc
进程后,还要返回执行内核进行一些收尾工作, 并且要负责执行真正的根文件系统/sbin/init
。
- 根据上面的流程对比可知,
1.3 initramfs
在linux 2.5
内核开始引入initramfs
技术,是一个基于ram
的文件系统,只支持cpio
格式。
它的作用和cpio-initrd
类似,initramfs
和内核一起编译到了一个新的镜像文件。
initramfs
被链接进了内核中特殊的数据段.init.ramfs
上,其中全局变量__initramfs_start
和__initramfs_end
分别指向这个数据段的起始地址和结束地址。内核启动时会对.init.ramfs
段中的数据进行解压,然后使用它作为临时的根文件系统。
1.3.1 内核配置
要制作这样的内核,我们只需要在make menuconfig
中配置以下选项就可以了:
General setup --->
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
(/opt/filesystem) Initramfs source file(s)
其中/opt/filesystem
就是根目录,这里可以使一个现成的gzip
压缩的cpio
,也可以使一个目录。
1.3.2 挂载方式
initramfs
和cpio-initrd
的区别, initramfs
是将cpio rootfs
编译进内核,而cpio-initrd
中cpio rootfs
是不编译入内核,是外部的。其挂载方式和cpio-initrd
是一致的。
1)当系统启动的时候,内核将initramfs
释放到rootfs
,结束内核对initramfs
的操作;
2)执行initramfs
中的/init
文件,加载真正的rootfs
,并启动了init
进程/sbin/init
;
二、源码分析
内核有关根文件系统挂载的调用链路如下:
start_kernel()
setup_arch(&command_line) // arch/arm64/kernel/setup.c
// __fdt_pointer:dtb所在的物理地址,由uboot通过x0寄存器传递过来
setup_machine_fdt(__fdt_pointer) // arch/arm64/kernel/setup.c
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
vfs_caches_init() // fs/dcache.c
mnt_init() // fs/namespace.c
// 注册类型未rootfs的文件系统
init_rootfs()
register_filesystem(&rootfs_fs_type)
nit_ramfs_fs()
register_filesystem(&ramfs_fs_type)
// 挂载rootfs类型的文件系统
init_mount_tree()
rest_init()
// 内核线程
kernel_init()
kernel_init_freeable()
// 初始化设备驱动,加载静态内核模块,启动所有直接编译进内核的模块
do_basic_setup()
// 按优先级调用内核初始化启动函数
// 启动所有在__initcall_start和__initcall_end段的函数,静态编译进内核的modules也会将其入口放置在这段区间
do_initcalls()
// 解压initramfs/initrd到rootfs
populate_rootfs()
unpack_to_rootfs((char *)initrd_start,initrd_end - initrd_start)
if(!ramdisk_execute_command)
ramdisk_execute_command="/init"
// 如果/init文件不存在,尝试使用prepare_namespace挂载根文件系统
// 如果有就不会调用prepare_namespace,即不会挂载其它根文件系统了
// 因此打语音image-initrd会执行如下流程,cpio-image则不会执行如下流程
if (ksys_access((const char __user *)
ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
// 执行某个可执行文件
run_init_process(ramdisk_execute_command)
2.1 解析bootargs
(setup_machine_fdt
)
我们在《Rockchip RK3588 - uboot
引导方式介绍》文章中分析了内核是如何解析得到command_line
的。
这里我们再次回顾一下setup_machine_fdt
函数,定位到arch/arm64/kernel/setup.c
;
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
int size;
// 转换成虚拟地址,并做合法性检查,如地址对齐等
void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
const char *name;
if (dt_virt)
memblock_reserve(dt_phys, size);
if (!dt_virt || !early_init_dt_scan(dt_virt)) {
pr_crit("\n"
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
"\nPlease check your bootloader.",
&dt_phys, dt_virt);
while (true)
cpu_relax();
}
/* Early fixups are done, map the FDT as read-only now */
fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
name = of_flat_dt_get_machine_name();
if (!name)
return;
pr_info("Machine model: %s\n", name);
dump_stack_set_arch_desc("%s (DT)", name);
}
这里我们重点关注early_init_dt_scan
函数,early_init_dt_scan
主要是对dtb
进行早期的扫描工作,下面是简要介绍函数的调用流程和实现细节。
early_init_dt_scan(dt_virt) // drivers/of/fdt.c
// 对dtb头进行检查
early_init_dt_verify(dt_virt)
early_init_dt_scan_nodes() // 遍历设备树的节点,解析出重要的信息用于内核启动
/* Retrieve various information from the /chosen node */
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
2.1.1 of_scan_flat_dt
of_scan_flat_dt
对dtb
里面的所有节点进行扫描,用提供的回调函数循环处理节点信息,回调函数返回0继续扫描,返回非0结束扫描,当扫描到最后一个节点也会结束扫描;
/**
* of_scan_flat_dt - scan flattened tree blob and call callback on each.
* @it: callback function
* @data: context data pointer
*
* This function is used to scan the flattened device-tree, it is
* used to extract the memory information at boot before we can
* unflatten the tree
*/
int __init of_scan_flat_dt(int (*it)(unsigned long node,
const char *uname, int depth,
void *data),
void *data)
{
//dtb数据的地址,也就是根节点的地址
const void *blob = initial_boot_params;
const char *pathp;
int offset, rc = 0, depth = -1;
if (!blob)
return 0;
// 从根节点遍历dtb中每个节点,返回的offset就是每个节点的地址
// offset:表示节点的地址相对于根节点的偏移量,也是节点数据所在地址
// depth:代表节点相对于根节点的深度,比如根节点深度是0,/chosen节点是1
for (offset = fdt_next_node(blob, -1, &depth);
offset >= 0 && depth >= 0 && !rc;
offset = fdt_next_node(blob, offset, &depth)) {
// 解析出节点名称
pathp = fdt_get_name(blob, offset, NULL);
if (*pathp == '/')
pathp = kbasename(pathp);
// 回调函数解析节点,it是传递进来的设备树节点的解析函数,需要解析什么消息就传递进来相应的节点解析函数
rc = it(offset, pathp, depth, data);
}
return rc;
}
2.1.2 early_init_dt_scan_chosen
early_init_dt_scan_chosen
用于扫描设备树chosen
节点,并把bootargs
属性值拷贝到boot_command_line
中,如果内核定义了CONFIG_CMDLINE
这个宏,则把配置的命令行参数也拷贝到boot_command_line
;
/*
* Convert configs to something easy to use in C code
*/
#if defined(CONFIG_CMDLINE_FORCE)
static const int overwrite_incoming_cmdline = 1;
static const int read_dt_cmdline;
static const int concat_cmdline;
#elif defined(CONFIG_CMDLINE_EXTEND)
static const int overwrite_incoming_cmdline;
static const int read_dt_cmdline = 1;
static const int concat_cmdline = 1;
#else /* CMDLINE_FROM_BOOTLOADER */ // 走这里
static const int overwrite_incoming_cmdline;
static const int read_dt_cmdline = 1;
static const int concat_cmdline;
#endif
#ifdef CONFIG_CMDLINE
static const char *config_cmdline = CONFIG_CMDLINE;
#else
static const char *config_cmdline = "";
#endif
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
int depth, void *data)
{
int l = 0;
const char *p = NULL;
char *cmdline = data; // 即boot_command_line
const void *rng_seed;
pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);
// 节点的深度要为1,数据不能使NULL,同时节点名字是"chosen"或者"chosen@0"
if (depth != 1 || !cmdline ||
(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
return 0;
// 解析initrd相关
early_init_dt_check_for_initrd(node);
/* Put CONFIG_CMDLINE in if forced or if data had nothing in it to start */
if (overwrite_incoming_cmdline || !cmdline[0]) // 进入
strlcpy(cmdline, config_cmdline, COMMAND_LINE_SIZE);
/* Retrieve command line unless forcing */
if (read_dt_cmdline) // 从chosen节点中解析出bootargs属性
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0) {
if (concat_cmdline) {
int cmdline_len;
int copy_len;
strlcat(cmdline, " ", COMMAND_LINE_SIZE);
cmdline_len = strlen(cmdline);
copy_len = COMMAND_LINE_SIZE - cmdline_len - 1;
copy_len = min((int)l, copy_len);
strncpy(cmdline + cmdline_len, p, copy_len);
cmdline[cmdline_len + copy_len] = '\0';
} else { // 追加bootargs参数到boot_command_line
strlcpy(cmdline, p, min((int)l, COMMAND_LINE_SIZE));
}
}
pr_debug("Command line is: %s\n", (char*)data);
rng_seed = of_get_flat_dt_prop(node, "rng-seed", &l);
if (rng_seed && l > 0) {
add_bootloader_randomness(rng_seed, l);
/* try to clear seed so it won't be found. */
fdt_nop_property(initial_boot_params, node, "rng-seed");
/* update CRC check value */
of_fdt_crc32 = crc32_be(~0, initial_boot_params,
fdt_totalsize(initial_boot_params));
}
/* break now */
return 1;
}
2.1.3 early_init_dt_check_for_initrd
early_init_dt_check_for_initrd
用于解析initrd
,函数定义在drivers/of/fdt.c
;
#ifdef CONFIG_BLK_DEV_INITRD
#ifndef __early_init_dt_declare_initrd
static void __early_init_dt_declare_initrd(unsigned long start,
unsigned long end)
{
// 从设备树中解析出initrd的start和end地址后,转换为虚拟地址分别赋值给相应的变量
initrd_start = (unsigned long)__va(start);
printk(KERN_INFO "initramfs start address: 0x%lx 0x%lx\n", start,initrd_start);
initrd_end = (unsigned long)__va(end);
initrd_below_start_ok = 1;
}
#endif
/**
* early_init_dt_check_for_initrd - Decode initrd location from flat tree
* @node: reference to node containing initrd location ('chosen')
*/
static void __init early_init_dt_check_for_initrd(unsigned long node)
{
u64 start, end;
int len;
const __be32 *prop;
pr_debug("Looking for initrd properties... ");
// 获取initrd的start地址
prop = of_get_flat_dt_prop(node, "linux,initrd-start", &len);
if (!prop)
return;
start = of_read_number(prop, len/4);
// 获取initrd的end地址
prop = of_get_flat_dt_prop(node, "linux,initrd-end", &len);
if (!prop)
return;
end = of_read_number(prop, len/4);
__early_init_dt_declare_initrd(start, end);
pr_debug("initrd_start=0x%llx initrd_end=0x%llx\n",
(unsigned long long)start, (unsigned long long)end);
}
2.1.4 解析bootargs
上面的分析setup_machine_fdt
中得到两个重点:
- 从
chosen
节点中解析得到initrd_start
和initrd_end
,后面解压initrd
的时候会用到; - 从
chosen
节点中解析得到bootargs
,后面还会解析bootargs
参数。
下面就是关注解析bootargs
中的initrd
、init
、rdinit
等参数,解析后放在全局变量里;
#ifdef CONFIG_BLK_DEV_INITRD
// 如果命令行指定了initrd参数,通过解析命令行中的initrd初始化initrd_start、initrd_end
static int __init early_initrd(char *p)
{
unsigned long start, size;
char *endp;
start = memparse(p, &endp);
if (*endp == ',') {
size = memparse(endp + 1, NULL);
initrd_start = start;
initrd_end = start + size;
}
return 0;
}
early_param("initrd", early_initrd); // arch/arm64/mm/init.c
#endif
// 如果命令行指定了init参数,通过解析命令行中的init初始化execute_command
static int __init init_setup(char *str) // init/main.c
{
unsigned int i;
execute_command = str;
/*
* In case LILO is going to boot us with default command line,
* it prepends "auto" before the whole cmdline which makes
* the shell think it should execute a script with such name.
* So we ignore all arguments entered _before_ init=... [MJ]
*/
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);
// 如果命令行指定了rdinit参数,通过解析命令行中的rdinit初始化ramdisk_execute_command
static int __init rdinit_setup(char *str) // init/main.c
{
unsigned int i;
ramdisk_execute_command = str;
/* See "auto" comment in init_setup */
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("rdinit=", rdinit_setup);
// 如果命令行指定了root参数,通过解析命令行中的root初始化saved_root_name
static int __init root_dev_setup(char *line) // init/do_mounts.c
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup);
static int __init rootwait_setup(char *str) // init/do_mounts.c
{
if (*str)
return 0;
root_wait = 1;
return 1;
}
__setup("rootwait", rootwait_setup);
linux
源代码里面大量类似于__setup
这类宏定义的写法,将一类函数放到一个具体的代码段里面,然后再执行该代码段里函数。上面的函数调用链如下:
start_kernel()
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
其中unknown_bootoption
:
static bool __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
bool had_early_param = false;
p = __setup_start;
do {
int n = strlen(p->str);
if (parameqn(line, p->str, n)) {
if (p->early) {
/* Already done in parse_early_param?
* (Needs exact match on param part).
* Keep iterating, as we can have early
* params and __setups of same names 8( */
if (line[n] == '\0' || line[n] == '=')
had_early_param = true;
} else if (!p->setup_func) {
pr_warn("Parameter %s is obsolete, ignored\n",
p->str);
return true;
} else if (p->setup_func(line + n))
return true;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
/*
* Unknown boot options get handed to init, unless they look like
* unused parameters (modprobe will find them in /proc/cmdline).
*/
static int __init unknown_bootoption(char *param, char *val,
const char *unused, void *arg)
{
repair_env_string(param, val, unused, NULL);
/* Handle obsolete-style parameters */
if (obsolete_checksetup(param))
return 0;
/* Unused module parameter. */
if (strchr(param, '.') && (!val || strchr(param, '.') < val))
return 0;
if (panic_later)
return 0;
if (val) {
/* Environment option */
unsigned int i;
for (i = 0; envp_init[i]; i++) {
if (i == MAX_INIT_ENVS) {
panic_later = "env";
panic_param = param;
}
if (!strncmp(param, envp_init[i], val - param))
break;
}
envp_init[i] = param;
} else {
/* Command line option */
unsigned int i;
for (i = 0; argv_init[i]; i++) {
if (i == MAX_INIT_ARGS) {
panic_later = "init";
panic_param = param;
}
}
argv_init[i] = param;
}
return 0;
}
2.2 VFS
的注册(mnt_init
)
首先不得不从linux
系统的函数start_kernel
说起。函数start_kernel
中会去调用vfs_caches_init
来初始化VFS
,函数位于fs/dcache.c
;
void __init vfs_caches_init(void)
{
names_cachep = kmem_cache_create_usercopy("names_cache", PATH_MAX, 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, 0, PATH_MAX, NULL);
dcache_init();
inode_init();
files_init();
files_maxfiles_init();
mnt_init();
bdev_cache_init();
chrdev_init();
}
函数mnt_init
会注册rootfs
类型的文件系统,这是个虚拟的rootfs
,即内存文件系统,后面还会指向真实的文件系统。
可能有人会问,为什么不直接把真实的文件系统配置为根文件系统?
答案很简单,内核中没有根文件系统的设备驱动,如usb
、eMMC
等存放根文件系统的设备驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,其实所有的驱动是由在后面的kernel_init
线程进行加载,所以需要initrd/initramfs
。
mnt_init
函数位于fs/namespace.c
:
void __init mnt_init(void)
{
int err;
// 创建一个内存缓存 mnt_cache
mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct mount),
0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
// 分配一个大哈希表用于存储挂载点的缓存
mount_hashtable = alloc_large_system_hash("Mount-cache",
sizeof(struct hlist_head),
mhash_entries, 19,
HASH_ZERO,
&m_hash_shift, &m_hash_mask, 0, 0);
// 类似地,分配一个哈希表用于存储具体的挂载点。
mountpoint_hashtable = alloc_large_system_hash("Mountpoint-cache",
sizeof(struct hlist_head),
mphash_entries, 19,
HASH_ZERO,
&mp_hash_shift, &mp_hash_mask, 0, 0);
if (!mount_hashtable || !mountpoint_hashtable)
panic("Failed to allocate mount hash table\n");
kernfs_init();
// 初始化sysfs文件系统,最终会挂载到/sys/目录下
err = sysfs_init();
if (err)
printk(KERN_WARNING "%s: sysfs_init error: %d\n",
__func__, err);
// 在/sys/下创建fs目录
fs_kobj = kobject_create_and_add("fs", NULL);
if (!fs_kobj)
printk(KERN_WARNING "%s: kobj create error\n", __func__);
// 注册rootfs类型的文件系统
init_rootfs();
// 挂载rootfs类型的文件系统
init_mount_tree();
}
mnt_init
主要实现以下功能:
- 调用
init_rootfs
注册rootfs
类型的文件系统; - 调用
init_mount_tree
挂载rootfs
类型的文件系统,rootfs
是作为根文件系统挂载的,并将进程的当前路径和根路径切换到rootfs
,后面其他文件系统只能挂载到rootfs
下面。
2.2.1 init_rootfs
init_rootfs
定义在init/do_mounts.c
,用于向linux
内核注册rootfs
类型的文件系统。定义如下:
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.mount = rootfs_mount,
.kill_sb = kill_litter_super,
};
int __init init_rootfs(void)
{
// 注册rootfs类型的文件系统,注意rootfs是一种文件系统的名字
int err = register_filesystem(&rootfs_fs_type);
if (err)
return err;
// 注册ramfs/tmpfs文件系统类型,都是基于ram的文件系统,tmpfs是ramfs的一个变种,rootfs也是 ramfs/tmpfs的一个特殊例子
if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
(!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
err = shmem_init();
is_tmpfs = true;
} else {
err = init_ramfs_fs();
}
if (err)
unregister_filesystem(&rootfs_fs_type);
return err;
}
2.2.2 init_mount_tree
init_mount_tree
函数位于fs/namespace.c
,用于挂载rootfs
类型的文件系统;
static void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct mnt_namespace *ns;
struct path root;
struct file_system_type *type;
// 查找rootfs的类型定义结构体
type = get_fs_type("rootfs");
if (!type)
panic("Can't find rootfs type");
// 将rootfs挂载到vfs中,这是第一个挂载到vfs的文件系统,所以也是根文件系统,其他文件系统只能挂载到rootfs下面
mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
put_filesystem(type);
if (IS_ERR(mnt))
panic("Can't create rootfs");
// 创建第一个挂载命令空间
ns = create_mnt_ns(mnt);
if (IS_ERR(ns))
panic("Can't allocate initial namespace");
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);
// 初始化根文件系统的路径
root.mnt = mnt;
root.dentry = mnt->mnt_root;
mnt->mnt_flags |= MNT_LOCKED;
// 将当前线程工作路径和根文件系统的路径设置为刚挂载的rootfs
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
}
2.3 VFS
的挂载(rest_init
)
接下来,start_kernel
会去调用rest_init
并会去创建系统中的第一个线程kernel_init
,并由其调用所有模块的初始化函数,其中rootfs
的初始化函数也在这个期间被调用。
static noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
// 初始化调度器,以准备调度
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*
* 1. 创建初始化线程kernel_init,并获取其ID
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/*
* Pin init on the boot CPU. Task migration is not properly working
* until sched_init_smp() has been run. It will set the allowed
* CPUs for init to the non isolated CPUs.
*/
rcu_read_lock();
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
rcu_read_unlock();
numa_default_policy();
// 创建内核线程kthreadd
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
/*
* Enable might_sleep() and smp_processor_id() checks.
* They cannot be enabled earlier because with CONFIG_PREEMPT=y
* kernel_thread() would trigger might_sleep() splats. With
* CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
* already, but it's stuck on the kthreadd_done completion.
*/
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);
}
内核线程kernel_init
定义如下:
static int __ref kernel_init(void *unused)
{
int ret;
// 2. 重要
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
ftrace_free_init_mem();
jump_label_invalidate_initmem();
free_initmem();
mark_readonly();
/*
* Kernel mappings are now finalized - update the userspace page-table
* to finalize PTI.
*/
pti_finalize();
system_state = SYSTEM_RUNNING;
numa_default_policy();
rcu_end_inkernel_boot();
// ramdisk_execute_command值通过rdinit=指定,如果未指定,则在kernel_init_freeable中被设置为/init
if (ramdisk_execute_command) {
// 启动进程ramdisk_execute_command
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*
* execute_command值通过init=指定,如果未指定,则跳过
*/
if (execute_command) {
// 启动进程execute_command
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
// 如果uboot传过来的命令行参数没有init=xxx或者rdinit=xxx,则会执行该进程
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
}
上面的代码很明显,按照顺序依次执行,直至某个执行成功:
- 先执行
ramdisk_execute_command
,即由rdinit=xx
参数指定;如果未指定,则在kernel_init_freeable
中被设置为/init
; - 接着执行
execute_command
,即由init=xx
参数指定; - 最后按照
/sbin/init
、/etc/init
、/bin/init
和/bin/sh
的过程执行,都不存在那肯定是启动不了。
2.3.1 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]);
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
workqueue_init();
init_mm_internals();
do_pre_smp_initcalls();
lockup_detector_init();
smp_init();
sched_init_smp();
#ifdef CONFIG_ROCKCHIP_THUNDER_BOOT
kthread_run(defer_free_bootmem, NULL, "defer_mem");
#endif
page_alloc_init_late();
/* Initialize page ext after all struct pages are initialized. */
page_ext_init();
// 重要,调用所有模块的初始化函数,包括initramfs的初始化函数populate_rootfs
do_basic_setup();
#if IS_BUILTIN(CONFIG_INITRD_ASYNC)
async_synchronize_full();
#endif
/* Open the /dev/console on the rootfs, this should never fail */
if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("Warning: unable to open an initial console.\n");
(void) ksys_dup(0);
(void) ksys_dup(0);
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
// 如果未指定rdinit=xx,则设置ramdisk_execute_command=/init
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
// 检查initrd/initramfs文件系统中是否存在文件ramdisk_execute_command,如果不存在的话执prepare_namespace挂载根文件系统
if (ksys_access((const char __user *)
ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*
* rootfs is available now, try loading the public keys
* and default modules
*/
integrity_load_keys();
load_default_modules();
}
其中do_basic_setup
定义如下:
/*
* Ok, the machine is now initialized. None of the devices
* have been touched yet, but the CPU subsystem is up and
* running, and memory and process management works.
*
* Now we can finally start doing some real work..
*/
static void __init do_basic_setup(void)
{
cpuset_init_smp();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
// 内核模块的初始化函数都在这里执行
do_initcalls();
}
static void __init do_initcalls(void)
{
int level;
#ifdef CONFIG_INITCALL_ASYNC
initcall_init_workers();
#endif
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
#ifdef CONFIG_INITCALL_ASYNC
initcall_free_works();
#endif
}
do_initcalls
将按顺序从由__initcall_start
开始,到__initcall_end
结束的section
中以函数指针的形式取出这些编译到内核的驱动模块中初始化函数起始地址,来依次完成相应的初始化。
而这些初始化函数由__define_initcall(level,fn)
指示编译器在编译的时候,将这些初始化函数的起始地址值按照一定的顺序放在这个section
中。
其中就包括initramfs
的初始化函数populate_rootfs
。
2.3.2 run_init_process
run_init_process
函数的主要作用是启动一个初始化进程,通常是linux
系统启动时的第一个用户空间进程。
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
pr_info("Run %s as init process\n", init_filename);
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}
三、kernel_init_freeable
kernel_init_freeable
函数调用链如下:
kernel_init_freeable()
do_basic_setup()
do_initcalls()
// 解压initramfs/initrd到rootfs
populate_rootfs()
unpack_to_rootfs((char *)initrd_start,initrd_end - initrd_start)
if(!ramdisk_execute_command)
ramdisk_execute_command="/init"
// 如果init文件不存在,尝试使用prepare_namespace挂载根文件系统
// 因此对于image-initrd会执行如下流程,cpio-image则不会执行如下流程
if (ksys_access((const char __user *)
ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
其中有两个重要的子函数;
populate_rootfs
:主要完成initramfs/initrd
的检测工作;- 检测是否存在
initramfs
格式的文件系统,存在则直接释放到rootfs
; - 检测是
cpio-initrd
还是image-initrd
,并分别处理;- 若是
cpio-init
则直接释放到rootfs
,反之将image-initrd
保存在/initrd.image
; - 释放
initrd
所占用的内存空间;
- 若是
- 检测是否存在
prepare_namespace
:在initrd/intramfs
解压后,如果可执行文件init
不存在,尝试使用prepare_namespace
挂载根文件系统;- 因此对于
image-initrd
会执行prepare_namespace
函数; - 而对于
cpio-image
则不会执行prepare_namespace
函数;
- 因此对于
3.1 populate_rootfs
init/initramfs.c
文件实现了initramfs
的初始化,入口函数为populate_rootfs
;
#if IS_BUILTIN(CONFIG_INITRD_ASYNC)
#include <linux/kthread.h>
#include <linux/async.h>
static void __init unpack_rootfs_async(void *unused, async_cookie_t cookie)
{
populate_rootfs();
}
static int __init populate_rootfs_async(void)
{
async_schedule(unpack_rootfs_async, NULL);
return 0;
}
pure_initcall(populate_rootfs_async);
#else
rootfs_initcall(populate_rootfs);
#endif
populate_rootfs
函数将会解压initramfs/initrd
到rootfs
,函数定义如下:
static int __initdata do_dump_initrd;
static int __init dump_initrd_param(char *str)
{
// 字符串非空,即dump_initrd=xx
if (*str)
return 0;
// dump_initrd= 则设置为1
do_dump_initrd = 1;
return 1;
}
__setup("dump_initrd", dump_initrd_param);
static int __init populate_rootfs(void)
{
char *err;
// 正常不会进入
if (do_skip_initramfs) {
if (initrd_start)
free_initrd();
return default_rootfs();
}
/* Load the built in initramfs */
// 1. 检测是否initramfs文件系统,解压initramfs(即链接到内核的cpio-initrd)到rootfs
err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
if (err)
panic("%s", err); /* Failed to decompress INTERNAL initramfs */
/* If available load the bootloader supplied initrd */
// 2. 检测是cpio-initrd还是image-initrd无论这两种格式,uboot都会把它加载到内存里的initrd_start地址处
// IS_ENABLED(CONFIG_INITRAMFS_FORCE) :存在参数配置y或m,则返回1,反之返回0
if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) {
// 对于image-initrd,会使用内存当作物理磁盘,然后在上面载入文件系统,因此必须要配制CONFIG_BLK_DEV_RAM才会支持image-initrd
#ifdef CONFIG_BLK_DEV_RAM
int fd;
printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
// 2.1 判断加载的是不是cpio-initrd文件,若是直接将解压到rootfs,解压成功返回0
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start);
// 解压成功,则进入
if (!err) {
// 如果uboot设置了dump_initrd=,当做image-initrd处理
if (do_dump_initrd)
goto dump;
// 如果是cpio-image,则跳转到done
goto done;
}
// 失败才会走这里
clean_rootfs();
unpack_to_rootfs(__initramfs_start, __initramfs_size);
// 如果执行到这里,就说明是image-initrd,将其内容保存到文件/initrd.image中
printk(KERN_INFO "rootfs image is not initramfs (%s)"
"; looks like an initrd\n", err);
dump:
// 2.2 处理image-initrd,将image-initrd保存在rootfs下的/initrd.image中,
fd = ksys_open("/initrd.image",
O_WRONLY|O_CREAT, 0700);
if (fd >= 0) {
//将intird_start到initrd_end内容保存到/initrd.image文件中
ssize_t written = xwrite(fd, (char *)initrd_start,
initrd_end - initrd_start);
if (written != initrd_end - initrd_start)
pr_err("/initrd.image: incomplete write (%zd != %ld)\n",
written, initrd_end - initrd_start);
ksys_close(fd);
}
done:
/* empty statement */;
#else
// 不存在image-initrd,说明此时是cpio-initrd文件
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);
#endif
}
// 释放initrd_start和init_end之间的地址空间
free_initrd();
flush_delayed_fput();
/*
* Try loading default modules from initramfs. This gives
* us a chance to load before device_initcalls.
*/
load_default_modules();
return 0;
}
从上上面的代码可以看出,cpio-initrd
和initramfs
其实是一回事,只是传递给内核的方式不同;
initramfs
是通过将cpio-initrd
链接到内核方式;- 而
cpio-initrd
是通过uboot
的bootargs
或者设备树chosen
的属性传递给内核,解压函数都是unpack_to_rootfs
,可直接解压到rootfs
。
3.2 prepare_namespace
在分析kernel_init_freeable
时我们知道,在initrd/intramfs
解压后,校验可执行文件init
是否存在,如果init
文件不存在,尝试使用prepare_namespace
挂载根文件系统;
如果我们制作的是image-initrd
则不包含/init
文件,包含的是/linuxrc
,因此会执行prepare_namespace
,对于cpio-image
并不会执行该函数。
prepare_namespace
调用链如下;
prepare_namespace()
initrd_load()
1. 命令行未指定noinitrd参数(通常会走这里)
rd_load_image("initrd.image")
1.1 如果root != /dev/ram0
则调用handle_init通过linuxrc启动用户态
1.2 如果root == /dev/ram0
通过mount_root将/dev/ram0设备中的根文件系统挂载到/root目录,进入该目录并将/root设置为当前目录
通过ksys_mount将/root挂载到/目录
通过ksys_chroot将当前进程的根目录切换成当前目录
2. 命令行指定noinitrd参数
通过mount_root将root指定的设备中的根文件系统挂载到/root目录,进入该目录并将/root设置为当前目录
通过ksys_mount将/root挂载到/目录
通过ksys_chroot将当前进程的根目录切换成当前目录
这下,不管用的是initrd/initramfs
还是NFS
或者Flash
设备上文件系统,都已经完成了挂载,下一步就是执行某个可执行文件了,具体查看kernel_init
函数。
函数定义在init/do_mounts.c
;
/*
* Prepare the namespace - decide what/where to mount, load ramdisks, etc.
*/
void __init prepare_namespace(void)
{
int is_floppy;
// 过配置的延时,则等待
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设备,安装md设备驱动
md_run_setup();
// 启动参数指定了root,saved_root_name存放的是启动参数root=所指定的设备文件
if (saved_root_name[0]) {
root_device_name = saved_root_name;
// 判断指定的root=前3个字符是否是mtd或ubi
if (!strncmp(root_device_name, "mtd", 3) ||
!strncmp(root_device_name, "ubi", 3)) {
// mtdxx 挂载到根文件系统
mount_block_root(root_device_name, root_mountflags);
goto out;
}
// 通过解析设备的名称来获取设备的主、从设备号
ROOT_DEV = name_to_dev_t(root_device_name);
// 比较前5个字节是否为/dev/,嵌入式设备也常将Flash上的文件系统挂载为根文件系统,通常形如root=/dev/mtdblock2、root=/dev/mmcblk0p2
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
// image-initrd情况,root!=/dev/ram0时,进入
if (initrd_load())
goto out;
// 满足如下条件会走下面的逻辑
// 1. 命令行指定了noinitrd参数
// 2. 命令行没有指定noinitrd参数,但是指定了root==/dev/ram0
/* wait for any asynchronous scanning to complete */
/* 等待异步的scan完成 */
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(5);
async_synchronize_full();
}
// 指定的根文件系统在软盘的情况
is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
if (is_floppy && rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
// 挂载文件系统(root所指定的设备)到/目录
mount_root();
out:
// 挂载devtmpfs到/dev/目录
devtmpfs_mount("dev");
// 当前目录(上面设置的/root)挂载到/目录
ksys_mount(".", "/", NULL, MS_MOVE, NULL);
// 将当前进程的根目录切换成当前目录*
ksys_chroot(".");
}
3.2.1 initrd_load
在使用了image-initrd
情景下,命令行未指定noinitrd
参数,则执行initrd_load
函数,函数定义在init/do_mounts_initrd.c
;
// 如果命令行指定了noinitrd参数,通过解析命令行中的noinitrd初始化mount_initrd=0
static int __initdata mount_initrd = 1;
static int __init no_initrd(char *str)
{
mount_initrd = 0;
return 1;
}
__setup("noinitrd", no_initrd);
bool __init initrd_load(void)
{
// 命令行未指定noinitrd,则进入
if (mount_initrd) {
// 创建/dev/ram设备,设备编号为Root_RAM0
// 其中:Root_RAM0 = MKDEV(RAMDISK_MAJOR, 0), 定义一个特定的设备编号,表示第一个 RAM磁盘设备
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.
* 1. 调用rd_load_image将/initrd.image释放到/dev/ram0
* 2. 如果:
* root!=/dev/ram0,则进入该函数,执行handle_initrd,并返回true
* root==/dev/ram0,则不进入该函数,并返回false
*/
if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
ksys_unlink("/initrd.image");
// 执行initrd中的linuxrc文件,并且将真实的根目录设置为当前目录
handle_initrd();
return true;
}
}
ksys_unlink("/initrd.image");
return false;
}
3.2.1.1 rd_load_image
其中rd_load_image
定义在init/do_mounts_rd.c
,函数主要实现如下功能:
-
分别打开
initrd_load
在rootfs
中的创建的/dev/ram0
设备节点和image-initrd
镜像/initrd.image
; -
调用
crd_load
将initrd.image
内容加载到/dev/ram0
设备节点中。
函数定义如下:
int __init rd_load_image(char *from)
{
int res = 0;
int in_fd, out_fd;
unsigned long rd_blocks, devblocks;
int nblocks, i, disk;
char *buf = NULL;
unsigned short rotate = 0;
decompress_fn decompressor = NULL;
#if !defined(CONFIG_S390)
char rotator[4] = { '|' , '/' , '-' , '\\' };
#endif
// 1. 打开rootfs /dev/ram0设备节点
out_fd = ksys_open("/dev/ram", O_RDWR, 0);
if (out_fd < 0)
goto out;
// 2. 打开image-initrd镜像/initrd.image
in_fd = ksys_open(from, O_RDONLY, 0);
if (in_fd < 0)
goto noclose_input;
nblocks = identify_ramdisk_image(in_fd, rd_image_start, &decompressor);
if (nblocks < 0)
goto done;
if (nblocks == 0) {
// 加载/initrd.image到/dev/ram0
if (crd_load(in_fd, out_fd, decompressor) == 0)
goto successful_load;
goto done;
}
/*
* NOTE NOTE: nblocks is not actually blocks but
* the number of kibibytes of data to load into a ramdisk.
*/
if (ksys_ioctl(out_fd, BLKGETSIZE, (unsigned long)&rd_blocks) < 0)
rd_blocks = 0;
else
rd_blocks >>= 1;
if (nblocks > rd_blocks) {
printk("RAMDISK: image too big! (%dKiB/%ldKiB)\n",
nblocks, rd_blocks);
goto done;
}
/*
* OK, time to copy in the data
*/
if (ksys_ioctl(in_fd, BLKGETSIZE, (unsigned long)&devblocks) < 0)
devblocks = 0;
else
devblocks >>= 1;
if (strcmp(from, "/initrd.image") == 0)
devblocks = nblocks;
if (devblocks == 0) {
printk(KERN_ERR "RAMDISK: could not determine device size\n");
goto done;
}
buf = kmalloc(BLOCK_SIZE, GFP_KERNEL);
if (!buf) {
printk(KERN_ERR "RAMDISK: could not allocate buffer\n");
goto done;
}
printk(KERN_NOTICE "RAMDISK: Loading %dKiB [%ld disk%s] into ram disk... ",
nblocks, ((nblocks-1)/devblocks)+1, nblocks>devblocks ? "s" : "");
for (i = 0, disk = 1; i < nblocks; i++) {
if (i && (i % devblocks == 0)) {
pr_cont("done disk #%d.\n", disk++);
rotate = 0;
if (ksys_close(in_fd)) {
printk("Error closing the disk.\n");
goto noclose_input;
}
change_floppy("disk #%d", disk);
in_fd = ksys_open(from, O_RDONLY, 0);
if (in_fd < 0) {
printk("Error opening disk.\n");
goto noclose_input;
}
printk("Loading disk #%d... ", disk);
}
ksys_read(in_fd, buf, BLOCK_SIZE);
ksys_write(out_fd, buf, BLOCK_SIZE);
#if !defined(CONFIG_S390)
if (!(i % 16)) {
pr_cont("%c\b", rotator[rotate & 0x3]);
rotate++;
}
#endif
}
pr_cont("done.\n");
successful_load:
res = 1;
done:
ksys_close(in_fd);
noclose_input:
ksys_close(out_fd);
out:
kfree(buf);
ksys_unlink("/dev/ram");
return res;
}
3.2.1.2 handle_initrd
如果root!=/dev/ram0
,则执行handle_initrd
通过/linuxrc
启动用户进程。handle_initrd
定义在init/do_mounts_rd.c
;
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);
// 在rootfs根目录创建真正的根文件系设备节点:/dev/root 其设备号是Root_RAM0的值,因此这个节点对应是/dev/ram0的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);
// 创建/old设备节点
ksys_mkdir("/old", 0700);
// 切换到/old目录下
ksys_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 */
ksys_mount("..", ".", NULL, MS_MOVE, NULL);
/* switch root and cwd back to / of rootfs */
ksys_chroot("..");
// 如果real_root_dev直接配置为Root_RAM0,也即直接使用直接使用initrd作为realfs,改变当前目录到initrd中,并直接返回
// 如果real_root_dev在linuxrc中重新设成Root_RAM0,则initrd就是最终真实的文件系统,改变当前目录到initrd中,不作后续处理直接返回
if (new_decode_dev(real_root_dev) == Root_RAM0) {
ksys_chdir("/old");
return;
}
// 切换到根目录/
ksys_chdir("/");
// 如果执行完Linuxrc后,没有设置成则Root_RAM0,则需要重新挂载上面linuxrc文件执行时指定的跟文件系统
ROOT_DEV = new_decode_dev(real_root_dev);
// 调用mount_root将lfs挂载到VFS的/root目录下,并将当前的目录配置为VFS的/root
mount_root();
printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = ksys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay\n");
else {
int fd = ksys_open("/dev/root.old", O_RDWR, 0);
if (error == -ENOENT)
printk("/initrd does not exist. Ignored.\n");
else
printk("failed\n");
printk(KERN_NOTICE "Unmounting old root\n");
ksys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = ksys_ioctl(fd, BLKFLSBUF, 0);
ksys_close(fd);
}
printk(!error ? "okay\n" : "failed\n");
}
}
3.2.2 mount_root
mount_root
函数定义在init/do_mounts.c
,用于将设备/dev/root
挂载到/root
目录;
void __init mount_root(void)
{
// 1. 网络文件系统nfs的情况
#ifdef CONFIG_ROOT_NFS
if (ROOT_DEV == Root_NFS) {
if (mount_nfs_root())
return;
printk(KERN_ERR "VFS: Unable to mount root fs via NFS, trying floppy.\n");
ROOT_DEV = Root_FD0;
}
#endif
// 2. 软盘
#ifdef CONFIG_BLK_DEV_FD
if (MAJOR(ROOT_DEV) == FLOPPY_MAJOR) {
/* rd_doload is 2 for a dual initrd/ramload setup */
if (rd_doload==2) {
if (rd_load_disk(1)) {
ROOT_DEV = Root_RAM1;
root_device_name = NULL;
}
} else
change_floppy("root floppy");
}
#endif
// 3. 文件系统在在块设备(Flash、MMC等)的情况
#ifdef CONFIG_BLOCK
{
// 在rootfs中创建/dev/root设备文件,一般就是指内核启动参数指定的包含根文件系统的设备,在rootfs中,这个设备文件被命名为/dev/root
int err = create_dev("/dev/root", ROOT_DEV);
if (err < 0)
pr_emerg("Failed to create /dev/root: %d\n", err);
// 将/dev/root设备挂载到/root目录
mount_block_root("/dev/root", root_mountflags);
}
#endif
}
3.2.2.1 mount_block_root
mount_block_root
函数定义在init/do_mounts.c
,用于将设备/dev/root
挂载到/root
目录;
static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
struct super_block *s;
// 将/dev/root/挂载到/root目录,函数有5个参数,其中前4个
// source: 文件系统所在设备名称
// target: 要挂载到的位置,一般是目录名
// filesystemtype: 文件系统名称
// mountflags: 文件系统通用挂载选项
int err = ksys_mount(name, "/root", fs, flags, data);
if (err)
return err;
// 将当前目录切换到/root
ksys_chdir("/root");
s = current->fs->pwd.dentry->d_sb;
ROOT_DEV = s->s_dev;
printk(KERN_INFO
"VFS: Mounted root (%s filesystem)%s on device %u:%u.\n",
s->s_type->name,
sb_rdonly(s) ? " readonly" : "",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
return 0;
}
void __init mount_block_root(char *name, int flags)
{
struct page *page = alloc_page(GFP_KERNEL);
char *fs_names = page_address(page);
char *p;
#ifdef CONFIG_BLOCK
char b[BDEVNAME_SIZE];
#else
const char *b = name;
#endif
get_fs_names(fs_names);
retry:
// 针对指定文件系统类型,调用do_mount_root尝试挂载,如果指定的文件系统类型和存储设备上的文件系统类型一致,那么挂载成功
for (p = fs_names; *p; p += strlen(p)+1) {
int err = do_mount_root(name, p, flags, root_mount_data);
switch (err) {
case 0:
goto out;
case -EACCES:
case -EINVAL:
continue;
}
/*
* Allow the user to distinguish between failed sys_open
* and bad superblock on root device.
* and give them a list of the available devices
*/
#ifdef CONFIG_BLOCK
__bdevname(ROOT_DEV, b);
#endif
printk("VFS: Cannot open root device \"%s\" or %s: error %d\n",
root_device_name, b, err);
printk("Please append a correct \"root=\" boot option; here are the available partitions:\n");
printk_all_partitions();
#ifdef CONFIG_DEBUG_BLOCK_EXT_DEVT
printk("DEBUG_BLOCK_EXT_DEVT is enabled, you need to specify "
"explicit textual name for \"root=\" boot option.\n");
#endif
panic("VFS: Unable to mount root fs on %s", b);
}
if (!(flags & SB_RDONLY)) {
flags |= SB_RDONLY;
goto retry;
printk("List of all partitions:\n");
printk_all_partitions();
printk("No filesystem could mount root, tried: ");
for (p = fs_names; *p; p += strlen(p)+1)
printk(" %s", p);
printk("\n");
#ifdef CONFIG_BLOCK
__bdevname(ROOT_DEV, b);
#endif
panic("VFS: Unable to mount root fs on %s", b);
out:
put_page(page);
}
参考文章
[1] linux
内核启动initramfs
与initrd
及其挂载