Zircon 采用微内核设计风格。微内核设计的一个复杂性在于如何引导初始用户空间进程。通常,这可通过让内核仅出于引导目的实现文件系统读取和程序加载的最小版本来实现,即使这些内核设施在启动后从未使用过也是如此。Zircon 采用了不同的方法。
引导加载程序和内核启动
引导加载程序会将内核加载到内存中,并将控制权转移到内核的启动代码。本文未介绍引导加载程序协议的详细信息。与 Zircon 搭配使用的引导加载程序会同时加载内核映像和 Zircon 引导映像格式的数据 blob。ZBI 格式是一种简单的容器格式,用于嵌入引导加载程序传递的项,包括硬件专用信息、提供启动选项的内核“命令行”,以及 RAM 磁盘映像(通常是压缩的)。内核会在启动阶段的早期提取一些基本信息供其自行使用。
BOOTFS
Zircon 启动映像中嵌入的一个项目是初始 RAM 磁盘文件系统映像。图片通常使用 zstd 格式进行压缩。解压缩后,映像将采用 BOOTFS 格式。这是一种简单的只读文件系统格式,只会列出文件名,以及每个文件在 BOOTFS 映像中的偏移量和大小(这两个值都必须与页面对齐,并且都限制为 32 位)。
主要的 BOOTFS 映像包含用户空间系统运行所需的所有内容:可执行文件、共享库和数据文件。这些包括设备驱动程序的实现和更高级的文件系统,这些实现使您能够从存储设备或网络设备读取更多代码和数据。
系统自行引导后,主要 BOOTFS 中的文件会成为根位于 /boot
的只读文件系统树(由组件管理器提供)。
内核加载 userboot
内核不包含用于解压缩 zstd 格式的任何代码,也不包含用于解读 BOOTFS 格式的任何代码。而是由第一个用户空间进程(称为 userboot
)完成所有这些工作。
userboot
是一个常规的用户空间进程。它只能像任何其他进程一样通过 vDSO 进行标准系统调用,并且受完整的 vDSO 强制执行机制的约束。userboot
的特别之处在于其加载方式。
userboot
构建为 ELF 动态共享对象,使用与 vDSO 相同的 RODSO 布局。与 vDSO 一样,userboot
ELF 映像会在编译时嵌入到内核中。其布局简单,这意味着加载它不需要内核在启动时解析 ELF 头文件。内核只需知道三件事:只读段的大小、可执行段的大小和 userboot
入口点的地址。在编译时,这些值会从 userboot
ELF 映像中提取,并用作内核代码中的常量。
与任何其他进程一样,userboot
必须从已映射到其地址空间的 vDSO 开始,才能进行系统调用。内核会将 userboot
和 vDSO 映射到第一个用户进程,然后在 userboot
入口点启动该进程。
内核发送 processargs
消息
在正常程序加载期间,系统会向每个新进程发送一个引导加载程序消息。进程的第一个线程会在寄存器中接收一个通道句柄。然后,它就可以读取其创建者发送的数据和句柄。
内核使用完全相同的协议启动 userboot
。内核命令行会拆分为字词,这些字词会成为引导消息中的环境字符串。此消息中包含 userboot
本身需要的所有句柄,以及系统其余部分需要访问内核设施的所有句柄。句柄信息条目遵循常规格式,用于说明每个句柄的用途。其中包括 PA_VMO_VDSO
手柄。
userboot 在 vDSO 中查找系统调用
用于告知新进程其 vDSO 映射的标准惯例要求该进程解析 vDSO 的 ELF 头文件和符号表,以定位系统调用入口点。为避免这种复杂性,userboot
会以其他方式在 vDSO 中查找入口点。
当内核将 userboot
映射到第一个用户进程时,它会选择内存中的随机位置,就像正常的程序加载一样。不过,在映射 vDSO 时,它不会像平常一样选择其他随机位置。而是将 vDSO 映像放置在内存中的 userboot
映像之后。这样,vDSO 代码始终位于 userboot
代码的固定偏移处。
在编译时,系统会从 vDSO ELF 映像中提取所有系统调用入口点的符号表条目。然后,这些符号会转换为链接器脚本符号定义,这些定义会使用每个符号在 vDSO 映像中的固定偏移量,以便在相对于链接器提供的 _end
符号的固定偏移量处定义该符号。这样,userboot
代码就可以在内存中 userboot
映像本身之后的确切位置直接调用每个 vDSO 入口点。
userboot 解压缩 BOOTFS
userboot
首先要做的是读取内核发送的引导消息。在从内核获取的句柄中,有一个句柄的句柄信息条目为 PA_HND(PA_VMO_BOOTDATA, 0)
。这是一个包含引导加载程序中的 ZBI 的 VMO。userboot
会从此 VMO 读取 ZBI 标头,以查找类型为 ZBI_TYPE_STORAGE_BOOTFS
的第一个项。该文件包含 BOOTFS 映像。项的 ZBI 头文件指示其是否已压缩(通常是已压缩)。userboot
会映射到 VMO 的这一部分。userboot
包含 zstd 格式支持代码,用于将项解压缩为新的 VMO。
userboot 从 BOOTFS 加载第一个“真实”用户进程
接下来,userboot
会检查从内核收到的环境字符串,这些字符串代表内核命令行。如果存在字符串 userboot.next=
file+optional_arg1+optional_arg2=foo+...,则 file 将作为第一个真实用户进程加载,并将通过“+”分隔的参数传递给它。如果不存在此类选项,则默认文件为 bin/component_manager+--boot
。这些文件位于 BOOTFS 映像中。
为了加载文件,userboot
实现了一个功能齐全的 ELF 程序加载器。通常,要加载的文件是具有 PT_INTERP
程序头的动态链接可执行文件。在这种情况下,userboot
会搜索 PT_INTERP
中命名的文件,并改为加载该文件。
然后,userboot
会在随机地址处加载 vDSO。它会按照标准惯例启动新进程,并向其传递通道句柄和 vDSO 基地址。在该通道上,userboot
会发送标准的 processargs
消息。它会传递从内核收到的所有重要句柄(将特定句柄,例如进程-自身句柄和线程-自身句柄,替换为新进程(而非 userboot
本身)的句柄)。
userboot 加载程序服务
根据标准程序加载协议,当 userboot
通过 PT_INTERP
加载程序时,它会在主消息之前发送一条额外的 processargs
消息,以供动态链接器使用。此消息包含一个频道的 PA_LDSVC_LOADER
句柄,userboot
会在该句柄上提供标准加载器服务的最小实现。
userboot
只有一个线程,该线程会一直循环处理加载器服务请求,直到通道关闭。收到 LOADER_SVC_OP_LOAD_OBJECT
请求后,它会在 BOOTFS 中将前缀为 lib/
的对象名称作为文件进行查找,并返回其内容的 VMO。因此,第一个“真实”用户进程可以是(通常是)需要各种共享库的动态链接可执行文件。动态链接器、可执行文件和共享库都从同一 BOOTFS 页面加载,这些页面稍后会显示为 /boot
中的文件。
将由 userboot
加载的可执行文件(即 component manager
)通常应在完成启动后关闭其加载器服务通道。这样,userboot
就会知道不再需要它了。
userboot 驶向夕阳
当加载器服务通道关闭(或者如果可执行文件没有 PT_INTERP
,因此不需要加载器服务,那么在进程启动后立即关闭),userboot
就不再有任何事情可做。
如果在内核命令行中指定了 userboot.shutdown
选项,则 userboot
会等待其启动的进程退出,然后关闭系统(就像通过 power shutdown
命令关闭系统一样)。这对于运行单个测试程序,然后关闭机器(或模拟器)非常有用。例如,命令行 userboot.next=bin/core-tests userboot.shutdown
会运行 Zircon 核心测试,然后关闭。
否则,userboot
不会等待进程退出。userboot
会立即退出,让第一个“真实”用户进程负责启动和关闭系统的其余部分。