在Linux内核被加载到内存并运行后,内核进程最终需要切换到用户太的进程来使用计算机,而用户进程又存在于外存储设备上,比如systemd进程,通常systemd进程所在的存储设备也是Linux真正的根文件系统所在的位置,我们知道内核源码是没有包含驱动程序的,驱动程序在外存储设备上,那么问题来了,要切换到systemd进程,就需要外存储的驱动,但是没有驱动又没办法访问外存储,这就进入先有鸡还是先有蛋的问题了(那到底是先有鸡还是先有蛋呢:)),这个时候initramfs就闪亮登场了。
第一,除非是一个专用系统,目标系统的硬件平台是固定不变的,否则,对于一个通用操作系统,比如Linux的桌面发行版,将运行在各种不同的硬件平台上。因此,根文件系统可能存储在各种各样的介质上,比如IDE硬盘、SATA硬盘、SCSI硬盘、Flash存储器,以及随着技术的发展,不断出现的新的存储设备。为了能够兼容更多的硬件平台,显然系统需要支持尽可能多的存储设备。但是如果将所有这些设备的驱动全部编译进内核,显然不是一个好办法。因为对于某个特定的硬件平台,可能只需要一个驱动即可,内核中的其他驱动根本用不上,将它们编译进内核只会徒增内核的尺寸、占用内存空间,尤其对于一些内存或者存储介质空间有限的设备,这个问题尤为明显。于是将这些驱动编译为模块,存储在根文件系统中,按需载入内存是一个解决问题的办法。
第二,根文件系统可能不在一个简单的硬盘上,比如当使用磁盘阵列RAID时,根文件系统可能横跨几个存储设备,或者根文件系统在某个网络设备上。以使用NFS挂载根文件系统为例,除了要支持网卡驱动外,还要进行网络配置,甚至还要进行网络认证。某些根文件系统经过压缩、加密,在挂载前需要解压缩、解密等操作。如果这些都由内核处理,将会使内核变得异常复杂,继而可能导致内核的稳定性、可靠性、灵活性等一系列的问题。因此,将复杂的操作移到用户空间是解决上述问题的一个思路。
为了解决上述问题,内核开发者们设计了initramfs机制。initramfs是一个临时的文件系统,其中包含了必要的设备如硬盘、网卡、文件系统等的驱动以及加载驱动的工具及其运行环境,比如基本的C库,动态库的链接加载器等等。同时,那些处理根文件系统在RAID、网络设备上的程序也存放在initramfs中。由第三方程序(如Bootloader)负责将initramfs从硬盘装载进内存。bootloader如何将内核与initramfs载入内存,敬请期待,写好后我将连接放到此处
以驱动硬盘为例,内核就不必再从硬盘,而是从已经加载到内存的initramfs中获取硬盘控制器等相关驱动了,继而可以驱动硬盘,访问硬盘上的根文件系统,从而解决了前面提到的鸡和蛋的矛盾。
在初始化的最后,内核运行initramfs中的init程序,该程序将探测硬件设备、加载驱动,挂载真正的文件系统,执行文件系统上的/sbin/init,进而切换到真正的用户空间。真正的文件系统挂载后,initramfs即完成了使命,其占用的内存也会被释放。
在讲解initramfs之前,先介绍initramfs的一个前辈initrd
在2.4以及更早版本的内核中,内核使用的是initrd。initrd是基于ramdisk技术的,而ramdisk就是一个基于内存的块设备,因此initrd也具有块设备的一切属性。比如initrd容量是固定的,一旦创建initrd时设定了一个大小,就不能再进行动态调整。而且,如同块设备一样,initrd 需要按照一定的文件系统格式进行组织,因此制作initrd时需要使用如mke2fs这样的工具“格式化”initrd,访问initrd时需要通过文件系统驱动。更重要的是,虽然initrd是一个伪块设备,但是从内核的角度看,其与真实的块设备并无区别,因此,内核访问initrd也需使用缓存机制,显然这是多此一举的,因为本身initrd就在内存中。
由于initrd太拉胯,于是initram呼之欲出
鉴于ramdisk机制的种种限制,Linus Torvalds提出了一个想法:能否将cache当作一个文件系统直接挂载使用?基于这个想法,Linus Torvalds基于已有的缓存机制实现了ramfs。ramfs与ramdisk有着本质的区别,ramdisk本质上是基于内存的一个块设备,而ramfs是基于缓存的一个文件系统。因此,ramfs去除了前述块设备的一些限制。比如,ramfs根据其中包含的文件大小可自由伸缩;增加文件时,自动分配内存;删除文件时,自动释放内存。更重要的是,ramfs是基于已有的缓存机制,因此不必再像ramdisk那样需要和缓存之间进行多余的复制一环。
伴随着ramfs的出现,从2.6开始,内核开发人员基于ramfs开发了initramfs替代initrd。那么initramfs是怎样工作的呢?
当2.6版本的内核引导时,在挂载真正的根文件系统之前,首先将挂载一个名为rootfs的文件系统,并将rootfs的根作为虚拟文件系统目录树的总根。那么为什么要使用rootfs这么一个中间过程呢?原因之一还是为了解决鸡和蛋的问题。内核需要根文件系统上的驱动以及程序来驱动和挂载根文件系统,但是这些驱动和程序有可能没有编译进内核,而在根文件系统上。如果不借助第三方,内核是没有办法挂载真正的根文件系统的。而rootfs虽然名称为rootfs,但是并不是什么新的文件系统,事实上,rootfs就是一个ramfs,只不过换了一个名称。换句话说,rootfs是在内存中的,内核不需要特殊的驱动就可以挂载rootfs,所以内核使用rootfs作为一个过渡的桥梁。
在挂载了rootfs后,内核将Bootloader加载到内存中的initramfs中打包的文件解压到rootfs中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具,实现了挂载真正的根文件系统。此后,rootfs也完成了历史使命,被真正的根文件系统覆盖(overmount)。但是rootfs作为虚拟文件系统目录树的总根,并不能被卸载。但是这没有关系,前面我们已经谈到了,rootfs基于ramfs,删除其中的文件即可释放其占用的空间。
1、挂载rootfs
用于不同操作系统的文件系统其物理存储结构是不同的,但是Linux的虚拟文件系统通过为这些文件系统建立中间适配层,实现了Linux对各个文件系统的支持,Linux的虚拟文件系统将文件系统组织为树形结构。在初始化阶段,内核挂载rootfs文件系统,虚拟文件系统从无到有,rootfs的根作为虚拟文件系统这棵大树中的第一个节点,自然成为所有后来创建的节点的祖先。也就是说,虚拟文件系统目录树的根就是rootfs的根,本质上,rootfs就是一个ramfs文件系统。
通过挂载rootfs,虚拟文件系统的根目录已经建立起来,根目录已经可以容纳文件了。所以,接下来内核解压initramfs的内容到虚拟文件系统的根中,利用initramfs中的内容挂载并切换到真正的根文件系统。
2、解压initramfs到rootfs
在挂载了rootfs后,内核将Bootloader加载到内存中的initramfs中的文件解压到rootfs中。而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具实现挂载真正的根文件系统。
3、挂载并切换到真正的根目录
将initramfs成功解压后,挂载真正的根文件系统所需的驱动、程序等已经全部俱备,可以挂载真正的根文件系统了,在grub.cfg文件中对应的就是root=XXX。内核将真正的根文件系统挂载到initramfs文件系统中的/root目录下。
挂载真正的根文件系统后,rootfs中的内容已经没有保留的意义,但是并不能将rootfs卸载,因为rootfs是整个虚拟文件系统的根。因此,为了不占用内存空间,将rootfs中的内容(文件)释放掉即可,然后将真正的根文件系统移动到虚拟文件系统的根(即rootfs的根)下,最后再将进程的文件系统的namespace切换到真正的根文件系统。
lsinitrd
/usr/lib/dracut/skipcpio initramfs-3.10.0-229.el7.x86_64.img | zcat | cpio -ivd
[root@ct7_node02 initramfs]# find . | cpio -o -H newc | gzip -9 > /tmp/test.img
注意:修改完initramfs后,需要删除grub.cfg中的linux16 /vmlinuz-3.10.0-514.el7.x86_64 root=/dev/mapper/cl-root ro biosdevname=0 net.ifnames=0 console=ttyS0,115200n8不相关的内容,否则会导致内核启动失败。
内核启动后会执行initramfs中的init程序,创建一个init,里面写入bash脚本,脚本添加两行信息,屏幕打印hello信息,并运行bash程序,exec是内核运行程序的函数,bash一般存放在/bin/bash路径,这里保持这个路径,其次是添加bash所依赖的库文件。
[root@ct7_node02 initramfs]# ll
total 4
drwxr-xr-x 2 root root 18 Aug 2 15:14 bin
-rwxr-xr-x 1 root root 49 Aug 2 15:13 init
drwxr-xr-x 2 root root 90 Aug 2 15:17 lib64
[root@ct7_node02 initramfs]# cat init
#!/bin/bash
echo "Hello Michael"
exec /bin/bash
[root@ct7_node02 initramfs]# tree ./
./
|-- bin
| `-- bash
|-- init
`-- lib64
|-- ld-linux-x86-64.so.2
|-- libc.so.6
|-- libdl.so.2
`-- libtinfo.so.5
2 directories, 6 files
[root@ct7_node02 initramfs]#
通过ldd命令查看bash所依赖的库文件,并将这些文件复制到上面的lib64目录中
[root@ct7_node02 initramfs]# ldd /usr/bin/bash
linux-vdso.so.1 => (0x00007ffcbd4aa000)
libtinfo.so.5 => /lib64/libtinfo.so.5 (0x00007f4268c01000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f42689fd000)
libc.so.6 => /lib64/libc.so.6 (0x00007f426863b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4268e31000)
[root@ct7_node02 initramfs]#
[root@ct7_node02 tmp]# lsinitrd test.img
Image: test.img: 1.4M
========================================================================
Version:
Arguments:
dracut modules:
========================================================================
drwxr-xr-x 4 root root 0 Aug 2 15:17 .
drwxr-xr-x 2 root root 0 Aug 2 15:14 bin
-rwxr-xr-x 1 root root 960392 Aug 2 15:14 bin/bash
-rwxr-xr-x 1 root root 49 Aug 2 15:13 init
drwxr-xr-x 2 root root 0 Aug 2 15:17 lib64
-rwxr-xr-x 1 root root 155064 Aug 2 15:15 lib64/ld-linux-x86-64.so.2
-rwxr-xr-x 1 root root 2116736 Aug 2 15:15 lib64/libc.so.6
-rwxr-xr-x 1 root root 19344 Aug 2 15:16 lib64/libdl.so.2
-rwxr-xr-x 1 root root 174520 Aug 2 15:16 lib64/libtinfo.so.5
========================================================================
将这些文件制作成initramfs并添加到/boot目录下进行测试
同理其他的命令,例如ls、cd、pwd等的相关命令都可通过以上方式添加到initramfs文件中