4.1 为什么需要initramfs
在引导过程的最后,内核启动第一个用户进程,内核需要访问根文件系统,加载相应的可执行程序。这就要求内核能够正确驱动根文件系统所在设备。所以在上一章中,我们将SATA磁盘驱动编译进了内核,为的就是内核可以正确驱动硬盘。
寻找根文件系统的过程中,我们需要考虑以下两种情况。
第一,除非是一个专用系统,目标系统的硬件平台是固定不变的,否则,对于一个通用操作系统,比如Linux的桌面发行版,将运行在各种不同的硬件平台上。因此,根文件系统可能存储在各种各样的介质上,比如IDE硬盘、SATA硬盘、SCSI硬盘、Flash存储器,以及随着技术的发展,不断出现的新的存储设备。为了能够兼容更多的硬件平台,显然系统需要支持尽可能多的存储设备。但是如果将所有这些设备的驱动全部编译进内核,显然不是一个好办法。因为对于某个特定的硬件平台,可能只需要一个驱动即可,内核中的其他驱动根本用不上,将它们编译进内核只会徒增内核的尺寸、占用内存空间,尤其对于一些内存或者存储介质空间有限的设备,这个问题尤为明显。于是将这些驱动编译为模块,存储在根文件系统中,按需载入内存是一个解决问题的办法。
第二,根文件系统可能不在一个简单的硬盘上,比如当使用磁盘阵列RAID时,根文件系统可能横跨几个存储设备,或者根文件系统在某个网络设备上。以使用NFS挂载根文件系统为例,除了要支持网卡驱动外,还要进行网络配置,甚至还要进行网络认证。某些根文件系统经过压缩、加密,在挂载前需要解压缩、解密等操作。如果这些都由内核处理,将会使内核变得异常复杂,继而可能导致内核的稳定性、可靠性、灵活性等一系列的问题。因此,将复杂的操作移到用户空间是解决上述问题的一个思路。
但是,无论是将驱动编译为模块,还是将处理如RAID挂载的程序存储在文件系统上,都会导致一个鸡和蛋的问题:内核要加载这些模块或者运行这些程序才能正确识别根文件系统所在的设备,但是保存这些模块或者程序的根文件系统又存储在这些设备上。
为了解决上述问题,内核开发者们设计了initramfs机制。initramfs是一个临时的文件系统,其中包含了必要的设备如硬盘、网卡、文件系统等的驱动以及加载驱动的工具及其运行环境,比如基本的C库,动态库的链接加载器等等。同时,那些处理根文件系统在RAID、网络设备上的程序也存放在initramfs中。由第三方程序(如Bootloader)负责将initramfs从硬盘装载进内存。以驱动硬盘为例,内核就不必再从硬盘,而是从已经加载到内存的initramfs中获取硬盘控制器等相关驱动了,继而可以驱动硬盘,访问硬盘上的根文件系统,从而解决了前面提到的鸡和蛋的矛盾。
在初始化的最后,内核运行initramfs中的init程序,该程序将探测硬件设备、加载驱动,挂载真正的文件系统,执行文件系统上的/sbin/init,进而切换到真正的用户空间。真正的文件系统挂载后,initramfs即完成了使命,其占用的内存也会被释放。