Zircon是一种微内核的设计风格。微内核设计的一种复杂性在于如何引导初始的用户空间进程。通常达到这一功能是通过让内核实现以引导为目的的最小版本的文件系统读取和程序加载,这些内核功能可能在引导之后再也不会使用。Zircon采用了不同的方法。
文章目录
Boot loader 与内核启动
引导加载程序boot loader
将内核加载到内存中,并将控制权转交到内核的启动代码。引导加载程序协议的详细信息不在此处描述。Zircon使用的boot loader
以Zircon Boot Image的格式加载内核镜像与数据块(data blob)。ZBI格式是嵌入了boot loader
传递的参数项的容器格式,包括硬件特定信息、提供引导选项的[kernel “command line”](kernel_cmdline.md)和RAM Disk镜像(通常是压缩格式的)。内核在启动的早期阶段提取了一些供它自身使用的基本信息。
BOOTFS
ZBI
中嵌入的项目之一是初始RAM Disk文件系统镜像。其通常使用LZ4压缩格式。一旦解压缩,镜像将采用BOOTFS格式。这是一个小型的只读文件系统格式,简单的列出了文件名,以及每个文件在BOOTFS镜像中的偏移量和大小(两个值都必须以页面对齐,限制为32位)。
主BOOTFS镜像包含用户空间系统需要运行的所有:可执行文件、共享库和数据文件。这些当中包括设备驱动程序和更高级的文件系统的实现,使从存储设备或网络中读取更多代码和数据成为可能。
系统自启动后,主BOOTFS镜像中的文件作为只读文件系统挂载到/boot
目录下(并由bootsvc管理)。
内核加载userboot
内核不包含任何用于解压缩LZ4格式的代码,也不包含能够解释BOOTFS格式的任何代码。相反,所有这些工作由第一个用户空间进程userboot
完成。
userboot
是一个常规的用户空间进程。它只能像其它进程一样,通过vDSO请求标准的系统调用,以及完全受到vDSO enforcement的约束。userboot
的特殊之处在于它的加载方式。
userboot
程序以ELF动态共享对象格式生成,使用与vDSO相同的RODSO layout布局。类似于vDSO,userboot
ELF镜像在编译时嵌入到的内核中。它的简单布局意味着要加载它不需要内核在引导时解释ELF头部信息。内核只需要知道三件事:只读段的大小、可执行段的大小和userboot
执行入口点。在编译时,由userboot
程序的ELF镜像中提取这些值,在内核代码中当中常量使用。
像其它进程一样,userboot
必须在执行前确保VDSO已经映射到其地址空间,以便进行系统调用。内核将userboot
和vDSO映射到第一个用户进程中,然后在userboot
入口点启动它。
内核发送processargs
消息
在常规 program loading,过程中,发送一个 bootstrap message 到新进程中。进程的首个线程接收一个位于寄存器中的channel句柄。接下来可以读取进程创建者发送的数据和句柄。
内核使用与以上完全相同的协议来启动userboot
。内核命令行被拆分为单独的词,这些词将成为引导消息中的环境变量字符串。userboot
本身需要的所有句柄,以及系统的其余部分访问内核设施所需的句柄,都包含在此消息中。按照正常格式,handle info enties描述每个句柄的用途。其中包括PA_VMO_VDSO
句柄。
userboot在vDSO中查找system calls
用于通知新进程VDSO映射的standard convention标准,要求进程来解释vDSO的ELF头部信息和符号表,用于定位系统调用入口点。为了避免这种复杂性,userboot
以一种不同的方式在vDSO中查找系统调用入口点。
当内核将userboot
映射到第一个用户进程时,它选择内存中的一个随机位置,就像正常程序加载时一样。但是,当它映射vDSO时,它不会像在通常情况下选择另一个随机的位置。相反,它会将vDSO镜像正好放置在内存中的userboot
镜像之后。这样,vdDSO代码总是位于userboot
代码的固定偏移处。
在编译时,所有系统调用入口点的符号表条目都将由vDSO的ELF镜像中提取出来。然后,发送给链接器脚本符号定义,由于每个符号在vDSO镜像中的偏移是固定的,使用该偏移来定义位于链接器提供的_end
符号的固定偏移符号。这样,userboot
代码可以直接调用每个vDSO入口点,因为这些入口将在userboot
镜像本身之后的正确内存位置中。
userboot解压BOOTFS
userboot
做的第一件事是读取内核发送的启动引导消息。所有它从内核获得的句柄中,有一个handle info entry PA_HND(PA_VMO_BOOTDATA, 0)
。这是一个VMO包含来自boot loader的ZBI镜像。userboot
从这个VMO中读取ZBI头部信息,查找第一个类型为ZBI_TYPE_STORAGE_BOOTFS
的项。其包含BOOTFS镜像。此项的ZBI头部指示它是否被压缩,通常是这样。userboot
映射VMO的这一部分。userboot
包含LZ4格式支持代码,用于将此项解压缩到新的VMO中。
userboot由BOOTFS加载第一个"real"用户进程
接下来,userboot
检查它从内核接收到的环境变量,其表示内核命令行。如果存在字符串userboot=
file,随后file将作为第一个实际用户进程加载。如果不存在这样的项,则默认的file是bin/bootsvc
。其包含在BOOTFS镜像中。
要加载文件,userboot
实现了一个功能齐全的ELF程序加载器。通常加载的文件是一个动态链接的可执行文件,带有PT_INTERP
程序头部。在本例中,userboot
查找以PT_INTERP
命名的文件并加载。
然后,userboot
加载vDSO到一个随机的地址。使用标准约定启动一个新的进程,传递一个通道channel句柄和vDSO基址。在通道句柄上,userboot
发送标准的processargs
信息。它传递从内核接收到的所有重要句柄(需要替换特定的句柄,如进程本身和线程本身句柄,替换为新进程本身的,而不是userboot
进程的)。
userboot加载器服务
遵循标准程序加载协议,当userboot
通过PT_INTERP
加载程序,在主消息之前发送额外的processargs
消息,旨在使用动态链接器。此消息包括一个PA_LDSVC_LOADER
通道句柄,在此通道上,userboot
提供一个最小实现的标准loader service.
userboot
只有一个线程,它处于循环中,处理加载器的服务请求,直到通道channel
关闭。当它收到LOADER_SVC_OP_LOAD_OBJECT
请求,它在BOOTFS中查找前缀为lib/
的对象名文件,并返回其内容的VMO。因此,第一个"real"的用户进程可以(通常是)一个动态链接的可执行文件,其运行需要各种共享库。动态链接器、可执行文件和共享库都是从相同的BOOTFS页面加载,其后以文件的形式显示在/boot
目录中。
将由userboot
加载的可执行文件(i.e. bootsvc
)通常应在启动完成之后关闭它的加载器服务通道。这让userboot
知道它不再需要了。
userboot进程结束
当加载器服务通道关闭的时候(或者如果可执行文件没有PT_INTERP
,因此不再需要加载器服务,进程已经启动完成),userboot
不再有任何操作。
如果userboot.shutdown
选项指定在内核命令行中,userboot
将等待它开启的进程退出,然后关闭系统(就像通过dm shutdown
命令)。这对于运行单个测试程序,然后关闭计算机(或模拟器)很有用。例如,命令行userboot=bin/core-tests userboot.shutdown
运行Zircon的核心测试程序,然后关闭。
否则,userboot
不等待其启动的进程退出。userboot
立即退出,留下第一个"真正的"用户进程负责处理系统其它部分的启动和关闭。