::欢迎转载,请注明出处和链接:)
如果你了解Linux系统,一定知道系统的启动参数中,有一个叫做“initrd=”的选项,再进一步,你也许听过关于initrd和initramfs的一些故事。如果你刚好是一位嵌入式Linux系统开发者,那么你的目标板系统也许用不上initramfs,但会发现,宿主机Linux系统上的GRUB(一种Bootloader)配置文件中,Linux系统的启动参数一定包含“initrd=”这个选项。
那么,你是否对这几个概念有过疑问,或者有探究一下的好奇心?今天,通过简单梳理Linux系统的启动过程,我们一起来聊一下initramfs的前世今生,以及“initrd=”和”root=”两个启动参数的亲密关系。友情提醒,“initrd=”加=号以示与initrd具体类型的区别,后同。
▲
令人眼花缭乱的名词~
先来看看Linux系统的启动过程。
1. Linux系统的启动过程
一个Linux系统要启动并运行起来,大概会经历这几个过程,如图所示。
▲
Linux系统的启动过程
Bootloader把内核镜像加载进内存后,内核完成自解压和一些必要的初始化,进入start_kernel。这时Bootloader传进来的内核启动参数cmdline,跟应用开发中启动一个进程时带的一串参数,其实非常相似,增加了系统的伸缩性和可配置性。接下来,内核首先要进行基本的系统初始化,比如CPU、内存、时钟、中断向量、定时器、进程状态机、模块的集中初始化等,完成这些工作后系统就绪了,下一步要进入用户空间继续其他初始化。
这个用户空间的初始化入口,就是一个负责系统初始化的最小文件系统(initial fs),由Bootloader启动参数“initrd=”来指定。让人不解的是,“initrd=”参数名竟然与initrd类型镜像重名!其实,这跟它的发展历史有关,一开始用“initrd=”来表示,后来用习惯了,改动反而会牵连太多,所以索性不改了。它是用个体实例来命名一类功能,容易以偏概全,也给同学们造成一定的困扰,本文表述时尽量用“initrd=”和initrd来区分。所以,建议“initrd=”改成initfs=或entry=或usentry=(即userspaceentry)。
接着,initrd中的启动脚本处理一些前期的准备工作,比如加载rootfs需要的驱动,最后一般会调用chroot工具转向最终的rootfs。initrd中的启动脚本也可以由rdinit=指定。
根文件系统rootfs一般部署在一个块设备的某个分区上,所以需要另一个内核启动参数root=来指定这个分区。看到这里,应该明白root=就是指定根文件系统设备(root device),所以建议不如改成rootdev=,毕竟root的含义太广泛了。
如果不指定“initrd=”,比如给出这个参数noinitrd,那么,就要指定一个root=来确定根文件系统分区。
成功加载rootfs后,通过参数init=来找到用户空间执行的第一个程序,一般是rootfs中的/linuxrc或/init等。当然,也可以不指定init=,此时内核会自动寻找/sbin/init或/bin/init等,具体实现和差别参见相关内核源码。比如linux-3.18.109是这样的。
点击(此处)折叠或打开
- if (!try_to_run_init_process("/sbin/init") ||
- !try_to_run_init_process("/etc/init") ||
- !try_to_run_init_process("/bin/init") ||
- !try_to_run_init_process("/bin/sh"))
- return 0;
进入最终的rootfs后,一般会经过/sbin/init、inittab、rc.system、一系列系统基础服务、rc.local的初始化过程。其中一系列系统基础服务的启动,从早期SysVinit的串行,到Upstart的部分并行,再到如今Systemd的完全并行,效率越来越高,系统启动越来越快。系统基础服务的这三种启动策略,分别在红帽的RHEL5、RHEL6、RHEL7发行版中实现。当然,对于各个Linux发行版来说,BaseServices的启动过程和策略会有差异,比如新的Systemd服务已经把inittab给抛弃了。
小结一下,“initrd=”表示要加载的初始化实体initialfs,rdinit=表示初始化实体initialfs中的启动脚本,init=表示系统要真正执行的第一个用户程序。后面两个现在使用不多。“initrd=”一般用在桌面版或服务器操作系统中,嵌入式系统中用得不多。
2. 鸡生蛋,还是蛋生鸡?
在Linux系统的启动过程中,内核要想成功挂载根文件系统rootfs,首先必须能识别rootfs所在设备的物理类型,还要读懂rootfs的文件系统类型。所以,这就需要rootfs所在块设备驱动,需要相应的文件系统驱动,可能需要逻辑卷相关模块,还可能需要特殊的数据解密驱动。
这些驱动模块,如果放在rootfs中,就不能使用!因为承载rootfs的根文件系统设备还没就绪;如果放在内核image中,内核镜像可能会太臃肿,而且每增加支持一种新设备或文件系统,就要重新编译一次内核镜像,耦合太紧;再加上内核较严格的软件许可证制度,而很多设备厂商有一定的知识产权保护要求,这些驱动模块也许不方便放入内核镜像(另,Android采用了另外一种机制HAL规避知识产权冲突)。
鸡生蛋,or 蛋生鸡
▼
那么,怎么解决这个难题呢?
解决办法其实刚才在描述Linux系统的启动过程时已经给出来了,就是提供一个用户空间的初始化实体,即一个初始化最小文件系统(initial fs)。
具体实现有两个办法,一是把这些驱动模块通通放进initial fs,二是在initialfs启动后,用U盘加载相应设备的二进制驱动。新的Dell服务器支持旧的操作系统,比如RHEL5.5时,由于旧系统没有对应的RAID驱动,采用的就是第二种办法,即发布RAID设备的相应版本二进制驱动的image,initial fs启动后通过linuxdd方式读取U盘镜像,从而加载RAID控制卡驱动,支持RAID磁盘。
在嵌入式Linux系统中,这个问题却并不存在!因为,嵌入式Linux系统普遍把rootfs部署在flash中。内核直接把块设备驱动和文件系统驱动编译进去,因为固定类型的flash,其文件系统也是固定的,比如nandflash这种块设备,一般都使用yaffs或ubi文件系统。内核只需要支持特定的块设备或文件系统,就可以很好的实现需求。
但在桌面和服务器Linux操作系统的发展中,存储的块设备一直在进化,其驱动当然也在变化,同时,操作系统支持的文件系统也越来越多。再把块设备驱动和文件系统驱动编进内核,会让内核越来越庞大。
所以还是需要一个初始化实体initial fs,于是initrd技术“粉墨登场”了。
3. initrd
Linux kernel在自身初始化完成之后,需要能够找到并运行第一个用户程序(这个初始化脚本或程序通常叫做“init”)。用户程序init存在于文件系统之中,因此,内核必须找到并挂载一个文件系统,才可以成功完成系统的引导过程。
随着硬件的发展,很多情况下这个文件系统也许是存放在USB设备、SCSI设备、RAID设备等等多种多样的设备之上,如果需要正确引导,USB、SCSI或者RAID驱动模块首先需要运行起来,可是不巧的是,这些块设备驱动程序也是存放在文件系统里,这时候就形成了一个悖论般的死循环,也就是前面说过的“鸡生蛋or蛋生鸡”问题。
为解决此问题,Linux kernel提出了一个RAM Disk的解决方案,把一些启动所必须的用户程序和驱动模块放在RAM Disk中,这个RAM Disk看上去和普通的Disk一样,有文件系统、有cache,就差真实的Disk设备驱动了(因为是在RAM中模拟Disk设备,所以不需要真实的Disk设备驱动)。内核启动时,首先把RAM Disk挂载起来,等到初始化程序和一些必要模块运行起来之后,再切到真正的根文件系统之中。
这里RAM Disk的方案实际上就是initrd!其中rd即RAM Disk的简写。
内核启动完后要先挂载这个初始文件系统initrd,在initrd中处理完部分基础工作,并加载rootfs所在设备的驱动和指定文件系统的驱动后,再把真正的rootfs挂载到根目录“/”上。于是,在GRUB中提供一个选项“initrd=”用来指定这个初始化最小文件系统,所以,GRUB的启动参数一般是:kernel=/boot/vmlinuz root=/dev/sda3,rw initrd=/boot/initrd.img。
如前所述,“initrd=”是指定initrd.img所在位置,“root=”是指定真正的rootfs所在块设备分区。这里就是要让initrd.img支持块设备/dev/sda3的驱动,前面也提到过,把各种不同块设备的驱动编进内核不是一种最佳选择,因为内核不能太臃肿了,而且,模块静态编译进内核可能会使其太大而不能适应存储空间(尤其是嵌入式系统中flash空间有限),或者静态编译可能会违反内核软件许可条款GPL。所以,针对不同的存储外设,灵活配以不同的initrd.img,而内核镜像保持稳定,才是最好的产品发布策略。
如果“root=/dev/ram”,那么就把运行在内存中的initrd.img当作最终的rootfs,写在上面的文件断电无法保存,要保存新的文件需另外挂载用户文件系统(比如/dev/sda5)到某个文件夹(比如/tftpboot)。这样的话,似乎进化成了后面的initramfs。
▲
initrd也可以进化成initramfs?
仔细考虑一下,RAM Disk的方案initrd虽然解决了问题但并不完美。比如,Disk有cache机制,对于RAM Disk来说,这个cache机制就显得很多余且浪费空间;Disk需要文件系统,那文件系统(如ext3等)必须被编译进kernel而不能作为模块来使用。
于是,Linux 2.6 kernel提出了一种新的实现机制,即initramfs技术。
好,下面继续聊一下initrd的升级版,即今天的主角——initramfs。
4. initramfs
顾名思义,initramfs是一种RAMfilesystem而不是RAM Disk。initramfs实际是一个cpio压缩包,启动所需的用户程序和用于读取rootfs的驱动模块,通通被打包成了一个文件。因此,加载initramfs不需要cache,也不需要文件系统驱动。它只是一个最小文件系统的目录而已。
▲
原来initrd和initramfs是两种东西。。。
如果说initrd实现了第一次飞跃,脱离了物理意义上的根设备,initrd镜像挂载在内存模拟的块设备上,那么相比initrd,initramfs更进一步,完全没有了根设备Disk。initramfs直接解压并加载在内存中,这样就运行更快。而且initramfs更加轻量化,系统启动更快,更多的处理放在用户的启动脚本里。比如把存储用户数据的块设备分区挂载到某个子目录中去,如/dev/sda2挂载到/tftpboot用作用户程序,/dev/sda3挂载到/data用作存储用户数据库的内容。当然,最重要的根设备(由root=指定),存放着最终的rootfs,会首先通过chroot跳转并被挂载到根目录“/”上!
有时候,启动参数中也可以不指定root=。无需切到某个硬盘分区的rootfs上,基础文件系统basefs驻留在内存,不必把一些系统常用共享库经常换出内存,这样系统运行效率更高。这是一个很棒的主意!在一些要求颇高、硬件配置(关键是内存)也高的服务器中得到了应用,比如通信系统中的核心网服务器。在通信行业的核心网平台干了十来年的我,对此深有体会。
实际上,“initrd=”也可以不用指定,因为通过内核配置,可以在编译内核时,把initramfs跟内核镜像一起打包,内核镜像的size虽然有点偏大,但适合某些启动时只能接收一次镜像的Bootloader,比如国产Loongson的PMON。
▲
内核配置initramfs与kernel捆绑在一起
如上图所示。内核配置make menuconfig,选择Generalsetup ---> Initial RAM filesystem and RAM disk(initramfs/initrd) support,选择 Initramfs source file(s),输入initramfs所在文件夹,比如当前子目录_initfs。
▲
原来initramfs可以做这么多文章。。。
让人无解的是,话说都升级成initramfs了,RHEL5、RHEL6却还是给initial fs image起了一个类似initrd-2.6.18.img的名字,这无疑让世人误会更深。。。
这个问题直到RHEL7才解决了~~如下所示。
点击(此处)折叠或打开
- linux16 /vmlinuz-3.10.0-693.el7.x86_64 root=/dev/mapper/centos-root ro
- initrd16 /initramfs-3.10.0-693.el7.x86_64.img
5 . 嵌入式系统的启动参数
在实际的嵌入式系统应用中,“initrd=”也可以不指定,即通过bootcmd=bootmkernel_addr rootfs_addr中rootfs_addr来指定初始文件系统的位置。
当然嵌入式系统的启动参数,还有其他配置方法。
按照前面的说法,如果块设备即存储介质不会变化,那么让内核支持这个块设备的驱动。这样的话,就可以直接挂载真实的根设备为rootfs。这是嵌入式设备的通常作法。此时作为Bootloader的u-boot的内核启动参数一般为bootargs=console=ttyS0,115200 root=/dev/mtdblock3 rw。此时,“initrd=”亦无须指定。比如:
点击(此处)折叠或打开
- ~ # cat /proc/cmdline
- noinitrd rw console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom ehci-hcd.park=3 lpm_levels.sleep_disabled=1 earlycon=msm_hsl_uart,0x78b0000 rootfstype=ubifs rootflags=bulk_read root=ubi0:rootfs
6. 翩翩起舞的胖子
initramfs的实现,重用了内核cache部分的代码。实际上,initramfs是cache思想用在tmpfs之后的又一次新发展。所以跟tmpfs类似,这里initramfs也有最大size的限制。
综而观之,initramfs体现了内核的一贯思想和发展历程,那就是,尽量将内核或启动脚本做的事推迟到用户空间,以加快内核启动速度。这样可以让系统启动更快,同时让系统的伸缩性和可配置性得到更大增强。这就让发展越来越重的Linux系统,成长为一个灵活的胖子。而initramfs,就是让那个胖子翩翩起舞的指挥家。
▲
很投入吧~
内核底层做的事,如果在用户空间也可以完成得很好,而且依环境不同,可以经常变化,那就尽量将它推向用户空间。关键在于,隐秘的底层只处理极度抽象化的框架性工作,不断沉淀,而透明的上层则完成千变万化的策略性事务,体现了变与不变思想的动态调整性。这在诸多系统设计中都有所体现,比如Linux系统的udev架构,比如通信3GPP的IMS架构。这个系统设计原则,我们可以称之为:【框架沉淀,策略透明】。
这,才是能让那个胖子翩翩起舞的灵魂。
7. 后续问题
留下两个问题,供有兴趣的同学们继续思考:)
a. kernel image一般放在/boot目录,那么Bootloader是如何找到并加载kernel镜像呢?
b. initial fs image也放在/boot目录,那么系统如何找到这个目录的?/boot目录创建时可以设置成逻辑卷吗?
=====
此文于2017年完成,2019年首发在我的Linux微信公众号“Linux编程之美”(朋友们有什么更好的名字可以推荐给我哈~)上,今天网上查阅时发现有家网站转载后排版实在看不下去,遂将原文搬上chinaunix blog(这两年忘记密码了@_@),顺便吐槽一下最后那张配图,这选择得有多大的勇气啊~~。原文链接:
https://mp.weixin.qq.com/s?__biz=MzU5MDQ2NjIzMA==&mid=2247483674&idx=1&sn=68394ae928f6ae1dfe79fbc9c3407380&chksm=fe3c9cc3c94b15d57913f40a6ca8c1d03879e043f63a3b651b950bd4a34e4a77db7582aa7332&mpshare=1&scene=1&srcid=0812iERD1fvsW8utSDyx6R1G&sharer_sharetime=1597240102889&sharer_shareid=d31ffd07942ef99321e1347a264e41a5&key=28b32a5b213a4e2ff92e1bdbe784291e7eb45454d9d10553a4f08ac8f57e7dec7ae04c9b59494d05a27a47b83096d69e087789f3156ac4df57d08df00bd2e4c68aa91e1e02a42802a69612cf9f10d7ba&ascene=1&uin=MTM1NjU5NDU0Mg%3D%3D&devicetype=Windows+7+x64&version=62090529&lang=zh_CN&exportkey=A3kIbKicaFVYOhAicbw2rco%3D&pass_ticket=aZJtkhpxhfx1btRmKa0squOZiaiNOEqCSxX%2F6FItWAt2w97HCcWp45IRAiPaAJdT