Chinaunix首页 | 论坛 | 博客
  • 博客访问: 311348
  • 博文数量: 22
  • 博客积分: 186
  • 博客等级: 入伍新兵
  • 技术积分: 1091
  • 用 户 组: 普通用户
  • 注册时间: 2012-01-12 19:54
文章存档

2022年(1)

2020年(1)

2018年(1)

2013年(10)

2012年(9)

分类: LINUX

2020-08-12 22:26:03

 
::欢迎转载,请注明出处和链接:)

如果你了解Linux系统,一定知道系统的启动参数中,有一个叫做“initrd=”的选项,再进一步,你也许听过关于initrdinitramfs的一些故事。如果你刚好是一位嵌入式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=(userspace entry)

接着,initrd中的启动脚本处理一些前期的准备工作,比如加载rootfs需要的驱动,最后一般会调用chroot工具转向最终的rootfsinitrd中的启动脚本也可以由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是这样的。      

点击(此处)折叠或打开

  1. if (!try_to_run_init_process("/sbin/init") ||
  2.            !try_to_run_init_process("/etc/init") ||
  3.            !try_to_run_init_process("/bin/init") ||
  4.            !try_to_run_init_process("/bin/sh"))
  5.               return 0;

进入最终的rootfs后,一般会经过/sbin/initinittabrc.system、一系列系统基础服务、rc.local的初始化过程。其中一系列系统基础服务的启动,从早期SysVinit的串行,到Upstart的部分并行,再到如今Systemd的完全并行,效率越来越高,系统启动越来越快。系统基础服务的这三种启动策略,分别在红帽的RHEL5RHEL6RHEL7发行版中实现。当然,对于各个Linux发行版来说,Base Services的启动过程和策略会有差异,比如新的Systemd服务已经把inittab给抛弃了。

小结一下,“initrd=”表示要加载的初始化实体initial fsrdinit=表示初始化实体initial fs中的启动脚本,init=表示系统要真正执行的第一个用户程序。后面两个现在使用不多。“initrd=”一般用在桌面版或服务器操作系统中,嵌入式系统中用得不多。
 

2. 鸡生蛋,还是蛋生鸡?

Linux系统的启动过程中,内核要想成功挂载根文件系统rootfs,首先必须能识别rootfs所在设备的物理类型,还要读懂rootfs的文件系统类型。所以,这就需要rootfs所在块设备驱动,需要相应的文件系统驱动,可能需要逻辑卷相关模块,还可能需要特殊的数据解密驱动。

这些驱动模块,如果放在rootfs中,就不能使用!因为承载rootfs的根文件系统设备还没就绪;如果放在内核image中,内核镜像可能会太臃肿,而且每增加支持一种新设备或文件系统,就要重新编译一次内核镜像,耦合太紧;再加上内核较严格的软件许可证制度,而很多设备厂商有一定的知识产权保护要求,这些驱动模块也许不方便放入内核镜像(另,Android采用了另外一种机制HAL规避知识产权冲突)

鸡生蛋,or 蛋生鸡


那么,怎么解决这个难题呢?

解决办法其实刚才在描述Linux系统的启动过程时已经给出来了,就是提供一个用户空间的初始化实体,即一个初始化最小文件系统(initial fs)

具体实现有两个办法,一是把这些驱动模块通通放进initial fs,二是在initial fs启动后,用U盘加载相应设备的二进制驱动。新的Dell服务器支持旧的操作系统,比如RHEL5.5时,由于旧系统没有对应的RAID驱动,采用的就是第二种办法,即发布RAID设备的相应版本二进制驱动的imageinitial fs启动后通过linux dd方式读取U盘镜像,从而加载RAID控制卡驱动,支持RAID磁盘。

在嵌入式Linux系统中,这个问题却并不存在!因为,嵌入式Linux系统普遍把rootfs部署在flash中。内核直接把块设备驱动和文件系统驱动编译进去,因为固定类型的flash,其文件系统也是固定的,比如nandflash这种块设备,一般都使用yaffsubi文件系统。内核只需要支持特定的块设备或文件系统,就可以很好的实现需求。

但在桌面和服务器Linux操作系统的发展中,存储的块设备一直在进化,其驱动当然也在变化,同时,操作系统支持的文件系统也越来越多。再把块设备驱动和文件系统驱动编进内核,会让内核越来越庞大。

所以还是需要一个初始化实体initial fs,于是initrd技术粉墨登场了。
 

3. initrd

Linux kernel在自身初始化完成之后,需要能够找到并运行第一个用户程序(这个初始化脚本或程序通常叫做“init”)。用户程序init存在于文件系统之中,因此,内核必须找到并挂载一个文件系统,才可以成功完成系统的引导过程。

随着硬件的发展,很多情况下这个文件系统也许是存放在USB设备、SCSI设备、RAID设备等等多种多样的设备之上,如果需要正确引导,USBSCSI或者RAID驱动模块首先需要运行起来,可是不巧的是,这些块设备驱动程序也是存放在文件系统里,这时候就形成了一个悖论般的死循环,也就是前面说过的鸡生蛋or蛋生鸡问题。

为解决此问题,Linux kernel提出了一个RAM Disk的解决方案,把一些启动所必须的用户程序和驱动模块放在RAM Disk中,这个RAM Disk看上去和普通的Disk一样,有文件系统、有cache,就差真实的Disk设备驱动了(因为是在RAM中模拟Disk设备,所以不需要真实的Disk设备驱动)。内核启动时,首先把RAM Disk挂载起来,等到初始化程序和一些必要模块运行起来之后,再切到真正的根文件系统之中。

这里RAM Disk的方案实际上就是initrd!其中rdRAM 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虽然解决了问题但并不完美。比如,Diskcache机制,对于RAM Disk来说,这个cache机制就显得很多余且浪费空间;Disk需要文件系统,那文件系统(如ext3等)必须被编译进kernel而不能作为模块来使用。

于是,Linux 2.6 kernel提出了一种新的实现机制,即initramfs技术。

好,下面继续聊一下initrd的升级版,即今天的主角——initramfs

4. initramfs

顾名思义,initramfs是一种RAM filesystem而不是RAM Diskinitramfs实际是一个cpio压缩包,启动所需的用户程序和用于读取rootfs的驱动模块,通通被打包成了一个文件。因此,加载initramfs不需要cache,也不需要文件系统驱动。它只是一个最小文件系统的目录而已。


原来initrdinitramfs是两种东西。。。

如果说initrd实现了第一次飞跃,脱离了物理意义上的根设备,initrd镜像挂载在内存模拟的块设备上,那么相比initrdinitramfs更进一步,完全没有了根设备Diskinitramfs直接解压并加载在内存中,这样就运行更快。而且initramfs更加轻量化,系统启动更快,更多的处理放在用户的启动脚本里。比如把存储用户数据的块设备分区挂载到某个子目录中去,如/dev/sda2挂载到/tftpboot用作用户程序,/dev/sda3挂载到/data用作存储用户数据库的内容。当然,最重要的根设备(由root=指定),存放着最终的rootfs,会首先通过chroot跳转并被挂载到根目录“/”上!

有时候,启动参数中也可以不指定root=。无需切到某个硬盘分区的rootfs上,基础文件系统basefs驻留在内存,不必把一些系统常用共享库经常换出内存,这样系统运行效率更高。这是一个很棒的主意!在一些要求颇高、硬件配置(关键是内存)也高的服务器中得到了应用,比如通信系统中的核心网服务器。在通信行业的核心网平台干了十来年的我,对此深有体会。

实际上,“initrd=”也可以不用指定,因为通过内核配置,可以在编译内核时,把initramfs跟内核镜像一起打包,内核镜像的size虽然有点偏大,但适合某些启动时只能接收一次镜像的Bootloader,比如国产LoongsonPMON


内核配置initramfskernel捆绑在一起

如上图所示。内核配置make menuconfig,选择General setup ---> Initial RAM filesystem and RAM disk(initramfs/initrd) support,选择 Initramfs source file(s),输入initramfs所在文件夹,比如当前子目录_initfs


原来initramfs可以做这么多文章。。。

让人无解的是,话说都升级成initramfs了,RHEL5RHEL6却还是给initial fs image起了一个类似initrd-2.6.18.img的名字,这无疑让世人误会更深。。。

这个问题直到RHEL7才解决了~~如下所示。      

点击(此处)折叠或打开

  1. linux16 /vmlinuz-3.10.0-693.el7.x86_64 root=/dev/mapper/centos-root ro
  2. initrd16 /initramfs-3.10.0-693.el7.x86_64.img

5 . 嵌入式系统的启动参数

在实际的嵌入式系统应用中,“initrd=”也可以不指定,即通过bootcmd=bootm kernel_addr rootfs_addrrootfs_addr来指定初始文件系统的位置。

当然嵌入式系统的启动参数,还有其他配置方法。

按照前面的说法,如果块设备即存储介质不会变化,那么让内核支持这个块设备的驱动。这样的话,就可以直接挂载真实的根设备为rootfs。这是嵌入式设备的通常作法。此时作为Bootloaderu-boot的内核启动参数一般为bootargs=console=ttyS0,115200  root=/dev/mtdblock3 rw。此时,“initrd=”亦无须指定。比如:      

点击(此处)折叠或打开

  1. ~ # cat /proc/cmdline
  2. 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部分的代码。实际上,initramfscache思想用在tmpfs之后的又一次新发展。所以跟tmpfs类似,这里initramfs也有最大size的限制。

综而观之,initramfs体现了内核的一贯思想和发展历程,那就是,尽量将内核或启动脚本做的事推迟到用户空间,以加快内核启动速度。这样可以让系统启动更快,同时让系统的伸缩性和可配置性得到更大增强。这就让发展越来越重的Linux系统,成长为一个灵活的胖子。而initramfs,就是让那个胖子翩翩起舞的指挥家。


很投入吧~

内核底层做的事,如果在用户空间也可以完成得很好,而且依环境不同,可以经常变化,那就尽量将它推向用户空间。关键在于,隐秘的底层只处理极度抽象化的框架性工作,不断沉淀,而透明的上层则完成千变万化的策略性事务,体现了变与不变思想的动态调整性。这在诸多系统设计中都有所体现,比如Linux系统的udev架构,比如通信3GPPIMS架构。这个系统设计原则,我们可以称之为:【框架沉淀,策略透明】。

这,才是能让那个胖子翩翩起舞的灵魂。

7. 后续问题

留下两个问题,供有兴趣的同学们继续思考:)

a. kernel image一般放在/boot目录,那么Bootloader是如何找到并加载kernel镜像呢?

b. initial fs image也放在/boot目录,那么系统如何找到这个目录的?/boot目录创建时可以设置成逻辑卷吗?

 
=====
此文于2017年完成,2019年首发在我的Linux微信公众号“Linux编程之美”(朋友们有什么更好的名字可以推荐给我哈~)上,今天网上查阅时发现有家网站转载后排版实在看不下去,遂将原文搬上chinaunix blog(这两年忘记密码了@_@),顺便吐槽一下最后那张配图,这选择得有多大的勇气啊~~。原文链接:
%3D%3D&devicetype=Windows+7+x64&version=62090529&lang=zh_CN&exportkey=A3kIbKicaFVYOhAicbw2rco%3D&pass_ticket=aZJtkhpxhfx1btRmKa0squOZiaiNOEqCSxX%2F6FItWAt2w97HCcWp45IRAiPaAJdT

阅读(5334) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~