linux启动流程——initrd和initramfs

Linux内核启动

linux启动流程——initrd和initramfs
当系统首次引导时,或系统被重置时,处理器会执行一个位于已知位置处的代码。在个人计算机(PC)中,这个位置在基本输入/输出系统(BIOS)中,它保存在主板上的闪存中。嵌入式系统中的*处理单元(CPU)会调用这个重置向量来启动一个位于闪存/ROM
中的已知地址处的程序。在这两种情况下,结果都是相同的。因为 PC
提供了很多灵活性,BIOS
必须确定要使用哪个设备来引导系统。稍后我们将详细介绍这个过程。

当找到一个引导设备之后,第一阶段的引导加载程序就被装入 RAM
并执行。这个引导加载程序在大小上小于 512
字节(一个扇区),其作用是加载第二阶段的引导加载程序。

当第二阶段的引导加载程序被装入 RAM 并执行时,通常会显示一个动画屏幕,并将
Linux 和一个可选的初始 RAM
磁盘(临时根文件系统)加载到内存中
。在加载映像时,第二阶段的引导加载程序就会将控制权交给内核映像,然后内核就可以进行解压和初始化了。在这个阶段中,第二阶段的引导加载程序会检测系统硬件、枚举系统链接的硬件设备、挂载根设备,然后加载必要的内核模块。完成这些操作之后启动第一个用户空间程序(init),并执行高级系统初始化工作。

Initrd

initrd 字面上的意思就是"boot loader initialized RAM
disk",换言之,这是一块特殊的RAM disk,在载入Linux kernel 前,由boot loader
予以初始化,具体动作就是从特定的储存装置中载入initrd 到RAM
中(由启动参数"initrd=" 指定image 的实体或逻辑位置),随后linux kernel
被载入并执行时,会优先处理置放initrd
的记忆体空间,而这个空间基本上也有档案系统,通常会包含init
等程式,故可用以挂入某些特别的驱动程式,比方说SCSI,完成阶段性目标后,kernel
会将真正的root file system 挂载,并执行/sbin/init 程式。

话说回来,我们为何需要此等迂回的开机途径呢?原因是,root file system
(由启动参数"root="所指定,以下简称rootfs)所在的储存装置很可能极难寻找,比方说SCSI装置就需要复杂且耗时的程序,若用RAID系统更是需要看配置情况而定,同样的问题也发生在USB
storage上,因为kernel得花上更长的等待与配置时间,或说远端挂载rootfs,不仅得处理网路装置的问题,甚至还得考虑相关的伺服器认证、通讯往返时间等议题。更重要的是,我们可在initrd放置某些特别的程式,一来作为挂载rootfs作准备,比方说硬体初始化、解密、解压缩等等,二来提示使用者或系统管理员目前的状态,这对于消费性电子产品来说,有很大的意义。整体来说,如果能增加开机的弹性(比方说配合简单的shell
script即可达成USB/SCSI初始化动作,若透过kernel
code实做,恐怕上百千行是免不掉的),又能适度降低kernel
image本身的设计复杂度与空间使用量,采取initrd是很不错的方式,所以几乎各大Linux
distribution都有提供initrd,以解决在不同硬体、不同装置上开机的技术议题,也能确保一片CD-ROM/DVD可装入多种个人电脑系统,也可支援[
bootsplash ]一类显示开机动画的程式。

具体来说,initrd 提供了「两阶段开机」程序。首先,一切都还是在kernel
mode,由kernel 完成与硬体相关的初始化工作,接着,在适当的时机点,当kernel
读取并挂载initrd 所在记忆体空间的档案系统后,kernel 首次从kernel space切入user
space,以执行存放于RAM disk 中的init 程式,当然,这需要完整的执行环境(比方说C
runtime 或必要的program loader 等),另外,也得确定rootfs 可被kernel
所找到并正确挂载。待第一阶段的initrd 步入尾声后,再回到kernel mode,initrd
所在的记忆体空间也会适度被释放(依据组态而定),这才进入第二阶段,也就是执行真正的rootfs
中的init 程式。在Linux kernel 2.4 中,initrd
大致的处理流程如下:(方括号表示主要的执行单元)

  • [boot loader] Boot loader 依据预先设定的条件,将kernel 与initrd 这两个image
    载入到RAM

  • [boot loader -> kernel] 完成必要的动作后,准备将执行权交给Linux kernel

  • [kernel] 进行一系列初始化动作,initrd 所在的记忆体被kernel 对应为/dev/initrd
    装置设备,透过kernel 内部的decompressor (gzip 解压缩)
    解开该内容并复制到/dev/ram0 装置设备上

  • [kernel] Linux 以R/W (可读写) 模式将/dev/ram0 挂载为暂时性的rootfs

  • [kernel-space -> user-space] kernel 准备执行/dev/ram0 上的/linuxrc
    程式,并切换执行流程

  • [user space] /linuxrc 与相关的程式处理特定的操作,比方说准备挂载rootfs 等

  • [user-space -> kernel-space] /linuxrc 执行即将完毕,执行权转交给kernel

  • [kernel] Linux 挂载真正的rootfs 并执行/sbin/init 程式

  • [user space] 依据Linux distribution 规范的流程,执行各式系统与应用程式

值得一提的是,以上「两阶段开机」是initrd提出的弹性开机流程,在真实的应用中,也可能从未需要挂载真正的rootfs,换言之,只是把系统当作都在RAM
disk上运作,或者永远都在initrd所引导执行的/linuxrc程序中执行(注意:kernel永远保留PID=1作为init
process识别,而/linuxrc执行的PID必非为1),在许多装置如智慧型手机,都是行之有年的,不过这不影响我们后续的探讨。

initramfs

Linux
Kernel的发展文化就是愿意舍弃既有实做,大胆采用新的途径(在符合国际规格的前提下),Linux
2.6的initramfs之所以提出,就是要修正initrd的种种技术问题。问题在哪呢?

首先,回顾刚刚探讨的流程,initrd RAM disk对kernel来说,本身是个真实的block
device,为了建构存放其中的档案(最起码要有/linuxrc),通常我们需要ext2一类的档案系统(建议)。所以,就建构如此的initrd
image来看,通常会透过mkfs.ext2与losetup (功能:“set up and control loop
devices”)等工具建立loopback device并编修,所以自然需面对以下问题:

  • initrd
    必须绑定某个档案系统实做,如ext2,可是多数的情况下,我们根本不需要在此阶段拥有完整的实做

  • /dev/initrd block device 建构时即有空间限制,维护繁琐

  • 运作于initrd 阶段,档案操作实际上是不断将/dev/initrd (对应于某段记忆体)
    对应到可存取档案系统的记忆位址,做了不必要的资源消耗

事实上,原本的设计甚至更加浪费记忆体,因为Linux在设计上就会尽可能将读入/写入自block
device的档案或目录予以cache,所以,Linux会自ramdisk中复制资料到page
cache与dentry
cache,如此往返,徒增资源使用的浪费,这一切问题的根源就是将initrd以block
device来操作的本质使然。Linus Torvalds为此提出一个想法:能否将这些cache
被挂载为档案系统呢?就在cache
中保持这些档案,但不清除这些,直到实际上被删去或者系统重启。

基于这些想法,Linus Torvalds
实做了ramfs,随后在其他核心开发者的改进下,成为tmpfs,支援写入swap
空间与限制记忆体使用量等特征。而,initramfs 就是建构于tmpfs
的基础上。采取此途径的效益就是,档案系统可自行调整空间使用量,以符合所需资料储存的空间,同时,也不再会有重复的block
device 与cache
资料,因为跟本不需要,更重要的是,这样的档案系统实做,其实就只是cache
机制的延伸,没有太多新的程式码,所以系统可保持简单明了。以下是对initrd
与initramfs 的概念性比较:

initrd initramfs
Image 压缩过的档案系统(如ext2 + gzip) 封装过的档案(cpio + gzip)
实做途径 block device (RAM disk) tmpfs
首先执行的程式 /linuxrc /init
挂载rootfs方式 将欲载入的rootfs 挂载于某个目录,再pivot_root 切换rootfs 使用switch_root

如同前述所及,Linux kernel
2.4中,initrd可被视为起始参数"root="的先前处理机制,透过一系列的程序,协助kernel找到最终的rootfs,并一举挂载进系统,不过,过去的设计其实做了一个假设:「真正的rootfs所在的装置是block
device,同时initrd绝非是真正的rootfs」,这也是为何要让kernel在第一次准备切入user-space时,是执行/linuxrc
,而非/init或/sbin/init,因为后者的PID恒为1且不可被kill
(终止),但前者因为只是过度的存在,随时仍可被kill。

而在Linux
2.6引入initramfs的设计后,上述别扭的假设与处理方式就不复存在,不再区隔「真正」的rootfs是如何「存在」,也就是一开机,kernel就执行位于initramfs中的/
init,作为PID=1的init
process,仅以switch_root作rootfs的重新定位罢了(选择性)。正因为这样的特性,核心开发者也将initramfs的行为称为[
Early User Space ]

参考:

https://en.wikipedia.org/wiki/Linux_startup_process

https://www.ibm.com/developerworks/cn/linux/l-linuxboot/index.html

http://blog.linux.org.tw/~jserv/archives/001954.html