程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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的过程执行,都不存在那肯定是启动不了。

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
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-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
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-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(1399)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2023-09-23 Rockchip RK3399 - MMC&SD&SDIO基础
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示