linux启动过程分析
开源软件中最古老的笑话是“该代码是自记录文档”的说法。 经验表明,阅读源代码类似于收听天气预报:明智的人仍在外面看天空。 接下来的内容是有关如何利用熟悉的调试工具的知识在启动时检查和观察Linux系统的一些技巧。 分析运行良好的系统的引导过程,使用户和开发人员可以应对不可避免的故障。
initrd
( 初始ramdisk )和引导加载程序执行吗?
等等,为什么以太网端口上的LED一直亮着?
继续阅读,以获取这些和其他问题的答案; GitHub上也提供了用于描述的演示和练习的代码 。
关闭状态表示系统没有电,对吗? 明显的简单性具有欺骗性。 例如,由于系统上启用了LAN唤醒(WOL),所以以太网LED点亮。 通过键入以下内容检查是否是这种情况:
$# sudo ethtool
其中,
可能是eth0
。 (在具有相同名称的Linux软件包中找到ethtool
。)如果输出中的“ Wake-on”显示g
,则远程主机可以通过发送MagicPacket引导系统。 如果您无意远程唤醒系统并且不希望其他人唤醒系统,请在系统BIOS菜单中或通过以下方式关闭WOL:
$# sudo ethtool -s wol d
响应MagicPacket的处理器可能是网络接口的一部分,也可能是基板管理控制器 (BMC)。
当系统正常关闭时,BMC并不是唯一可以监听的微控制器(MCU)。 x86_64系统还包括用于系统远程管理的英特尔管理引擎(IME)软件套件。 从服务器到笔记本电脑,各种各样的设备都包含此技术,该技术可启用诸如KVM远程控制和英特尔功能许可服务之类的功能。 根据英特尔自己的检测工具 , IME具有未修补的漏洞 。 坏消息是,很难禁用IME。 Trammell Hudson创建了一个me_cleaner项目 ,该项目擦除了一些更糟糕的 IME组件,例如嵌入式Web服务器,但也可能使运行该系统的系统变砖。
IME固件和引导时紧随其后的系统管理模式(SMM)软件基于Minix操作系统,并在单独的Platform Controller Hub处理器而不是主系统CPU上运行。 然后,SMM在主处理器上启动通用可扩展固件接口(UEFI)软件,该软件已经编写了很多东西。 Google的Coreboot小组已经启动了一个雄心勃勃的雄心勃勃的不可扩展精简固件 (NERF)项目,该项目旨在不仅替换UEFI,而且替换早期的Linux用户空间组件(例如systemd)。 在等待这些新成果的结果的同时,Linux用户现在可以从禁用了IME的 Purism,System76或Dell购买笔记本电脑,此外,我们也希望使用ARM 64位处理器的笔记本电脑。
除了启动错误的间谍软件外,早期启动固件还提供什么功能? 引导加载程序的工作是为新供电的处理器提供运行通用操作系统(如Linux)所需的资源。 开机时,不仅没有虚拟内存,而且在启动控制器之前也没有DRAM。 然后,引导加载程序会打开电源并扫描总线和接口,以便找到内核映像和根文件系统。 诸如U-Boot和GRUB之类的流行引导加载程序支持常见的接口(例如USB,PCI和NFS)以及更特定于嵌入式的设备(例如NOR和NAND闪存)。 引导加载程序还与可信平台模块 (TPM)等硬件安全设备进行交互,以从最早的引导建立信任链。
从Raspberry Pi到Nintendo设备再到汽车主板再到Chromebook,该系统均支持广泛使用的开源U-Boot引导加载程序。 没有系统日志,并且当事情横盘整理时,通常甚至没有任何控制台输出。 为了方便调试,U-Boot团队提供了一个沙箱,可以在构建主机上,甚至在夜间的持续集成系统中对补丁进行测试。 在安装了通用开发工具(如Git和GNU编译器集合(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,并且可以测试棘手的功能,例如模拟存储设备重新分区,基于TPM的密钥操作以及USB设备的热插拔。 U-Boot沙箱甚至可以在GDB调试器下单步执行。 通过使用沙箱进行开发,比通过将引导加载程序重新刷新到板上进行测试要快10倍,并且可以使用Ctrl + C恢复“砖状”沙箱。
任务完成后,引导程序将跳转到已加载到主存储器中的内核代码,并开始执行,同时传递用户指定的所有命令行选项。 内核是哪种程序? file /boot/vmlinuz
表明它是一个bzImage,表示很大的压缩文件。 Linux源代码树包含一个extract-vmlinux工具 ,可用于解压缩文件:
$# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux
$# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
linked, stripped
内核是可执行和链接格式 (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
就像Bash和Python脚本一样,ELF二进制文件也具有解释器,但是不需要使用#!
来指定解释器#!
与脚本中一样,因为ELF是Linux的本机格式。 ELF解释器通过调用_start()
提供具有所需资源的二进制文件 , _start()
是glibc
源包中可用的函数,可以通过GDB对其进行检查 。 内核显然没有解释器,必须提供自己,但是如何?
通过GDB检查内核的启动即可找到答案。 首先为包含未剥离版本的vmlinux
的内核安装调试软件包,例如apt-get install linux-image-amd64-dbg
,或者从源代码编译并安装自己的内核,例如,按照出色的Debian中的说明进行操作内核手册 。 gdb vmlinux
后面的info files
显示ELF部分init.text
。 用l *(address)
在init.text
列出程序执行的开始,其中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()
函数。
引导时,内核需要有关已为其编译的处理器类型之外的硬件的信息。 代码中的指令由单独存储的配置数据扩展。 存储该数据的主要方法有两种: 设备树和ACPI表 。 内核通过读取这些文件来了解每次引导时必须运行的硬件。
对于嵌入式设备,设备树是已安装硬件的清单。 设备树只是与内核源同时编译的文件,通常与vmlinux
一起位于/boot
。 要查看ARM设备上的二进制设备树中的内容,只需在名称与/boot/*.dtb
匹配的文件上使用binutils
软件包中的strings
命令,因为dtb
指的是设备树二进制文件。 显然,可以通过编辑组成它的类似于JSON的文件并重新运行内核源代码随附的特殊dtc
编译器来修改设备树。 虽然设备树是静态文件,其文件路径通常由引导加载程序在命令行上传递到内核,但近年来已添加了设备树覆盖功能,内核可根据需要动态加载其他片段。引导后发生热插拔事件。
x86系列和许多企业级ARM64设备都使用替代的高级配置和电源接口( ACPI )机制。 与设备树相反,ACPI信息存储在/sys/firmware/acpi/tables
虚拟文件系统中,该文件系统由内核在启动时通过访问板载ROM创建。 读取ACPI表的简单方法是使用acpica-tools
软件包中的acpidump
命令。 这是一个例子:
是的,您的Linux系统已准备就绪,可用于Windows 2001。 与设备树不同,ACPI同时具有方法和数据,而设备树更像是一种硬件描述语言。 引导后,ACPI方法继续处于活动状态。 例如,启动命令acpi_listen
(来自apcid
包)并打开和关闭笔记本电脑盖将显示ACPI功能始终在运行。 尽管可以临时动态覆盖ACPI表 ,但永久更改它们涉及在启动时与BIOS菜单进行交互或重新刷新ROM。 如果您遇到了那么多麻烦,也许您应该只安装coreboot ,这是开源固件替换。
init / main.c中的代码令人惊讶地可读,并且有趣的是,仍然保留了1991-1992年Linus Torvalds的原始版权。 dmesg | head
找到的行dmesg | head
dmesg | head
一个新引导的系统上,从这个源文件来源于居多。 第一个CPU已向系统注册,初始化了全局数据结构,并且调度器,中断处理程序(IRQ),计时器和控制台以严格的顺序逐个联机。 在函数timekeeping_init()
运行之前,所有时间戳均为零。 内核初始化的这一部分是同步的,这意味着执行恰好在一个线程中进行,并且直到最后一个线程完成并返回后,才执行任何功能。 结果,即使两个系统之间具有相同的设备树或ACPI表, dmesg
输出也将是完全可重现的。 Linux的行为类似于运行在MCU(例如QNX或VxWorks)上的RTOS(实时操作系统)之一。 这种情况持续存在于函数rest_init()
,该函数在终止时由start_kernel()
调用。
谦逊的名字rest_init()
产生了一个运行kernel_init()
的新线程,该线程调用do_initcalls()
。 用户可以对窥探initcalls
行动通过附加initcall_debug
到内核命令行,导致dmesg
每一次输入的initcall
功能运行。 initcalls
经历七个连续级别:早期,核心,后核心,arch,subsys,fs,设备和晚期。 initcalls
最用户可见的部分是探测和设置所有处理器的外围设备:总线,网络,存储,显示等,并加载其内核模块。 rest_init()
还会在引导处理器上产生第二个线程,该线程首先运行cpu_idle()
同时等待调度程序为其分配工作。
kernel_init()
还会设置对称多处理 (SMP)。 对于较新的内核,通过查找“启动辅助CPU ...”在dmesg
输出中找到这一点,SMP通过“热插拔” CPU进行,这意味着它使用状态机来管理其生命周期,该状态机在概念上类似于诸如热插拔的USB棒。 内核的电源管理系统经常使单个内核脱机,然后根据需要唤醒它们,以便在不繁忙的计算机上一遍又一遍地调用相同的CPU热插拔代码。 使用名为offcputime.py
的BCC工具,观察电源管理系统对CPU热插拔的调用。
请注意,当smp_init()
运行时, init/main.c
中的代码几乎完成了执行:引导处理器已完成大多数一次初始化,而其他内核无需重复执行。 尽管如此,必须为每个内核产生每个CPU线程,以管理每个内核上的中断(IRQ),工作队列,计时器和电源事件。 例如,请参阅通过ps -o psr
命令运行中的softirq和工作队列的每个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()
寻找一个initrd
可以代表它执行init
进程。 如果找不到,内核将直接执行init
。 那为什么要一个initrd
呢?
除了设备树之外,引导时可选地提供给内核的另一个文件路径是initrd
。 initrd
通常与x86上的bzImage文件vmlinuz或ARM的类似uImage和设备树一起位于/boot
。 使用lsinitramfs
工具列出initrd
的内容,该工具是initramfs-tools-core
软件包的一部分。 Distro initrd
方案包含最小的/bin
, /sbin
和/etc
目录以及内核模块,以及/scripts
一些文件。 所有这些看起来都应该很熟悉,因为initrd
在很大程度上只是一个最小的Linux根文件系统。 表面上的相似性有点欺骗性,因为ramdisk中/bin
和/sbin
几乎所有可执行文件都是到BusyBox二进制文件的符号链接,导致/bin
和/sbin
目录比glibc的目录小10倍。
何苦创建initrd
,如果它是所有加载一些模块,然后开始init
在常规根文件系统? 考虑一个加密的根文件系统。 解密可能依赖于加载内核模块,该内核模块存储在根文件系统上的/lib/modules
,当然,也存储在initrd
中。 可以将加密模块静态编译到内核中,而不是从文件中加载,但是出于各种原因,我们不希望这样做。 例如,使用模块静态编译内核可能会使内核太大而无法容纳可用的存储,或者静态编译可能违反软件许可的条款。 毫不奇怪, initrd
也可能存在存储,网络和人工输入设备(HID)驱动程序-基本上,不是安装根文件系统所需的内核固有部分的任何代码。 initrd
还是用户可以存储自己的自定义ACPI表代码的地方。
initrd
对于测试文件系统和数据存储设备本身也非常有用。 将这些测试工具存放在initrd
,然后从内存而不是从被测对象运行测试。
最后,当init
运行时,系统启动! 由于辅助处理器现在正在运行,因此机器已成为我们认识和喜爱的异步,可抢占,不可预测的高性能生物。 实际上, ps -o pid,psr,comm -p 1
易于表明用户空间的init
进程不再在引导处理器上运行。
考虑到即使在简单的嵌入式设备上也要参与的不同软件的数量,Linux的启动过程听起来令人难以接受。 换个角度来看,引导过程非常简单,因为引导中不存在诸如抢占,RCU和竞争条件等功能所引起的令人困惑的复杂性。 仅关注内核和PID 1会忽略引导加载程序和辅助处理器在准备内核运行平台时可能要做的大量工作。 虽然内核在Linux程序中肯定是唯一的,但可以通过将一些与检查其他ELF二进制文件相同的工具应用于该内核,来了解其结构。 在启动过程正常进行的同时研究引导过程,以防止系统维护人员出现故障。
要了解更多信息,请参加1月22日至26日在悉尼举行的Alison Chaiken在Linux.conf.au上的演讲: Linux 第二篇 。
感谢Akkana Peck最初提出此主题并进行了许多更正。
翻译自: https://opensource.com/article/18/1/analyzing-linux-boot-process
linux启动过程分析