一、基本概念介绍
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-start
和linux,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/ram0
:bootloader
- >kernel
->image-initrd
(加载访问real rootfs
的必备驱动) ->/linuxrc
脚本(加载real rootfs
),内核卸载/dev/ram0
,释放initrd
内存,最后内核启动init
进程(/sbin/init
);root = /dev/ram0
:bootloader
->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-start
和linux,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 挂载方式
initramfs
和cpio-initrd
的区别, initramfs
是将cpio rootfs
编译进内核,而cpio-initrd
中cpio 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);
}
可能有人会问,为什么不直接把真实的文件系统配置为根文件系统?
答案很简单,内核中没有根文件系统的设备驱动,如usb
、eMMC
等存放根文件系统的设备驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,其实所有的驱动是由在后面的kernel_init
线程进行加载,所以需要initrd/initramfs
。
2.2 VFS
的挂载
参考文章
[1] linux
内核启动initramfs
与initrd
及其挂载