Linux核心啟動之根檔案系統掛載

大奥特曼打小怪兽發表於2024-09-23

一、基本概念介紹

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,即:在編譯核心時,要選中如下配置;

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壓縮包大小大就可以了。

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中配置以下選項就可以了:

General setup  ---> 
    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
1.2.2.2 啟動引數

透過備樹dts裡的chosen節點的中的linux,initrd-startlinux,initrd-end屬性指定其地址範圍;

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中配置以下選項就可以了:

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)。

二、原始碼分析

核心有關根檔案系統掛載的呼叫鏈路如下:

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

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

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

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

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] 根檔案系統的含義和相關重要概念以及載入程式碼分析

相關文章