Linux内核启动之根文件系统挂载

一、基本概念介绍

1.1 rootfs

什么是根文件系统?理论上说一个嵌入式设备如果内核能运行起来,且不需要用户进程的话(估计这种情况很少),是不需要文件系统的。文件系统简单的说就是一种目录结构,由于linux操作系统的设备在系统中是以文件的形式存在,将这些文件分类管理以及提供和内核交互的接口,就形成了一定的目录结构也就是文件系统。文件系统是为用户反映系统的一种形式,为用户提供一个检测控制系统的接口。

而根文件系统,就是一种特殊的文件系统。那么根文件系统和普通的文件系统有什么区别呢?

借用书上的话说就是,根文件系统就是内核启动时挂载的第一个文件系统。由于根文件系统是启动时挂载的第一个文件系统,那么根文件系统就要包括linux启动时所必须的目录和关键性的文件,例如linux启动时都需要有用户进程init对应的文件,在linux挂载分区时一定要会找/etc/fstab这个挂载文件等,根文件系统中还包括了许多的应用程序,如 /bin目录下的命令等。任何linux启动时所必须的文件的文件系统都可以称为根文件系统。

根文件系统,对应/目录节点,分为虚拟rootfs和真实rootfs

1.1.1 虚拟rootfs

虚拟rootfs由内核自己创建和加载,仅仅存在于内存之中,其文件系统是tmpfs类型或者ramfs类型。

1.1.2 真实rootfs

真实rootfs则是指根文件系统存在于存储设备上,内核在启动过程中会在虚拟rootfs上挂载这个存储设备,然后将/目录节点切换到这个存储设备上,这样存储设备上的文件系统就会被作为根文件系统使用。

1.2 initrd

initrd总的来说目前有两种格式:image格式和cpio格式。

当系统启动的时候,bootloader会把initrd文件读到内存中,然后把initrd文件在内存中的起始地址和大小传递给内核;

  • 可以通过bootargs参数initrd指定其地址范围;
  • 也可以通过备树dts里的chosen节点的中的linux,initrd-startlinux,initrd-end属性指定其地址范围;

内核在启动初始化过程中会解压缩initrd文件,然后将解压后的initrd挂载为根目录,然后执行根目录中的/linuxrc脚本;

  • cpio格式的initrd/init
  • image格式的initrd/initrc;,

我们可以在这个脚本中加载真实文件系统。这样,就可以mount真正的根目录,并切换到这个根目录中来。

1.2.1 image-initrd

image-initrd是将一块内存当作物理磁盘,然后在上面载入文件系统,比如我们在《Rockchip RK3399 - busybox 1.36.0制作根文件系统》制作的ramdisk文件系统就是就属于这一种。

1.2.1.1 内核ramdisk配置

为了能够使用ramdisk,内核必须要支持ramdisk,即:在编译内核时,要选中如下配置;

c 复制代码
Device Drivers  ---> 
     [*] Block devices  --->
          <*>   RAM block device support
          (1)     Default number of RAM disks
        (131072) Default RAM disk size (kbytes)

配置完成,会在.config生成如下配置:

shell 复制代码
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=131072

同时为了让内核有能力在内核加载阶段就能装入ramdisk,并运行其中的内容,要选中:

c 复制代码
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加入如下配置:

shell 复制代码
initrd=0x42000000,0x14000000

initrd参数格式为:地址,长度,这里设备RAM地址为0x42000000起始,只要是在内核RAM物理地址空间内。长度这里只要比ramdisk.gz压缩包大小大就可以了。

1.2.1.3 挂载方式

当系统启动的时候,bootloader会把image-initrd文件读到内存中,内核将image-initrd保存在rootfs下的initrd.image中, 并将其读入/dev/ram0中,根据root是否等于/dev/ram0做不同的处理;

  • root != /dev/ram0bootloader - >kernel -> image-initrd(加载访问real rootfs的必备驱动) -> /linuxrc 脚本(加载real rootfs),内核卸载/dev/ram0,释放initrd内存,最后内核启动init进程(/sbin/init);
  • root = /dev/ram0bootloader -> kernel -> image initrd直接将/dev/ram0作为根文件系统, 内核启动init进程/sbin/init
1.2.2 cpio-initrd

特指使用cpio格式创建的initrd映像,和编译进内核的initramfs格式是一样的,只不过它是独立存在的,也被称为外部initramfs

1.2.2.1 内核配置

需要在make menuconfig中配置以下选项就可以了:

shell 复制代码
General setup  ---> 
    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
1.2.2.2 启动参数

通过备树dts里的chosen节点的中的linux,initrd-startlinux,initrd-end属性指定其地址范围;

shell 复制代码
chosen {
	linux,initrd-start=xxxxxx
	linux,initrd-end=xxxxxx
}
1.2.2.3 挂载方式

当系统启动的时候,bootloader会把cpio-initrd文件读到内存中,内核将cpio-initrd释放到rootfs,结束内核对cpio-initrd的操作。

bootloader -> kernel -> cpio-initrd(加载访问real rootfs的必备驱动等)-> /init脚本(加载real rootfs,并启动了init进程/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中配置以下选项就可以了:

shell 复制代码
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是一致的。

当系统启动的时候,内核将initramfs释放到rootfs,结束内核对initramfs的操作。

bootloader -> kernel -> initramfs(加载访问real rootfs的必备驱动等)-> /init脚本(加载real rootfs,并启动了init进程/sbin/init)。

二、源码分析

内核有关根文件系统挂载的调用链路如下:

c 复制代码
start_kernel
	vfs_caches_init()
		mnt_init()
    		// 创建虚拟根文件系统
			init_rootfs()
				register_filesystem(&rootfs_fs_type)
				init_ramfs_fs
					register_filesystem(&ramfs_fs_type)
    		// 注册根文件系统
			init_mount_tree()
	rest_init
		kernel_init
			kernel_init_freeable
				if(!ramdisk_execute_command)
					ramdisk_execute_command="/"
				do_basic_setup
					do_initcalls
						populate_rootfs
							unpack_to_rootfs
		run_init_process(ramdisk_execute_command)
2.1 VFS的注册

首先不得不从linux系统的函数start_kernel说起。函数start_kernel中会去调用vfs_caches_init来初始化VFS,函数位于fs/dcache.c

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

2.1.1 mnt_init

mnt_init函数位于fs/namespace.c

c 复制代码
void __init mnt_init(void)
{
        int err;

        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();

        err = sysfs_init();
        if (err)
                printk(KERN_WARNING "%s: sysfs_init error: %d\n",
                        __func__, err);
        fs_kobj = kobject_create_and_add("fs", NULL);
        if (!fs_kobj)
                printk(KERN_WARNING "%s: kobj create error\n", __func__);
    	// 创建虚拟根文件系统
        init_rootfs();
    	// 注册根文件系统
        init_mount_tree();
}
2.1.2 init_rootfs

init_rootfs定义在init/do_mounts.c

c 复制代码
static struct file_system_type rootfs_fs_type = {
        .name           = "rootfs",
        .mount          = rootfs_mount,
        .kill_sb        = kill_litter_super,
};

int __init init_rootfs(void)
{
        int err = register_filesystem(&rootfs_fs_type);

        if (err)
                return err;

        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.1.3 init_mount_tree

init_mount_tree函数位于fs/namespace.c

c 复制代码
static void __init init_mount_tree(void)
{
        struct vfsmount *mnt;
        struct mnt_namespace *ns;
        struct path root;
        struct file_system_type *type;

        type = get_fs_type("rootfs");
        if (!type)
                panic("Can't find rootfs type");
    	// 创建虚拟文件系统
        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;

        set_fs_pwd(current->fs, &root);
    	// 将当前的文件系统配置为根文件系统
        set_fs_root(current->fs, &root);
}

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

答案很简单,内核中没有根文件系统的设备驱动,如usbeMMC等存放根文件系统的设备驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,其实所有的驱动是由在后面的kernel_init线程进行加载,所以需要initrd/initramfs

2.2 VFS的挂载

参考文章

1 linux内核启动initramfsinitrd及其挂载

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

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

相关推荐
为思念酝酿的痛7 小时前
POSIX信号量
linux·运维·服务器·后端
专业白嫖怪7 小时前
什么是docker
运维·docker·容器
ccddsdsdfsdf7 小时前
DBeaver怎么链接mongoDB
数据库·mongodb
Dfreedom.8 小时前
Windows、虚拟机、开发板组网通信原理及调试通联步骤
人工智能·windows·部署·边缘计算·开发板·模型加速
隔窗听雨眠8 小时前
Nginx网关响应慢排查手记
java·服务器·nginx
丷丩8 小时前
Postgresql基础实践教程(十一)各种Join
数据库·postgresql·join
星夜夏空999 小时前
FreeRTOS学习(4)——内存映射
数据库·学习·mongodb
人还是要有梦想的9 小时前
linux下用搜狗输入法,中英文切换
linux·运维·服务器
北京智和信通9 小时前
某部队IT基础设施及机房动环统一运维建设实例
运维·网管平台·网管软件·网络管理系统·网络运维平台·网络运维系统
乐维_lwops9 小时前
从 “救火运维” 到 “自动驾驶”:运维智能体到底解决了什么?
运维·人工智能·运维智能体