深入理解Linux 2.6的initramfs机制(上)
日前结束一个消费性电子产品的开发工作,稍有心得,试着整理采取Linux kernel 2.6 initramfs机制以加速系统开发的经验,同时也谈论对fast-booting设计的重要性,顺便解决某些像是「 kinit/klibc为何被提出?」等疑难杂症。
进入主题前,先看看所谓的booting。 相传在十八世纪,德国Baron Münchhausen男爵常夸大吹嘘自己的英勇事迹,其中一项是「拉着自己的头发,将自己从受陷的沼泽中提起」,此事后来收录于德国《吹牛大王历险记》,则改写为「用拔靴带把自己从海中拉起来」,这里的「拔靴带」(bootstrap)指的是长统靴靴筒顶端后方的小环带,是用以辅助穿长统靴。 这种有违物理原理的夸大动作,却让不同领域的人们获得灵感,Robert A. Heinlein发表于1941年的短文〈By His Bootstraps〉收录典故并给予多种延伸想法;滑鼠发明人博士甚至在1989年以此命名其研究机构「Bootstrap学院」,并担任该院主任。 在商业上,bootstrapping则被引申为一种创业模式,也就是初期投入少量的启动资本,然后在创业过程中主要依靠从客户得来的销售收入,形成一个良好的正现金流。 在电脑资讯领域,因为开机过程是环环相扣,先透过简单的程式读入记忆体,执行后又载入更多磁区、程式码来执行,直到作业系统完全载入为止,所以开机过程也被称为bootstrapping,简称"boot"。
自1991年Linux问世以来,资讯技术的应用有了极大的转变,笔者之前的文章[ 探索Linux bootloader的佳作 ]与[ kboot初探与模拟验证 ]约略提及光是开机本身的设计来说,就有多种冲击与需求,随着Linux走出个人电脑领域,在嵌入式系统应用上,更是五花八门。 本文所探讨的initramfs,衍生自Linux kernel的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可装入多种个人电脑系统,也可支援[ ]一类显示开机动画的程式。
具体来说,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),在许多装置如智慧型手机,都是行之有年的,不过这不影响我们后续的探讨。
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 (对应于某段记忆体)对应到可存取档案系统的记忆位址,做了不必要的资源消耗
Kernel文件( Documentation/filesystems/ramfs-rootfs-initramfs.txt )更指出:Another reason ramdisks are semi-obsolete is that the introduction of loopback devices offered a more flexible and convenient way to create synthetic block devices, now from files instead of from chunks of memory.
基于上述资源使用与效能考量,原本ramdisk途径就被标示为「老旧」,而initramfs的提出,则是基于更简单有效率的ramfs与新的处理方式。
回到initrd ramdisk,事实上,原本的设计甚至更加浪费记忆体,因为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 |
前面的段落已说明这两者对于记忆体存取与档案操作的落差,同时也提及实做途径,接下来的重点是这两者如何看待真正的rootfs。 如同前述所及,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的行为称为[ ],Jeff Garzik于2002年十一月发表于lkml的文章[ ]提到他的愿景:The Future.
Early userspace is going to be merged in a series of evolutionary changes, following what I call "The Al Viro model." NO KERNEL BEHAVIOR SHOULD CHANGE. [that's for the lkml listeners, not you ] "make" will continue to simply Do The Right Thing(tm) on all platforms, while the kernel image continues to get progressively smaller.
核心开发者很喜欢彼此取笑,这里提到的[ ]是位知名的kernel hacker,常常为了捍卫核心设计的一致性与许多开发者对立。 这意思就是说,藉由Early userspace整合到核心设计后,原本很不容易处理的开机模式,比方说LVM (Linux Volume Manager)、网路开机、特别储存装置的开机等,都可交由user- space的应用程式专门处理,相对来说,kernel就不必过度涉入,长远来说,对于发展的分工、降低系统复杂度,以及提高可性赖度,均有很大的助益。
基于initramfs / Early userspace的想法,核心开发者又思考为何不将过去难以有效维护但又非得存在不可的程式码,比方说do_mount这一类用以实做挂载特定装置或逻辑储存设备的功能,全面转交给user-space的程式去执行呢? 这样kernel可专心提升功能或者效能的改进。为此,以H. Peter Anvin为首的核心开发者引入[ ]与kinit,前者(至少目标上)是最小的C library实做,用来支持后者所需(定位与[ ]或[ ]一类精巧但通用性的libc实做不同),而kinit就是将前述原本在核心实做的程式(很难侦错且分析的kernel code)拉出到user-space中,他于2006年六月提交的patch [ ]就展现了将不同的档案系统(cramfs, ext2, ext3, jfs, lvm2, minixfs, reiserfs, romfs, xfs, ...)予以挂载(即user-space的do_mount)、ipconfig (bootp, dhcp)、nfsmount等等,整合到kinit程式中一并处理,kernel image可因此大幅缩减。
大致理解initramfs的原理与定位后,我们就可以探讨实做与相关的细节。 笔者的测试环境是IBM/lenovo X60笔记型电脑(Intel Centrino Duo 1.83GHz)加上Ubuntu Linux 7.10,进行下述实做过程之前,请先自[ ]取得stable kernel,本文采用"linux- 2.6.22.5",所需的套件有:假设工作目录为$HOME/initramfs-workspace,作些准备动作:('$'开头表示输入的指令,以下同)
$ cd /home/jserv/initramfs-workspace
$ tar jxvf $HOME/sources/linux-2.6.22.5.tar.bz2
$ mkdir -p hello-initramfs
首先设立的目标是,可印出"Hello World"的kernel + initramfs,并透过qemu进行模拟验证。 首先,建立一个init.c,具备简单的实做: $ cd hello-initramfs
$ cat init.c
#include
int main()
{
printf("Hello World!\n");
sleep(99999);
return 0;
}
$ gcc -static -o init init.c
$ mkdir -p dev
$ sudo mknod dev/console c 5 1
建议先试着执行"./init"看看是否正确运作,程式码中的"sleep(99999)"只是让观察更容易,避免画面一闪而逝。 刚刚的"Hello World"程式就是我们预期的Early userspace,因为执行时期需要tty (terminal),所以刚刚也一并建立/dev/console的character device。 现在我们可以来准备建构kernel了: $ cd /home/jserv/initramfs-workspace/linux-2.6.22.5
$ make menuconfig
要注意的是,需将"General setup"的子项目"Initial RAM filesystem and RAM disk (initramfs/initrd) suppot"打开,并在下方提示"INITRAMFS_SOURCE"的画面输入我们期望的initramfs的来源目录,也就是"/home/jserv/initramfs-workspace/hello-initramfs",参考的配置画面如下:
也可以参考笔者的组态档[ ],当然之后就是建构核心: $ make bzImage
...
LD arch/i386/boot/compressed/vmlinux
OBJCOPY arch/i386/boot/vmlinux.bin
HOSTCC arch/i386/boot/tools/build
BUILD arch/i386/boot/bzImage
Kernel: arch/i386/boot/bzImage is ready
建构成功,透过qemu来模拟测试: $ qemu -kernel arch/i386/boot/bzImage -hda /dev/zero
参考的执行画面如下:
所以我们可以发现,在产生出来的kernel image中,其实已经包含了刚刚的initramfs,看来里头大有文章。 回头看看编译的过程: scripts/kconfig/conf -s arch/i386/Kconfig
CHK include/linux/version.h
CHK include/linux/utsrelease.h
CC arch/i386/kernel/asm-offsets.s
GEN include/asm-i386/asm-offsets.h
...
CC init/initramfs.o
CC init/calibrate.o
LD init/built-in.o
HOSTCC usr/gen_init_cpio
GEN usr/initramfs_data.cpio.gz
AS usr/initramfs_data.o
...
我们可注意到"GEN usr/initramfs_data.cpio.gz"这行,势必kernel 2.6中隐含了某种机制,执行看看之前产生的工具程式: $ usr/gen_init_cpio
Usage:
usr/gen_init_cpio
is a file containing newline separated entries that
describe the files to be included in the initramfs archive:
...
这里提到的"archive"就是透过[ ]工具产生的封装档案,在RedHat .rpm或Debian .deb均有采用此工具。 不过Linux kernel则提供一个整合性的工具,可一次处理目录与档案的封装,依据之前的流程试试看手动建立cpio + gzip: $ cd /home/jserv/initramfs-workspace
$ sudo cp -af hello-initramfs hello2-initramfs
$ cd hello2-initramfs
$ cat init.c
#include
int main()
{
printf("Yat Another Hello World!\n");
sleep(999999);
return 0;
}
$ gcc -static -o init init.c
$ cat desc_initramfs
dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
file /init /home/jserv/initramfs-workspace/hello2-initramfs/init 0755 0 0
$ ../linux-2.6.22.5/usr/gen_init_cpio desc_initramfs > my_initramfs.cpio
$ gzip my_initramfs.cpio
"desc_initramfs"是我们自己写的描述档,格式大抵就如上面展示,usr/gen_init_cpio这个工具则会建构对应的dir + device node + file的封装,最后我们以gzip压缩起来,于是可得到"my_initramfs. cpio.gz"这个新的initramfs image。 同样的,我们可用qemu测试验证,这次改由qemu模拟boot loader指定initramfs image的模式,操作如下: $ cd /home/jserv/initramfs-workspace/linux-2.6.22.5
qemu -kernel arch/i386/boot/bzImage -initrd ../hello2-initramfs/my_initramfs.cpio.gz -hda /dev/zero
这次应该就会在qemu模拟的输出画面最下方看到"Yat Another Hello World!"的字样。
只有"Hello World"一类的程式只能作切入点,还不能实际作点事情,从零到有建构rootfs也得花上一点功夫,还好,Ubuntu/Debian已经提供静态连结的[ ],安装方式很简单: $ sudo apt-get install busybox-static
随后,系统会安装/bin/busybox的执行档,观察一下: $ file /bin/busybox
/bin/busybox: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.8, statically linked, stripped
咱们就以此为基础,建立一个小而美的initramfs + kernel image: $ cd /home/jserv/initramfs-workspace
$ mkdir -p busybox-initramfs/bin
$ mkdir -p busybox-initramfs/proc
$ cd busybox-initramfs/bin
$ cp /bin/busybox .
$ ./busybox --help | ruby -e 'STDIN.read.split(/functions:$/m)[1].split(/,/).each{|i|`ln -s busybox #{i. strip}` unless i=~/busybox/}'
$ cd ..
$ echo -e '#!/bin/busybox sh\nmount -t proc proc /proc\nexec busybox sh\n' > init ; chmod +x init
$ find . | cpio -o -H newc | gzip > ../busybox.initramfs.cpio.gz
可看到$HOME/initramfs-workspace就输出了名为busybox.initramfs.cpio.gz的initramfs image,可仿造上一个范例,透过qemu模拟: $ cd /home/jserv/initramfs-workspace/linux-2.6.22.5
qemu -kernel arch/i386/boot/bzImage -initrd ../busybox.initramfs.cpio.gz -hda /dev/zero
参考的执行画面如下:
后续的篇幅,我们会探讨实务上如何应用,如Ubuntu的software suspend/resume image与fast-booting整合,以及kernel的实做细节。
阅读(1915) | 评论(0) | 转发(0) |