Linux内核启动过程分析

大家好!

Leonid在准备我们的Linux Administrator课程中的第一 公开课时 ,我们继续谈论如何加载Linux内核。

走吧

了解系统如何正常运行-准备修复不可避免的故障

开放源代码字段中最古老的笑话是“代码记录了自身”的声明。 经验表明,阅读源代码就像在听天气预报:聪明的人仍会走到外面看天空。 以下是使用熟悉的调试工具检查和检查Linux系统引导的提示。 对运行良好的系统的启动过程进行分析,可以为用户和开发人员解决不可避免的崩溃做好准备。

一方面,下载过程非常简单。 操作系统的内核(内核)在一个内核(内核)上单线程并同步运行,即使对于可悲的人类而言,这似乎也是可以理解的。 但是,操作系统的内核如何启动? initrd( 用于初始初始化的RAM磁盘 )和引导程序有什么功能? 等等,为什么以太网端口上的LED一直亮着?



继续阅读以获得这些和其他一些问题的答案; GitHub上也提供了用于演示和练习的代码。

启动开始:状态为OFF

局域网唤醒

状态为OFF表示系统没有电源,对吗? 明显的简单性正在欺骗。 例如,即使在此状态下,以太网LED也会亮起,这是因为系统中的LAN唤醒功能(WOL,从本地网络[信号]唤醒)已打开。 通过写确保:

$# sudo ethtool <interface name> 

例如,它可以是eth0(ethtool在Linux软件包中具有相同的名称)。 如果输出中的“ Wake-on”显示为g,则远程主机可以通过发送MagicPacket引导系统。 如果您不想自己远程打开系统并将此机会提供给其他人,请在系统BIOS菜单中禁用WOL,或使用:

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

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

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

BMC并不是唯一可以“监听”名义上关闭的系统的微控制器(MCU)。 X86_64系统具有用于远程系统管理的英特尔管理引擎(IME)软件包。 从服务器到笔记本电脑,各种各样的设备都具有具有 KVM远程控制或Intel功能许可服务等功能的技术。 根据Inte l 自己的工具IME具有未修补的漏洞。 坏消息是,禁用IME很难。 Trammell Hudson创建了me_cleaner项目,该项目删除了一些最糟糕的 IME组件,例如嵌入式Web服务器,但是与此同时,使用该项目有可能使正在运行的系统变成一个砖块。

IME固件和引导时紧随其后的系统管理模式(SMM)程序均基于Minix操作系统,并在单独的Platform Controller Hub处理器(而不是系统的主CPU)上运行。 然后,SMM在主处理器上启动通用可扩展固件接口(UEFI)程序,该程序已编写了不止一次 。 Coreboot小组在Google发起了一个雄心勃勃的雄心勃勃的“ 不可扩展的简化固件(NERF)”项目,该项目旨在不仅替换UEFI,而且替换Linux用户空间的早期组件,例如systemd。 在此期间,我们正在等待结果,Linux用户可以从禁用了IME的 Purism,System76或Dell购买笔记本电脑,此外,我们还希望带有64位ARM处理器的笔记本电脑。

装载机

可启动固件除了启动可疑间谍软件外还做什么? 引导加载程序的任务是为刚打开的处理器提供必要的资源,以运行诸如Linux之类的通用操作系统。 在加电期间,在提升控制器之前,不仅会有虚拟内存,而且还有DRAM。 引导加载程序然后打开电源,并扫描总线和接口以查找内核映像和根文件系统。 流行的引导加载程序(例如U-Boot和GRUB)既支持USB,PCI和NFS等通用接口,也支持其他更专门的嵌入式设备(例如NOR和NAND闪存)。 加载程序还与安全硬件设备(例如, 受信任的平台模块(TPM))进行交互,以从下载开始就建立信任链。


在构建服务器上的沙箱中运行U-boot加载器。

从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来恢复“砖”沙箱。

内核启动

引导内核供应

任务完成后,引导程序将切换到它加载到主存储器中的内核代码,并开始执行它,并传递用户指定的所有命令行参数。 内核是什么程序? 文件/ 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用户空间程序。 这意味着我们可以使用像readelf这样的binutils命令来学习它。 例如,比较以下结论:

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

二进制文件中的分区列表在大多数情况下是相似的。

因此,内核应启动其他ELF Linux二进制文件...但是用户空间程序如何运行? 在main()函数中,对吗? 不完全是

在运行main()函数之前,程序需要一个执行上下文,包括堆(堆)和堆栈(栈)内存,以及stdiostdoutstderr文件描述符。 用户空间程序从标准库(对于大多数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()为二进制文件提供所有必需的资源,该函数在glibc源代码包中可用,可以通过GDB来学习。 显然,内核没有解释器,它应该独立提供自身,但是如何?

对使用GDB启动内核的研究为该问题提供了答案。 首先,请安装内核调试软件包,其中包含vmlinux的未切割版本,例如apt-get install linux-image-amd64-dbg 。 或者,例如,按照出色的Debian Kernel Handbook中的说明,从某些来源编译并安装自己的内核。 gdb vmlinux后面的info files显示ELF部分init.text 。 用l *(address)init.text指示程序执行的开始,其中address是init.text的十六进制开始。 GDB将指示在arch/x86/kernel/head_64.S启动了x86_64内核,在该arch/x86/kernel/head_64.S中我们找到了构建函数start_cpu0()以及在调用x86_64 start_kernel()之前显式创建堆栈并解压缩zImage的x86_64 start_kernel() 。 32位ARM内核具有类似的arch/arm/kernel/head.S. start_kernel() arch/arm/kernel/head.S. start_kernel()与体系结构无关,因此该函数位于内核init/main.c 我们可以说start_kernel()是真正的main() Linux函数。

从start_kernel()到PID 1
内核硬件清单:ACPI表和设备树

引导时,内核除了需要为其编译的处理器类型之外,还需要有关硬件的信息。 代码中的指令由配置数据补充,配置数据分别存储。 有两种主要的数据存储方法:设备ACPI表 。 内核从这些文件中找出每次引导都需要运行哪些设备。

对于嵌入式设备,设备树(DU)是已安装设备的清单。 DU是一个与内核源代码同时编译的文件,通常与vmlinux一起位于/ boot中。 要查看ARM设备上的二进制设备树中的内容,只需使用名称与/boot/*.dtb对应的文件中binutils包中的strings命令,因为dtb表示设备树的二进制文件(Device-Tree Binary)。 您可以通过编辑远程控制组成的类似于JSON的文件并重新启动内核源代码随附的特殊dtc编译器来更改远程控制。 DU是一个静态文件,其路径通常由引导加载程序在命令行上传递给内核,但是近年来已添加了设备树覆盖 ,内核可以在其中动态加载其他片段,以响应加载后的热插拔事件。

x86系列和许多ARM64商业级设备使用备用的高级配置和电源接口( ACPI)机制。 与远程控制不同,ACPI信息存储在虚拟文件系统/sys/firmware/acpi/tables ,该文件系统是内核在启动时通过访问内部ROM创建的。 要读取ACPI表,请使用acpica-tools软件包中的acpidump命令。 这是一个例子:


联想笔记本电脑上的ACPI表已准备好用于Windows 2001。

是的,如果要安装Windows 2001,则可以使用Linux系统。 与远程控制相反,ACPI同时具有方法和数据,后者更像是一种硬件描述语言。 引导后,ACPI方法继续处于活动状态。 例如,如果运行acpi_listen命令(从apcid程序包中),然后合上并打开笔记本电脑的盒盖,您将看到ACPI功能一直保持工作状态。 可以临时和动态重写ACPI表 ,但是永久性更改将需要在引导或刷新ROM时与BIOS菜单进行交互。 除了如此复杂之外,也许您应该只安装coreboot ,这是开源固件的替代品。

从start_kernel()到用户空间

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_debug添加到内核命令行来监视initcalls的操作。 结果,每次运行initcall函数时,您将获得dmesg实体。 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中的代码几乎完成了执行。 引导处理器完成了大部分的一次性初始化,而其他内核则不需要重复执行。 但是,必须为每个内核创建线程,以便控制每个内核上的中断(IRQ),工作队列,计时器和电源事件。 例如,使用ps -o psr. psr命令查看可处理softirq和工作队列的处理器线程ps -o psr.

 $\# 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?

除了设备树之外,文件的另一个初始化路径(由内核在启动时提供)是initrdinitrd通常与x86上的bzImage vmlinuz文件一起位于/ boot中,或者与ARM的uImage和设备树相似。 可以使用lsinitramfs工具查看intrd内容列表,该工具是initramfs-tools-core软件包的一部分。 initrd分发映像包含最小目录/bin/sbin/etc ,以及/scripts内核模块和文件。 一切都应该或多或少看起来很熟悉,因为initrd在很大程度上类似于简化的Linux根文件系统。 这种相似性有点令人误解,因为ramdisk中/bin/sbin几乎所有可执行文件都是到BusyBox二进制文件的符号链接,这使/ bin和/ sbin目录比glibc目录小10倍。

如果仅通过加载一些模块并在常规的根文件系统上运行initrd为什么initrd尝试创建initrd ? 考虑一个加密的根文件系统。 解密可能取决于加载存储在根文件系统的/lib/modules的内核模块,并按预期加载initrd 。 可以将crypto模块静态编译到内核中,而不是从文件中加载,但是有很多理由拒绝这样做。 例如,带有模块的内核的静态编译可能使它太大而无法容纳可用的存储,或者静态编译可能违反软件许可条款。 毫不奇怪,存储驱动程序,网络和HID(人工输入设备)也可以initrd表示-基本上,不是挂载根文件系统所需的内核必需部分的任何代码。 同样在initrd中,用户可以为table存储自己的ACPI代码


与救援壳和自定义initrd一起玩。

initrd对于测试文件系统和存储设备也非常initrd 。 将测试工具放在initrd ,然后从内存而不是测试对象运行测试。

最后,当init运行时,系统正在运行! 由于辅助处理器已经在运行,因此该机器已成为我们都知道并喜欢的异步,分页,不可预测的高性能生物。 实际上, ps -o pid,psr,comm -p指示用户空间init进程不再在引导处理器上运行。

总结

考虑到受影响的软件数量,即使在简单的嵌入式设备上,Linux引导过程听起来也是被禁​​止的。 另一方面,引导过程非常简单,因为不会因为挤出多任务,RCU和竞争条件而导致过多的复杂性。 仅关注内核和PID 1,就可以忽略引导加载程序和辅助处理器为内核启动准备平台所做的巨大工作。 内核当然不同于其他Linux程序,但是使用工具与其他ELF二进制文件一起使用将有助于更好地了解其结构。 研究可行的启动过程将为将来的崩溃做准备。

结束

和往常一样,我们正在这里或在公开课程中等待列昂尼德被吹走的情况下,等待您的评论和问题。

Source: https://habr.com/ru/post/zh-CN425505/


All Articles