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