ramfs

initramfs的作用

1. 作为启动跳板

kernel挂载initramfs,运行init程序,该程序会探测硬件,加载驱动,最后挂载真正的文件系统,执行文件系统上的init程序,进而切换到用户空间,
真正的文件系统挂载后,initramfs使命完成,释放其占用空间。

2. 作为最终文件系统

ramfs也可以作为最终文件系统,优点是速度快,重启后文件复原,缺点是文件在ram和rom同时存在。

为什么要initramfs

1. 让内核兼容不同的存储介质

内核被bootloader加载到内存后,内核需要从rom中读取程序或驱动,而若要操作rom,内核必须具有对应驱动(如从磁盘数据,则内核必须编译进了磁盘驱动),
但是存储介质种类繁多,内核若要兼容所有的存储介质,则要将对应的所有驱动都编译进内核,这会导致内核冗余巨大。
所以开发者设计了initramfs,initramfs是基于内存的文件系统,所以所有环境下都兼容,那么让内核先挂载initramfs,内核根据当前硬件环境加载对应的
驱动,然后再挂载对于硬件的文件系统。

2. 文件系统挂载前可能需要复杂的工作

如挂载网络文件系统前,需要配置网络,此外某些文件系统还需要解压,解密等操作,为保证内核的稳定,我们希望将这些操作实现为应用程序,
但是启动应用程序需要内核能访问存储介质,可此时没有尚未挂载文件系统,
所以可以先挂在initramfs,完成文件系统挂载前的工作,再挂载新文件系统。

ramfs 和 initrd

ramfs 是 initrd 的替代品

1. initrd

initrd是基于 ramdisk 的技术,是基于内存的 快设备。因此 initrd 具有快设备的一切属性。

  • initrd 容量固定,一旦创建无法动态调整
  • initrd 需要按照一定的文件系统格式进行组织,因此制作时需要mke2fs这样的工具格式化initrd,访问initrd时需要文件系统驱动
  • initrd 是伪块设备,从内核角度,与真实块设备无区别,所以内核访问 initrd 也使用缓存机制。但这是多此一举,因为initrd本就在内存中。

2. ramfs

Linus Torvalds 将cache当作一个文件系统直接挂载使用。
ramfs 是基于缓存的文件系统。所以ramfs去除了块设备的一些限制

  • ramfs 根据其中包含的文件大小可以自由伸缩:增加文件时自动分配内存,删除文件时,自动释放内存。
  • ramfs 是基于已有的缓存机制,因此不必像ramdisk 那样需要缓存之间进行多余的复制

3. initramfs 的原理

3.1 initramfs 的工作流程

内核首先挂载一个名为rootfs的文件系统,并将rootfs的根作为虚拟文件系统目录树的总根。
挂载rootfs后,内核将bootloader加载到内存中的initramfs中打包的文件解压到rootfs中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动,使用这些工具,实现了挂载真正的根文件系统。
此后rootfs完成使命,被真正的根文件系统覆盖(overmount),但是rootfs作为虚拟文件系统目录树的总根,并不能被释放。但这没有关系,因为rootfs基于ramfs,删除其中的文件即可释放其占用的空间。

上面提到的 rootfs 就是一个 ramfs,由于其为基于缓存的文件系统,所以不需要设备驱动,有因为ramfs的空间是动态伸缩的,所以可以增删文件,将bootloader 加载的压缩文件 解压,到 rootfs ,就完成了 rootfs 的构建。

3.2 什么是挂载

3.2.1 文件系统的组织结构

以ExtX为例,

(1) 超级块(super block)
描述整个文件系统信息,包括inode总数,空闲的inode数量,块大小,挂载次数等。
(2)块组描述符(Group Descriptors)
包括所有块组的描述
(3)块位图(Block Bitmap)
描述块组中哪些块已使用,哪些空闲
(4)索引节点位图(Inode Bitmap)
和块位图类似,索引节点用于表示哪些inode已用,哪些inode空闲。
(5)索引节点表(Inode Table)
inode存储文件描述信息,包括,文件类型,权限,文件大小,创建/修改/访问时间,数据块的索引,等。
所有的inode组成一个inode数组。
(6)数据块(Data Block)
数据块存储文件数据,但是不同文件类型的文件,的数据块的存储内容是不同的,

  • 常规文件类型:数据块中存储的是文件的数据
  • 目录类型:数据块中存储的是目录下所有的文件名和子目录名
    对于 设备文件,socket文件等特殊文件,不需要数据块,只需要将相关信息保存到inode中。

以上信息,在格式化存储介质时,在存储介质上构造。

虚拟文件系统需要模拟上面的对象。

  • 树形文件系统
    文件系统都是以树形组织,新增的树可以挂载到原有的树的任何节点。
    虚拟文件系统是第一颗树,上面只有一个节点,第一个添加的文件系统挂载到虚拟文件系统的根节点。

  • mount结构,描述树之间的挂载关系
    mount->mnt_mountpoint指向 被挂载的文件系统的挂载点,
    mount->mnt_parent 指向挂载点所在文件系统的mount
    mount->mnt_root 指向 挂载的文件系统的根
    对于 基于ramfs 的 rootfs 挂载 虚拟文件系统的情况如下:
    首先为 rootfs 创建一个 mount,由于 rootfs整个虚拟文件的第一个文件系统,
    mnt_parent ,指向 rootfs的mount自己
    mnt_mountpoint 指向rootfs自己的根mnt_root

  • 超级块
    访问文件系统需要文件系统的超级块,
    所以载入rom的超级块,并在内存进行实例化。
    对于ramfs不存在超级块,所以模拟一个

  • inode
    每个文件都用一个inode表示,对于ramfs将模拟一个根节点inode

  • dentry
    dentry 表示父文件节点和子文件节点的关系,
    通过构建dentry,实现将文件加入虚拟文件系统树。
    dentry 在rom中没有实体,之存在于ram,内核会缓存最近访问的dentry。

挂载完基于ramfs的rootfs后,有如下结构

整个虚拟文件系统树对用户进程不可见,进程看到的只是一个分支,
也就是namespace的概念,每个进程有自己的文件系统空间,通常多数进程的namespace是相同的,

通过挂在rootfs,虚拟文件系统的根目录建立起了,根目录下可以扩充文件,
内核解压initramfs的内容到虚拟文件系统的根,并利用initramfs的内容挂载并切换到真正的文件系统。

3.3 内核如何获得 initramfs, 如何使用initramfs

  6 obj-y                          += noinitramfs.o
  7 obj-$(CONFIG_BLK_DEV_INITRD)   += initramfs.o

如果 CONFIG_BLK_DEV_INITRD ,则使用 initramfs
否则使用 默认则 initramfs
initramfs.c如下

                                                                                                                                                                                                                                                                            
636 static int __init populate_rootfs(void)
637 {
638     char *err;
639     
        ...
645     // 先解压 内嵌到内核的 initramfs
646     err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
647     if (err)
648         panic("%s", err); /* Failed to decompress INTERNAL initramfs */
649     // 若有相关 启动参数,则从外部加载initramfs 并解压
         if (initrd_start) {
650 #ifdef CONFIG_BLK_DEV_RAM

682 #else
683         printk(KERN_INFO "Unpacking initramfs...\n");
684         err = unpack_to_rootfs((char *)initrd_start,
685             initrd_end - initrd_start);
689 #endif
690         /*
691          * Try loading default modules from initramfs.  This gives
692          * us a chance to load before device_initcalls.
693          */
694         load_default_modules();
695     }
696     return 0;
697 }

// populate_rootfs加入初始化数组
716 rootfs_initcall(populate_rootfs);

__initramfs_start 和 __initramfs_size
在 vmlinux.lds中定义

896  .init.data : {
897   *(.init.data) *(.meminit.data) . = ALIGN(8); __start_mcount_loc = .; *(__mcount_loc) __stop_mcount_loc = .; *(.init.rodata) . = ALIGN(8); __start_ftrace_events = .; *(_ftrace_events) __stop_ftrace_events = .; __start_ftrace_enum_maps = .; *(_ftrace_enum_map) __stop_    ftrace_enum_maps = .; *(.meminit.rodata) . = ALIGN(8); __clk_of_table = .; *(__clk_of_table) *(__clk_of_table_end) . = ALIGN(8); __reservedmem_of_table = .; *(__reservedmem_of_table) *(__reservedmem_of_table_end) . = ALIGN(8); __clksrc_of_table = .; *(__clksrc_of_tabl    e) *(__clksrc_of_table_end) . = ALIGN(8); __iommu_of_table = .; *(__iommu_of_table) *(__iommu_of_table_end) . = ALIGN(8); __cpu_method_of_table = .; *(__cpu_method_of_table) *(__cpu_method_of_table_end) . = ALIGN(8); __cpuidle_method_of_table = .; *(__cpuidle_method_o    f_table) *(__cpuidle_method_of_table_end) . = ALIGN(32); __dtb_start = .; *(.dtb.init.rodata) __dtb_end = .; . = ALIGN(8); __irqchip_of_table = .; *(__irqchip_of_table) *(__irqchip_of_table_end) . = ALIGN(32); __earlycon_table = .; *(__earlycon_table) *(__earlycon_tab    le_end) . = ALIGN(8); __earlycon_of_table = .; *(__earlycon_of_table) *(__earlycon_of_table_end)
898   . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;
899   __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall2_start = .; *(.initcall2.init) *(.initcall2s.init) __initcall3_start = .; *(.initcall3    .init) *(.initcall3s.init) __initcall4_start = .; *(.initcall4.init) *(.initcall4s.init) __initcall5_start = .; *(.initcall5.init) *(.initcall5s.init) __initcallrootfs_start = .; *(.initcallrootfs.init) *(.initcallrootfss.init) __initcall6_start = .; *(.initcall6.init    ) *(.initcall6s.init) __initcall7_start = .; *(.initcall7.init) *(.initcall7s.init) __initcall_end = .;
900   __con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .;
901   __security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .;
902   . = ALIGN(4); __initramfs_start = .; *(.init.ramfs) . = ALIGN(8); *(.init.ramfs.info)
903  }

可见 initramfs 会被链接到 init_begin 和 init_end之间,所以在内核初始化完成后 cpio 压缩文件会从内存中释放。
而此时 cpio 解压获得的 initramfs 已经拷贝到了 ramfs 的 根文件系统的 根目录下。

如果用户没有使用 initramfs,则会用默认的 initramfs,也就是 noinitramfs.o

 33 int __init default_rootfs(void)
 34 {
 35     int err;
 36 
 37     err = sys_mkdir((const char __user __force *) "/dev", 0755);
 38     if (err < 0)
 39         goto out;
 40 
 41     err = sys_mknod((const char __user __force *) "/dev/console",
 42             S_IFCHR | S_IRUSR | S_IWUSR,
 43             new_encode_dev(MKDEV(5, 1)));
 44     if (err < 0)
 45         goto out;
 46 
 47     err = sys_mkdir((const char __user __force *) "/root", 0700);
 48     if (err < 0)
 49         goto out;
 50 
 51     return 0;
 52 
 53 out:
 54     printk(KERN_WARNING "Failed to create a rootfs\n");
 55     return err;
 56 }
 57 #if !IS_BUILTIN(CONFIG_BLK_DEV_INITRD)
 58 rootfs_initcall(default_rootfs);

可见内核会保证最小的文件系统,避免 由于第一个进程打不开控制台导致 panic.

4. 挂载新的文件系统


假设新的文件系统挂载到 initramfs 的 root 目录
mount2->mnt_parent 指向父文件系统
mount2->mnt_mountpoint 指向挂载点,这里是 root目录
mount2->mnt_root 指向自己的根目录
此时进程未切换文件系统空间,所以使用 initramfs的根目录,为进程的文件系统根
mount2 和 mount 会被加入hash表,
当访问某个dentry时,若其被标记为被挂载状态,则求hash得到对应的mount,在对应mount下访问文件。


真实情况,我们将mount2挂载到mount1的根目录下,然后切换进程的namespace。
对于mount1的文件需要删除,由于mount1是 ramfs类型,所以删除文件就自动释放对应内存,
由于mount1是整个虚拟文件系统的根,所以mount1不能卸载。

5. 基于ramfs构建基础文件系统

5.1 配置内核使用initramfs

开启 Initial RAM filesystem and RAM disk (initramfs/initrd) support
可以配置Initramfs source file(s) ,若配置,则使用嵌入内核的方式,initramfs会被压缩链接到.init.ramfs段,
不配置则使用 外部加载方式,若使用外部加载方式,需要bootloader传递启动参数。

5.2 devtmpfs

FHS规定在/dev下创建设备节点,以前是静态创建,但是硬件环境不同,静态创建不太适合,于是设计了udev,
udev为应用程序,负责创建节点和权限控制,udev会根据内核检查到的硬件信息自动创建设备节点。
由于 设备节点不需要持久记录,所以 推荐在 /dev目录挂载 ramfs 文件系统,
后来开发者在 专门 实现了 tmpfs,tmpfs就是 基于缓存的 文件系统,
linux从 2.6开始使用udev,/dev目录使用基于内存的文件系统哦 tmpfs
后来开发者又推出了 devtmpfs,在内核引导时,devtmpfs创建所有注册的设备的设备文件,
在进入 用户空间前,将 devtmpfs 挂载到 /dev目录,也就是说在 udev启动前,devtmpfs已经建立的初步的设备文件。
这样的好处是:由于进入应用空间前设备文件已创建,所以应用程序不需要等待udev。

而 devtmpfs 的本质是,若内核支持 tmpfs,则为tmpfs,否则为 ramfs

devtmpfs是内核实现所以要配置内核,
udev为应用程序

新编译的内核启动后,/dev目录下只有一个console,这是 initramfs创建的,而 devtmpfs创建的设备节点在 devtmpfs文件系统里,
所以需要挂载 devtmpfs 到 /dev目录

# -n : mount 会在 /etc/mtab 文件维护一个已挂载文件系统列表,由于我们没有 /etc/mtab文件,所以不需要维护
# udev : devtmpfs是基于内存的,没有对应设备,所以名称可以随便取,命名为udev,是为了说明下面文件为udev创建
mount -n -t devtmpfs udev /dev

挂载成功后,/dev目录下有很多设备文件。

5.3 挂载系统文件

这两个文件系统和 devtmpfs 一样都是基于内存的,所以名称也能随便取。
有了这两个文件,一些命令就可以用了,如 modprobe

mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys

posted on 2022-03-07 09:46  开心种树  阅读(1915)  评论(0编辑  收藏  举报