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
的过程执行,都不存在那肯定是启动不了。
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2023-09-23 Rockchip RK3399 - MMC&SD&SDIO基础