当Linux内核启动时,必须找到并执行第一个用户程序,通常是init,这就是根文件系统。对于根文件系统工作上用的比较多,基于内存和flash的都实现编译、裁剪和加载启动验证过,自认为对这一块还是比较深刻。但是没有深入的去看一下里面的原理,都是基于网上的一些常用的使用方法,稍微有点非常规的用法,出现问题了,就蒙了,原来还可以这样,有一种豁然开朗的感觉。本文档基于ubuntu16.04梳理一下了根文件系统的流程,内容包括
- 1.根文件系统是什么 、如何初始化、如何应用
- 2.基于内存根文件系统、存放在哪里、如何解压、解压在哪、如何启动
- 3.基于真实的根文件系统,其启动过程
1. 根文件系统分类
Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。在Linux应用中,主要的存储设备为RAM(DRAM, SDRAM)和ROM(常采用FLASH存储器),其框图如下:
- 1.基于ROM的文件系统
- 对于现在基于ROM的存储系统,嵌入式一般采用flash,基于MTD结构由其特性决定了,其文件系统一般采用jffs2、YaFFS、Cramfs等。
- 对于现在的PC系统,一般采用的是机械硬盘,其也经历了ext2,ext3,reiserfs,ext4,现在主流的系统都采用的是ext4。
- 对于现在手机,经历了很多的过程,2010年,Android从最初的YAFFS2切换到了Ext4,主要的原因YAFFS2只支持单线程,无法发挥多核CPU的潜力。2016年,为了解决了安卓平台一直以来的一个顽疾,长期使用后的卡顿,引入了F2FS。以前Linux沿用的Ext4文件系统适合机械硬盘,F2FS可以说是为闪存量身定制的文件系统,能极大地改善闪存长期使用后的读写性能。2018年华为提出了EROFS,可提供比其他只读文件系统方案更好的性能,且可以节省存储空间。因为文件系统本身是只读的设计,也加强了对于数据的安全防护。
- 2.基于RAM的文件系统
- Ramdisk:是将一部分固定大小的内存当作分区来使用。它并非一个实际的文件系统,而是一种将实际的文件系统装入内存的机制,并且可以作为根文件系统。将一些经常被访问而又不会更改的文件(如只读的根文件系统)通过Ramdisk放在内存中,可以明显地提高系统的性能。在Linux的启动阶段,initrd提供了一套机制,可以将内核映像和根文件系统一起载入内存。
- ramfs/tmpfs:把所有的文件都放在RAM中,所以读/写操作发生在RAM中,可以用ramfs/tmpfs来存储一些临时性或经常要修改的数据,例如/tmp和/var目录,这样既避免了对Flash存储器的读写损耗,也提高了数据读写速度。对于传统的Ramdisk的不同之处主要在于:不能格式化,文件系统大小可随所含文件内容大小变化,当系统重新启动后,丢失所有的数据。
- 网络文件系统NFS:可以利用该技术在主机上建立基于NFS的根文件系统,挂载到嵌入式设备,可以很方便地修改根文件系统的内容
2 为什么引入Ramdisk
在早期的Linux系统中,一般只有软盘或者硬盘被用来作为Linux的根文件系统,因此很容易把这些设备的驱动程序集中到内核中。但是对于现实的需求越来越多,硬件设备花样百出,其表现为:
- 1.根文件系统可能保存在各种设备中,包括SCSI,SATA,U盘等等,而不同的设备又要不同的硬件厂商的驱动,比如Intel的南桥自然需要Intel的IDE/SATA驱动,VIA的南桥需要VIA的IDE/SATA;
- 2.根文件系统也有不同的文件系统的可能,比如ubuntu发行版本可能用到ext3,而其他的版本可能用到ext4,不同的文件系统也需要不同的文件系统模块,
- 3.对所有的硬件的兼容性,特别是x86的系统,不同的硬件也有不同的硬件模块
假如把所有的驱动/模块都编译进内核,那么就问题就来了,内核会非常的庞大,已经违背了内核的精神和本质,对于内核现有的方案已经有成熟的方案
- 驱动/模块都驻留在根文件系统本身上/lib/modules/xxx,采用模块加载的方法
对于这块,内核本来采用的是文件系统的方式,通过将驱动/模块编译成.ko方式加载,而此时需要先运行.ko,然后在加载根文件系统,那么就出现先有“鸡”还是先有“蛋”的问题来了,如果不支持这个存储设备,那么文件系统就无法挂载,但是你却先要我挂载后的文件/lib/modules/xxx。于是内核就引入了ramdisk的方法,总之ramdisk的存在的意义
- 解决了“先有鸡还是先有蛋的问题”,既内核编译空间很小,同时也解决了在挂载真正的文件系统之前完成了各个模块/驱动的初始化
3. 根文件系统初始化
首选在内核启动过程,会初始化rootfs文件系统,rootfs和tmpfs都是内存中的文件系统,其类型为ramfs. 然后会把这个rootf挂载到根目录。 其代码如下:
void __init mnt_init(void)
{
int err;
...
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();
}
这个函数主要完成下面的功能
- sysfs_init初始化了sysfs文件系统,也就是根文件系统的sys目录,sysfs先于rootfs挂载是为全面展示linux驱动模型做好准备
- init_rootfs()注册rootfs,然后调用init_mount_tree()挂载rootfs
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;
}
在init_rootfs()中,注册rootfs文件类型,主要作用是把rootfs加入内核维护的一个文件系统类型的链表中,同时供其它模块进行系统调用,下面来看看init_mount_tree
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);
}
该函数首先拿到上面注册的rootfs文件系统,再调用vfs_kern_mount方法挂载该系统,然后将挂载结果mnt赋值给类型为struct path的变量root,同时将root.dentry赋值为mnt->mnt_root,即挂载的rootfs文件系统的根目录。最后,设置当前进程的当前目录和根目录都为root。
至此,rootfs文件系统建立、并且挂载于自己超级块(包括目录项dentry和i节点inod)对应的目录项,设置了系统current根目录和根文件系统、pwd的目录和文件系统。rootfs流程完毕,但此时的rootfs是ramfs,这些数据掉电丢失,在根文件系统下要永久保存一些文件,就需要把根文件系统安装到实际硬盘或flash中。
4. 虚拟挂载文件系统
4.1. 虚拟文件系统存放在哪里
initramfs/initrd根据CONFIG_BLK_DEV_INITRD定义是否使用,从vmlinux.lds.h文件可知,INIT_RAM_FS存放ramfs相关内容,包括.init.ramfs和.init.ramfs.info两个段。该方式主要针对arm架构,对于x86架构采用的是vmlinux.lds.S直接定义,
#ifdef CONFIG_BLK_DEV_INITRD
#define INIT_RAM_FS \
. = ALIGN(4); \
VMLINUX_SYMBOL(__initramfs_start) = .; \
KEEP(*(.init.ramfs)) \
. = ALIGN(8); \
KEEP(*(.init.ramfs.info))
#else
#define INIT_RAM_FS
#endif
然后将该区域放到采用以下方式存储于init.data区,
init.data : {
INIT_DATA
INIT_SETUP(16)
INIT_CALLS
CON_INITCALL
SECURITY_INITCALL
INIT_RAM_FS
*(.init.rodata.* .init.bss) /* from the EFI stub */
}
.init.ramfs和.init.ramfs.info两个段在initramfs_data.S中定义,通过编译后的汇编,我们可以知道这两个段的地址,对于存储的地址,我们需要重点关注__initramfs_start和__initramfs_size,而__initramfs_size是通过编译出来的INITRAMFS_IMAGE计算得到的地址大小
.section .init.ramfs,"a"
__irf_start:
.incbin __stringify(INITRAMFS_IMAGE)
__irf_end:
.section .init.ramfs.info,"a"
.globl VMLINUX_SYMBOL(__initramfs_size)
VMLINUX_SYMBOL(__initramfs_size):
#ifdef CONFIG_64BIT
.quad __irf_end - __irf_start
#else
.long __irf_end - __irf_start
#endif
对于编译过程,暂时不做分析,如果想认真分析请看内核的参考文档<<Documentation/filesystems/ramfs-rootfs-initramfs.txt>>,其大致过程如下:
- 1.gen_initramfs_list.sh脚本以cpio格式对目标root目录压缩生成XXX.cpio.gz文件
- 2.initramfs_data.S只做一件事情,创建一个Section,包含XXX.cpio.gz文件,参考编译出来的usr/.initramfs_data.o.cmd文件
- 3.编译链接,通过__initramfs_start和__initramfs_size执行XXX.cpio.gz文件开始地址和文件大小,后面加载和解压文件的时候会 用到。
4.2 虚拟文件系统如何启动
4.1 启动阶段解析
由第三节根目录已经挂上去了,可以挂载具体的文件系统了,其挂载的代码如下:
static int __ref kernel_init(void *unused)
{
....
if (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);
}
....
}
内核启动时,通过ramdisk_execute_command来决定是否挂载ramdisk,接着上节ramdisk_execute_command来看,什么情况下会给这个参数赋值,一是修改Kernel的bootargs,增加rdinit选项,也就是说启动时候将rdinit参数传递给内核,内核解析。
static int __init rdinit_setup(char *str)
{
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);
当系统没有指定这个参数的时候,会通过以下的代码来指定,如果没有指定,则会使用默认的/init
static noinline void __init kernel_init_freeable(void)
{
...
do_basic_setup();
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
...
}
我们知道对于ramdisk采用__initramfs_start和__initramfs_size读取,在我们不熟悉的情况下,那么我们只有通过编译存放的地址来看看内核怎么读取,然后解压等操作,先看看这个地址做了什么?
static int __init populate_rootfs(void)
{
/* Load the built in initramfs */
char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);----------------------【1】
if (err)
panic("%s", err); /* Failed to decompress INTERNAL initramfs */
/* If available load the bootloader supplied initrd */
if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) {------------------------------【2】
#ifdef CONFIG_BLK_DEV_RAM ------------------------------------------------【3】
int fd;
printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start);
if (!err) {
free_initrd();
goto done;
} else {
clean_rootfs();
unpack_to_rootfs(__initramfs_start, __initramfs_size);
}
printk(KERN_INFO "rootfs image is not initramfs (%s)"
"; looks like an initrd\n", err);
fd = sys_open("/initrd.image",
O_WRONLY|O_CREAT, 0700);
if (fd >= 0) {
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);
sys_close(fd);
free_initrd();
}
done:
/* empty statement */;
#else
printk(KERN_INFO "Unpacking initramfs...\n");
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start); --------------------------------------------------【4】
if (err)
printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err);
free_initrd();
#endif
}
flush_delayed_fput();
/*
* Try loading default modules from initramfs. This gives
* us a chance to load before device_initcalls.
*/
load_default_modules();
return 0;
}
对于这段代码做了些什么呢?是不是感觉前面分析的好像有点点不对头,是的,确实是的,下面来看看真实的有哪些不一样呢?
- 1.unpack_to_rootfs顾名思义,就是解压包到rootfs,这个主要是针对initramfs,它的作用和initrd类似,只是和内核编译成一个文件(该initramfs是经过gzip压缩后的cpio格式的数据文件),该cpio格式的文件被链接进了内核中特殊的数据段.init.ramfs上其中全局变量__initramfs_start和__initramfs_end分别指向这个数据段的起始地址和结束地址。内核启动时会对.init.ramfs段中的数据进行解压,然后使用它作为临时的根文件系统。
- 2.判断是否加载了initrd,这个initrd_start一般由启动参数传递进来,后面会再看看这个参数的来龙去脉
- 3.CONFIG_BLK_DEV_RAM是将内存假设为一个硬盘驱动器,你在他的上面存储文件,内存的I/O速度是硬盘的N倍,一般没有使用,暂不考虑
- 4.判断加载的是不是initramfs CPIO文件,如果解压成功,释放image中initrd对应内存。
通过这个函数发现,对于基于内存得文件系统,内核是分别处理的,那么我们再回头来看看这两个的区别initramfs和initrd
- 1.Linux内核只认cpio格式的initramfs文件包(因为unpack_to_rootfs只能解析cpio格式文件),非cpio格式的 initramfs文件包将被系统抛弃,而initrd可以是cpio包也可以是传统的镜像(image)文件,实际使用中initrd都是传统镜像文件。
- 2.initramfs在编译内核的同时被编译并与内核连接成一个文件,它被链接到地址__initramfs_start处,与内核同时被 bootloader加载到ram中,而initrd是另外单独编译生成的,是一个独立的文件,其地址是由启动参数传递给内核解析,这个后面再单独分析
- 3.initramfs被解析处理后原始的cpio包(压缩或非压缩)所占的空间(&__initramfs_start - &__initramfs_end)是作为系统的一部分直接保留在系统中,不会被释放掉,而对于initrd镜像文件,如果没有在命令行中设置"keepinitd"命令,那么initrd镜像文件被处理后其原始文件所占的空间(initrd_end - initrd_start)将被释放掉
4.2 启动
kernel_init()是用户空间第一个进程,启动ramdisk_execute_command来替代当前进程
static int __ref kernel_init(void *unused)
{
kernel_init_freeable(); ----------------------------------------【1】
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);----------------【2】
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
}
- 1.执行各种initcall,包括对ramfs注册和populate_rootfs()解压ramdisk;以及判断ramdisk_execute_command是否存在,否则prepare_namespace()
- 2.启动ramdisk_execute_command
那么我们就来看看ramdisk_execute_command这个参数是怎么来的,
static noinline void __init kernel_init_freeable(void)
{
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init"; ----------------------------【1】
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {------【2】
ramdisk_execute_command = NULL;
prepare_namespace();
}
}
static int __init rdinit_setup(char *str)
{
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);
- 1.从这个可以看出,对于ramdisk_execute_command一种方式是通过启动参数传递,如果没有传递就走默认的init,最后就会执行虚拟文件系统下面的init文件
- 2.然后sys_access()检查rootfs中是否存在ramdisk_execute_command,没有则需要prepare_namespace()准备rootfs,挂载真实的根文件系统
最后,我们回过头看看,怎么启动虚拟和真实的文件系统,如果虚拟的文件系统在解压后确实存在ramdisk_execute_command对应的目录,那么就会执行虚拟的文件系统,而如果不存在,那么就会在prepare_namespace挂载真实的文件系统,并且在启动参数中将execute_command传递给内核,就会在真实的文件系统中执行execute_command。有一个疑问,虚拟文件系统启动后,成功后就会返回,不会执行execute_command的真实文件系统,那么如果有虚拟的文件系统,怎么样去启动真正的文件系统呢?那么这个需要看虚拟文件系统做了些什么。
static int __ref kernel_init(void *unused)
{
if (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.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
}
该过程,如果定义了虚拟的文件系统就执行虚拟的文件系统的/init或者Bootloader传递的参数,如果没有定义虚拟文件系统,那么就走真实的文件系统。下面我们来看看怎么从虚拟的文件系统切换到真实的文件系统,我们来看看init这个脚本做了什么,该init文件是一个由sh解释并执行的脚本文件
- 1.建立了相关目录和挂载点,并将kernel运行过程中产生的信息挂载到/sys和/proc目录下。
- 2.如果在GRUB的kernel行上有quiet关键字,则在kernel启动和initrd中init脚本执行的过程中不会在屏幕上显示相关信息而是一个闪烁的下划线,否则将显示"Loading, please wait…"
- 3.在/dev目录下建立devtmpfs文件系统,devtmpfs是一个虚拟的文件系统被挂载后会自动在/dev目录生成很多常见的设备节点文件,当挂载devtmpfs失败时会手动建立/dev/console和/dev/null设备节点文件。/dev/console 总是代表当前终端,用于输出kernel启动时的输出内容,在最后通 过 exec 命令用指定程序替换当前 shell 时使用
- 4.引入 /conf/conf.d 下的所有文件,注意在引入的时候用了 -f 参数判断,这样只有普通的文件才 会被引入,链接(硬链接除外)、目录之类的非普通文件不会被引入,当使用不支持命令行参数的开机引导程序时,可以在该目录下建立各种参数设置文件
- 5.for循环中主要功能是获取kernel的命令行选项(/proc/cmdline )然后赋给相应的变量。其中最重要的是root,其它的可有可无
- 6.按照/scripts/init-top/ORDER文件的配置依次执行其下的脚本文件,这里最重要的是开启了udev daemon,udev 以 daemon 的方式启动 udevd,接着执行 udevtrigger 触发在机器启动前已 经接入系统的设备的 uevent,然后调用 udevsettle 等待,直到当 前 events 都被处理完毕
- 7.把系统的启动交给了将要进入的系统的 ${init} (上面初始化为 “/sbin/init”),并用 /dev/console 作为输入与输出的设备,最终也是调用到真实文件系统的/sbin/init。
5 真实文件系统
对于真实的文件系统,从上面的流程可以看出,其实可以分为直接启动真实文件系统,也就是我们大家熟悉的/sbin/init,还有一种是由虚拟的文件系统来启动真实的文件系统,对于直接启动真实的文件系统,比较简单,其流程应该是直接挂载,并执行就可以了,下面我们主要来看看挂载的流程
void __init prepare_namespace(void)
{
int is_floppy;
if (root_delay){ -----------------------------【1】
printk(KERN_INFO "Waiting %d sec before mounting root device...\n",
root_delay);
ssleep(root_delay);
}
wait_for_device_probe();------------------------------【2】
md_run_setup(); ------------------------------【3】
if (saved_root_name[0]) {-----------------------------【4】
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3) ||
!strncmp(root_device_name, "ubi", 3)) {
mount_block_root(root_device_name, root_mountflags);
goto out;
}
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
if (initrd_load()) ---------------------------------【5】
goto out;
/* wait for any asynchronous scanning to complete */
if ((ROOT_DEV == 0) && root_wait) { -----------------【6】
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;
mount_root();---------------------------------------【7】
out:
devtmpfs_mount("dev");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
- 1.安装 root 文件系统 之前等待10秒,由bootloader传递过来的
- 2.等待已知设备完成其探测
- 3.md_run_setup初始化MD设备,MD设备主要包含了LINUX内核的软RAID实现
- 4.该方法中的saved_root_name变量的值是在kernel启动时,由传给kernel的root参数决定的,对应的设置方法如下root=/dev/sda2,由启动__setup(“root=”, root_dev_setup)来解析
- 5.通过设备文件标识符ROOT_DEV判断启动设备文件是否是软盘,并把判断结果存入局部变量is_floppy中
- 6.调用initrd_load函数
- 7.挂载root,也是最重要的操作
void __init mount_root(void)
{
#ifdef CONFIG_ROOT_NFS ------------------------【1】
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
#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
#ifdef CONFIG_BLOCK -------------------------【2】
{
int err = create_dev("/dev/root", ROOT_DEV);
if (err < 0)
pr_emerg("Failed to create /dev/root: %d\n", err);
mount_block_root("/dev/root", root_mountflags);
}
#endif
}
1.对于网络文件系统NFS的挂载
2.调用create_dev函数在/dev目录中创建root设备文件,设备标识符为ROOT_DEV,设备文件名称为root_device_nam,调用函数mount_block_root安装根文件系统,遍历注册的文件系统类型,依次尝试将/dev/root指向的硬盘分区挂载到/root目录下。里面的细节暂不做分析,做完这些之后,其余的事项大家都很清楚了,最后直接运行/sbin/init。
5 结束语
对于根文件系统,其实有两种处理方式,一种是真实的文件系统,像现在很多平台都在使用,例如我们比较常见的JFFS2、YAFFS都是真实的文件系统,像基于内存的initramfs其实也是一个真实的文件系统,其实大部分的平台都是按照这个方式来做的。而现在linux 发行版,为了适配各种不同的硬件,将所有驱动编译进内核是不现实的,所有就采用了initrd技术来解决该问题,内核中只有编译基本的硬件功能,在按照过程中通过检查系统硬件,就产生我们现在看到虚拟和真实文件系统之分。