程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

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-startlinux,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-startlinux,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 挂载方式

initramfscpio-initrd的区别, initramfs是将cpio rootfs编译进内核,而cpio-initrdcpio 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_dtdtb里面的所有节点进行扫描,用提供的回调函数循环处理节点信息,回调函数返回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_startinitrd_end ,后面解压initrd的时候会用到;
  • chosen节点中解析得到bootargs ,后面还会解析bootargs参数。

下面就是关注解析bootargs中的initrdinitrdinit等参数,解析后放在全局变量里;

#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,即内存文件系统,后面还会指向真实的文件系统。

可能有人会问,为什么不直接把真实的文件系统配置为根文件系统?

答案很简单,内核中没有根文件系统的设备驱动,如usbeMMC等存放根文件系统的设备驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,其实所有的驱动是由在后面的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/initrdrootfs,函数定义如下:

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-initrdinitramfs其实是一回事,只是传递给内核的方式不同;

  • initramfs是通过将cpio-initrd链接到内核方式;
  • cpio-initrd是通过ubootbootargs或者设备树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_loadrootfs中的创建的/dev/ram0设备节点和image-initrd镜像/initrd.image

  • 调用crd_loadinitrd.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内核启动initramfsinitrd及其挂载

[2] 嵌入式软件开发之------浅析linux根文件系统挂载(九)

[3] 根文件系统的含义和相关重要概念以及加载代码分析

[4] Linux内核中的VFS层:文件系统抽象与接口实现

[5] 文件子系统-(rootfs)根文件系统挂载流程03

posted @ 2024-09-23 01:09  大奥特曼打小怪兽  阅读(515)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步