Analyzing the Linux boot process-分析Linux启动过程

本文翻译自Analyzing the Linux boot process.

箴言:了解运行良好的系统是将来处理不可避免的故障的很好准备

image

开源软件领域中流行的最为古老笑话:"the code is selfdocumenting."经验表明:阅读源码类似于收听天气预报(但明智的人仍会走出去检查天气)。下面是一些关于如何利用熟悉的调试工具在引导时检查和观察Linux系统的技巧。了解运行良好的系统是为将来处理不可避免的故障的很好准备。

在某些方面,内核的启动过程是十分简单的。内核在单个内核上以单线程和同步的方式启动,对于可怜的人类来说,这几乎是可以理解的。但是,内核是如何自举的?initrd(初始ramdisk)和引导加载程序执行哪些功能?等等,为什么以太网端口上的LED始终亮着?

继续阅读以获取这些问题和其他问题的答案;所描述的演示和练习的代码也可以在GitHub上获得。

启动的开始:网卡“关闭”状态

网络唤醒

网关的状态灯OFF是意味着系统没有加电,对吧?表面上的简单往往具有欺骗性。例如,以太网指示灯亮着可能是因为你的系统开启着wake-on-LAN(WOL)功能。输入以下内容检查是否是这种情况:

$# sudo ethtool <interface name>

可能是,例如,“eth0”。(ethtool可在同名的Linux软件包中找到)。如果输出中显示“Wake-on”的状态为g,那么远程主机可以通过MagicPacket来引导系统。如果您无意远程唤醒系统并且不希望其他人这样做,请在系统BIOS菜单中关闭WOL,或者通过以下方式关闭WOL:

$# sudo ethtool -s <interface name> wol d

响应MagicPacket的处理器可以是网络接口的一部分,也可以是底板管理控制器(BMC)。

英特尔管理引擎,平台控制器中心和Minix

当系统名义上关闭时,BMC不是唯一可以监听的微控制器(MCU)。x86_64系统还包括用于远程管理系统的英特尔管理引擎(Intel Management Engine IME) IME)软件套件。从服务器到笔记本电脑的各种设备都包含此技术,可实现 KVM远程控制和英特尔功能许可服务等功能。根据英特尔自己的检测工具,IME尚未修补漏洞。坏消息是,很难禁用IME。Trammell Hudson创建了一个me_cleaner项目,它可以擦除一些更糟糕的IME组件,比如嵌入式web服务器,但是也可以破坏运行它的系统。IME固件和系统管理模式(SMM)软件在引导时都是基于Minix操作系统,运行在独立的平台控制器集线器处理器上,而不是主系统CPU上。然后SMM在主处理器上启动通用可扩展固件接口(Universal Extensible Firmware Interface, UEFI)软件,关于该软件已经编写了很多内容。谷歌的Coreboot小组已经启动了一个雄心勃勃的非可扩展简化固件(NERF)项目,其目标不仅是替换UEFI,而且还替换早期的Linux用户空间组件,如systemd。在我们等待这些新努力的结果时,Linux用户现在可以从Purism、System76或禁用IME的Dell购买笔记本电脑,另外我们还可以希望购买ARM 64位处理器的笔记本电脑。

BootLoaders

除了启动有bug的间谍软件,早期引导固件还有什么功能?引导装载程序的工作是为新支持的处理器提供运行Linux等通用操作系统所需的资源。在开机时,不仅没有虚拟内存,而且在控制器启动之前也没有DRAM。然后,引导装载程序打开电源并扫描总线和接口,以定位内核映像和根文件系统。流行的引导加载程序,如U-Boot和GRUB,支持熟悉的接口,如USB、PCI和NFS,以及更嵌入式的特定设备,如NOR和NAND-flash。引导加载程序还与硬件安全设备(如受信任平台模块(Trusted Platform Modules, TPMs))交互,从最早的引导开始建立信任链。

image

在构建主机的沙箱中运行U-boot引导加载程序。

这个开源的、广泛使用的U-Boot引导加载程序支持各种系统,从树莓派到任天堂设备,从汽车板到chromebook。没有syslog,当事情偏离正题时,通常甚至没有任何控制台输出。为了方便调试,U-Boot团队提供了一个沙箱,可以在构建主机上测试补丁,甚至可以在夜间持续集成系统中测试补丁。在安装了Git和GNU Compiler Collection (GCC)等常用开发工具的系统上,使用U-Boot沙箱相对简单:

$# git clone git://git.denx.de/u-boot; cd u-boot
$# make ARCH=sandbox defconfig
$# make; ./u-boot
=> printenv
=> help

就是这样:您在x86_64上运行U-Boot,可以测试一些复杂的特性,如模拟存储设备(mock storage device)重新分区、基于tpm的密钥操作和USB设备的热插拔。U-Boot沙箱甚至可以在GDB调试器下单步执行。使用沙箱进行开发要比将引导加载程序重新加载到一块板上进行测试快10倍,并且可以使用Ctrl+C恢复“bricked”沙箱。

启动内核

提供一个引导内核

在完成任务后,BootLoader将执行一个跳转到已加载到主内存中的内核代码的操作,并开始执行,传递用户指定的任何命令行选项。内核是什么样的程序?文件/boot/vmlinuz表明它是一个bzImage,意思是一个大的压缩的bzImage。Linux源树包含一个 extract-vmlinux tool工具,可用于解压文件:

$# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux
$# file vmlinux 
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically 
linked, stripped

内核是一种Executable and Linking Format(ELF)二进制文件,就像Linux用户空间程序一样。这意味着我们可以使用来自binutils包(如readelf)的命令来检查它。例如,比较…的输出:

$# readelf -S /bin/date
$# readelf -S vmlinux

二进制文件中的节列表基本相同。

所以内核必须启动类似于其他Linux ELF二进制文件的东西……但是用户空间程序实际上是如何启动的呢?在main()函数中,对吧?并不完全准确。

在main()函数运行之前,程序需要一个执行上下文,其中包括堆和堆栈内存以及stdio、stdout和stderr的文件描述符。用户空间程序从标准库获得这些资源,标准库是大多数Linux系统上的glibc。例如:

$# file /bin/date 
/bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically 
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, 
BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,
stripped

ELF二进制文件有一个解释器,就像Bash和Python脚本一样,但是解释器不需要用#!在脚本中,ELF是Linux的本机格式。ELF解释器通过调用_start()提供一个包含所需资源的二进制文件,_start()是glibc源包中提供的一个函数,可以通过GDB进行检查。内核显然没有解释器,必须自己提供,但是如何提供呢?

用GDB检查内核的启动会给出答案。首先安装包含未剥离的vmlinux版本的内核的调试包,例如apt-get install linux-image-amd64-dbg,或者根据Debian Kernel Handbook中的说明,从源代码编译和安装自己的内核。执行gdb vmlinux,然后执行info files显示init.text分区信息。在init.text中列出程序执行的开始,带有l *(address)的文本,其中address是init.text的十六进制开头。GDB将指示x86_64内核在内核文件arch/x86/kernel/head_64.S中启动,在这里,我们找到了汇编函数start_cpu0(),以及在调用x86_64 start_kernel()函数之前显式创建堆栈并解压缩zImage的代码。ARM 32位内核具有类似的arch/ARM /kernel/head.S。start_kernel()不是特定于体系结构的,因此位于内核的init/main.c函数中的start_kernel()可以说是Linux真正的main()函数。

从start_kernel()到PID 1

内核的硬件清单:设备树和ACPI表

在引导时,内核需要有关已编译的处理器类型之外的硬件的信息。代码中的指令由单独存储的配置数据进行扩充。存储这些数据有两种主要方法:设备树和ACPI表。内核通过读取这些文件来了解每次引导时必须运行哪些硬件。

对于嵌入式设备,设备树是已安装硬件的清单。设备树只是一个与内核源代码同时编译的文件,通常与vmlinux一起位于/boot中。要查看ARM设备上的二进制设备树中的内容,只需对名称匹配/boot/*的文件使用binutils包中的strings命令即可。dtb是指一个设备树二进制文件。显然,可以通过编辑组成设备树的类似json的文件并重新运行内核源代码提供的特殊dtc编译器来修改设备树。虽然设备树是一个静态文件,其文件路径通常由命令行上的引导加载程序传递给内核,但是近年来添加了一个设备树覆盖工具,内核可以在引导后动态加载额外的片段以响应热插拔事件。

x86-family和许多企业级ARM64设备使用了替代的高级配置和电源接口(ACPI)机制。与设备树不同,ACPI信息存储在/sys/firmware/ ACPI /tables虚拟文件系统中,该文件系统是内核通过访问板载ROM在引导时创建的。读取ACPI表的简单方法是使用acpica-tools包中的acpidump命令。这里有一个例子:

联想笔记本电脑上的ACPI表都是为Windows 2001设置的

image

是的,如果您愿意安装的话,您的Linux系统已经为Windows 2001做好了准备。ACPI同时具有方法和数据,这与设备树不同,后者更像是一种硬件描述语言。ACPI方法在引导后仍然是活动的。例如,启动命令acpi_listen(来自包apcid)并打开和关闭笔记本电脑的盖子将显示ACPI功能一直在运行。虽然临时和动态地覆盖ACPI表是可能的,但是永久地更改它们涉及到在引导时与BIOS菜单进行交互或对ROM进行反流。如果您遇到这么多麻烦,也许您应该安装coreboot,这是一种开源固件替代品。

从start_kernel()到用户空间

init/main.c中的代码可读性惊人,而且有趣的是,它仍然保留着Linus Torvalds 1991-1992年的原始版权。在新引导系统上的dmesg |head中发现的行主要来自这个源文件。第一个CPU注册到系统中,初始化全局数据结构,调度程序、中断处理程序(IRQs)、计时器和控制台按严格的顺序逐一联机。在函数timekeeping_init()运行之前,所有时间戳都为零。内核初始化的这一部分是同步的,这意味着执行只在一个线程中进行,在最后一个线程完成并返回之前不会执行任何函数。因此,即使在两个系统之间,只要它们具有相同的设备树或ACPI表,dmesg输出也是完全可复制的。Linux的行为类似于运行在MCUs上的RTOS(实时操作系统),例如QNX或VxWorks。这种情况将持久化到函数rest_init()中,该函数在终止时由start_kernel()调用。

早期内核引导过程的总结
image

名称很不起眼的rest_init()生成一个运行kernel_init()的新线程,该线程调用do_initcalls()。用户可以通过在内核命令行中添加initcall_debug来监视initcall的运行,从而在每次运行initcall函数时生成dmesg条目。initcall通过七个顺序级别:early、core、postcore、arch、subsys、fs、device和late。initcalls中用户最可见的部分是探测和设置所有处理器的外围设备:buses, network, storage, displays等等,并加载它们的内核模块。rest_init()还在引导处理器上生成第二个线程,该线程在等待调度程序分配工作时首先运行cpu_idle()。kernel_init()还设置了对称多处理(SMP)。对于最新的内核,可以在demsg的输出中查找"“Bringing up secondary CPUs…”"SMP通过“hotpluging”cpu进行,这意味着它使用一种状态机来管理它们的生命周期,这种状态机在理论上类似于热插拔USB等设备的生命周期。内核的电源管理系统经常将单个内核脱机,然后根据需要唤醒它们,以便在不繁忙的机器上反复调用相同的CPU热插拔代码。使用名为offcputime.py的BCC工具观察电源管理系统对CPU热插拔的调用。

注意,当smp_init()运行时,init/main.c中的代码几乎已经完成执行:引导处理器已经完成了大多数其他内核不需要重复的一次性初始化。尽管如此,必须为每个内核派生每个cpu线程,以管理每个内核上的中断(irq)、工作队列、计时器和电源事件。例如,可以通过ps -o psr命令查看为软中断和工作队列提供服务的每个cpu线程。

$\# ps -o pid,psr,comm $(pgrep ksoftirqd)  
 PID PSR COMMAND 
   7   0 ksoftirqd/0 
  16   1 ksoftirqd/1 
  22   2 ksoftirqd/2 
  28   3 ksoftirqd/3 

$\# ps -o pid,psr,comm $(pgrep kworker)
PID  PSR COMMAND 
   4   0 kworker/0:0H 
  18   1 kworker/1:0H 
  24   2 kworker/2:0H 
  30   3 kworker/3:0H
[ . .  . ]

其中PSR字段代表“处理器”。每个核心还必须承载自己的计时器和cpuhp热插拔处理程序。

用户空间是如何开始的呢?在它的末尾,kernel_init()寻找一个可以代表它执行init进程的initrd。如果没有找到,内核直接执行init本身。那么,为什么可能需要一个initrd呢?

早期用户空间:谁订购了initrd?

除了设备树之外,在引导时可选地提供给内核的另一个文件路径是initrd。initrd通常与x86上的bzImage文件vmlinuz一起位于/boot中,或者对于ARM来说其与uImage和设备树在一起。使用lsinitramfs工具(initramfs-tools-core包的一部分)列出initrd的内容。发行版initrd方案包含最小/bin、/sbin和/etc目录以及内核模块,以及/scripts中的一些文件。所有这些看起来都很熟悉,因为initrd在很大程度上只是一个最小的Linux根文件系统。这种明显的相似性有点欺骗性,因为ramdisk中/bin和/sbin中的几乎所有可执行文件都是BusyBox binary件的符号链接,导致/bin和/sbin目录比glibc目录小10倍。

如果initrd所做的只是加载一些模块,然后在常规的根文件系统上启动init,那么为什么还要创建initrd呢?考虑加密的根文件系统。解密可能依赖于加载存储在根文件系统/lib/modules中的内核模块……不出所料,在initrd中也是如此。可以将crypto模块静态编译到内核中,而不是从文件中加载,但是有很多原因不希望这样做。例如,使用模块静态编译内核可能会使内核太大而无法装入可用的存储,或者静态编译可能违反软件许可证的条款。不出所料,存储、网络和人工输入设备(human input device, HID)驱动程序也可能出现在initrd中——基本上是挂载根文件系统所需的内核之外的任何代码。initrd也是用户可以存放自己的自定义ACPI表代码的地方。

image

initrd对于测试文件系统和数据存储设备本身也非常有用。将这些测试工具保存在initrd中,并从内存中而不是从被测试的对象中运行测试。

最后,当init运行时,系统就启动了!由于二级处理器现在正在运行,机器已经变成了异步的、可抢占的、不可预测的、高性能的生物,我们知道并喜爱它。实际上,ps -o pid、psr、comm - p1很容易显示用户空间的init进程不再在引导处理器上运行。

总结

考虑到即使在简单的嵌入式设备上也有许多不同的软件参与,Linux引导过程听起来令人生畏。从另一个角度看,引导过程相当简单,因为在引导中不存在抢占、RCU和竞态条件等特性所导致的令人困惑的复杂性。只关注内核和PID 1忽略了引导加载程序和辅助处理器在为内核运行准备平台时可能要做的大量工作。虽然内核在Linux程序中当然是独一无二的,但是可以通过将一些用于检查其他ELF二进制文件的相同工具应用到内核中来了解其结构。学习内核的正常启动过程可以为系统出现故障时做准备。

要了解更多信息,请参加Alison Chaiken的演讲:Linux: The first second

感谢Akkana Peck最初提出这个主题并进行了许多修正。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值