虚拟化 之一 详解 jailhouse 架构及原理、软硬件要求、源码文件、基本组件

Jailhouse 是一个基于 Linux 实现的针对创建工业级应用程序的小型 Hypervisor,是由西门子公司的 Jan Kiszka 于 2013 年开发的,并得到了官方 Linux 内核的支持,在开源社区中获得了知名度和吸引力。

Jailhouse

Jailhouse 是一种轻量级的虚拟化技术,可以将多个操作系统(或者裸机程序)同时运行在同一台硬件上。它是一个基于 Linux 的静态分区的 Hypervisor,但本身并不改造 Linux 内核,而是利用 Linux 系统的开放性,增加一个或多个实时操作系统,实现多系统在一个多核处理器上运行。

Jailhouse 不会模拟不存在的硬件资源,也不包含任何的调度处理,而是利用虚拟化技术将硬件资源划分为多个被称为 Cell 的独立空间,并将每个 Cell 分配给不同的虚拟机。每个 Cell 独占自己的处理器核心、内存、I/O 设备和中断控制器等资源。这样可以确保不同虚拟机之间的资源互相隔离,提高系统的可靠性。

Root Cell

当 Jailhouse 启动之后,原来的负责启动 Jailhouse 的 Linux 系统所在空间就被称为 Root Cell。对于所有 Jailhouse 虚拟机的管理都是在 Root Cell 中的 Linux 系统中通过 Jailhouse 提供的命令行工具来实现的。

Non-root Cell

除了 Root Cell 之外的每个独立的 Cell 就是一个 Non-root Cell,每个 Non-root Cell 就对应一个虚拟机。Non-root Cell 通过 Root Cell 进行管理,只要资源够用,Non-root Cell 可以有任意多个。

inmate

Non-root Cell 虚拟机中运行的系统(也可能是一个裸机程序)被称为 inmate ,目前可以是 LinuxFreeRTOSERIKA3 RTOSZephyr 其中之一。Jailhouse 都是直接将其作为原始二进制文件(不是 ELF 文件),直接加载到其对应的配置文件中指定的内存中去运行。

在 x86 平台下,由于 Jailhouse 只向 Non-root Cell 的 inmate 暴露最小环境,可用的资源不足以在不进行修改的情况下引导标准的 Linux 系统。为此,西门子官方提供了一个补丁队列,以便在 x86 平台下的 Non-root Cell 中启动 Linux。同时在构建时需要注意以下几点:

  1. 需要使能 CONFIG_JAILHOUSE_GUEST
  2. 禁用 CONFIG_SERIOCONFIG_PM_TRACE_RTC
  3. 一般还应该禁用所有不需要的驱动程序和特性,以避免不需要的 probe,并且使镜像大小和内存占用最小化

在 ARM / ARM64 平台上,引导 Linux 内核要比在 x86 平台上简单得多,因此在这种情况下,我们不需要为 Non-root Cell 特别修改 Linux 内核。

内存布局

Jailhouse 的实现需要使用一块连续内存,这块内存需要在启动 Linux 时保留出来。至于内存具体用哪一块,则取决于自己的设备,如下以 0x100000000 为例的示意图如下所示:

Cell 间通信

虽然 Jailhouse 将硬件资源进行了划分到了不同的 Cell 中,通过虚拟机监控器实现了相互隔离,但在实际应用过程中,Cell 间也需进行通信。为此,Jailhouse 通过虚拟 ivshmem(Inter-VM shared memory) PCI 设备在 Cell 之间提供共享内存和信号机制。一个通道将两个分区 1:1 对应地连接起来。

环境要求

Jailhouse 是一个依托于 Linux Kernel 的开放性从而直接使用硬件平台提供的虚拟化技术来实现的虚拟化解决方案。因此,Jailhouse 需要 Linux Kernel 的支持及硬件平台的虚拟化技术。

硬件要求

Jailhouse 利用 Intel 的 VT-x、AMD 的 AMD-V 和 ARM 虚拟化扩展等硬件辅助虚拟化技术来划分物理资源和限制虚拟机对这些资源的访问,从而实现对系统资源的隔离控制。

x86 架构

  • 针对 Intel 平台,需要 64 位架构以及 VMX(Virtual Machine Extensions,Intel CPU 中使用 vmx 标识 Intel 的 VT-x 虚拟化技术) 技术,具体细节:

    • 具备 EPT(Extended Page Tables,扩展页表)支持。扩展页表是用于内存管理单元(MMU)的 Intel 第二代 x86 虚拟化技术。 直接在实模式下启动逻辑 CPU 就需要 EPT 的支持,这一功能在英特尔的行话中称为无限制访客模式,并在 Westmere 微体系结构中引入。

      Intel 的 Core i3、Core i5、Core i7 和 Core i9 CPU 等均支持 EPT

    • Preemption Timer 是一种可以周期性使 VM 触发 VMEXIT 的一种机制。即设置了 Preemption Timer 之后,可以使得虚拟机在指定的 TSC cycle 之后产生一次 VMEXIT 并设置对应的 exit_reason,trap 到 VMM 中。

    • 支持中断重映射的 Intel IOMMU(Intel 官方称为 VT-d(Virtualization Technology for Directed I/O))

  • 针对 AMD 平台,需要 64 位架构以及 SVM(AMD Secure Virtual Machine(这个是 AMD 内部的研发代号,后来统一使用 AMD-V 作为对外名称),AMD CPU 中使用 svm 来表示 AMD 的 AMD-V 虚拟化技术)技术:

    • 必须具备 NPT(Nested Page Tables,嵌套页表)支持。嵌套页表现在称为快速虚拟化索引(Rapid Virtualization Indexing,RVI)是 AMD 第二代处理器内存管理单元(MMU)硬件辅助虚拟化技术。
    • 推荐具备 Decode Assists 支持
    • AMD IOMMU(AMD 官方称为 AMD-Vi(AMD's I/O Virtualization Technology))目前不被支持,但后续需要
  • 至少两个逻辑 CPU 核心。注意这里是逻辑 CPU 核心,不是物理 CPU 核心。

  • 在 x86 平台下,需要在 BIOS 或 UEFI 中开启虚拟化功能!

EPT 和 NPT 是 Intel 和 AMD 两家对于 Second Level Address Translation(SLAT,二级地址转换)在自家 CPU 上的具体实现。SLAT 是一种硬件辅助虚拟化技术,可以避免与软件管理的影子页表相关的开销。此外,IOMMU 也经常被我们称为 PCI 直通。

ARM 架构

  • ARMv8 架构或者是带有虚拟化扩展的 ARMv7 架构。ARM 的虚拟化扩展支持 SLAT,即由 Stage-2 MMU 提供的 Stage-2 页表。客户机使用 Stage-1 MMU。该支持在 ARMv7ve 架构中作为可选添加,并且在 ARMv8(32 位和 64 位)架构中也受支持。

  • 至少两个逻辑 CPU 核心

  • 支持如下 AArch32 架构的开发板

    • Banana Pi (see more)
    • Orange Pi Zero (256 MB version)
    • NVIDIA Jetson TK1
    • ARM Versatile Express with Cortex-A15 or A7 cores (includes ARM Fast Model)
    • emtrion emCON-RZ/G1x series based on Renesas RZ/G (see more)
  • 支持如下 AArch64 架构的开发板

    • AMD Seattle / SoftIron Overdrive 3000
    • LeMaker HiKey
    • NVIDIA Jetson TX1 and TX2
    • Xilinx ZCU102 (ZynqMP evaluation board)
    • NXP MCIMX8M-EVK

内核要求

Jailhouse 的构建是依赖于 Linux Kernel 的,因此,必须使用对应的 Linux Kernel。但是,不同版本的 Linux Kernel 对于 Jailhouse 的支持情况不尽相同。在使用比较新的 Linux Kernel 时,需要对 Linux Kernel 进行打补丁,否则将出现各种错误!

x86 架构

  1. 必须禁用 Linux Kernel 对 VT-d IOMMU 的使用(DMAR)。这个可以通过在 GRUB 配置文件中设置 GRUB_CMDLINE_LINUX = "intel_iommu=off"(IOMMU 硬件有 intel/amd/arm 的等,一般用 intel 的硬件) 这个启动参数来处理。

    1. kvm 一定要用 intel_iommu=on,DPDK/SPDK 如果绑定 vfio-pci 那也一定要求 intel_iommu=on,如果绑定 uio/igb_uio 那么就不需要intel_iommu=on
    2. dmesg | grep -E "DMAR|IOMMU" 查看
  2. 要利用更快的 x2APIC,需要在内核中打开中断重新映射。这个需要再构建内核时启用 CONFIG_IRQ_REMAP 这个配置项

  3. Jailhouse 本身和每个 Cell 都需要一块连续的 RAM,这个必须在 Kernel 启动之前配置。通常是使用 memmapmem 这两个启动命令来实现。在 x86 平台上,这通常是在 GRUB 配置文件中通过添加 GRUB_CMDLINE_LINUX="memmap=82M\\\$0x3a000000" 这个启动参数来处理。

ARM 架构

针对于 AArch32 架构,需要 Linux Kernel 版本大于等于 3.19;而对于 AArch64 架构,则需要 Linux Kernel 版本大于等于 4.7。此外,还需要适当的的引导程序(例如 U-Boot)的支持。

  • Linux Kernel 必须以 HYP 模式启动(工作在 HYP 模式下的 CPU 上,默认是 SVC 模式)。这里就涉及到了 ARM 架构中的不同特权等级,ARMv7 中使用的是 Privilege level 的概念,ARMv8 中则使用 Exception Level 这个概念
    • Supervisor Call(SVC)指令使用户模式程序可以请求操作系统服务。
    • Hypervisor Call(HVC)指令使客户操作系统能够请求 Hypervisor 服务。
    • Secure monitor Call(SMC)指令使普通世界能够请求安全世界服务。
  • PSCI 对 CPU 离线的支持。PSCI(Power State Coordination Interface,电源状态协调接口) 是一个接口,这个接口实现了电源管理用例。The ARM Trusted Firmware 实现了 PSCI(Power State Coordination Interface) 接口作为运行时服务。Normal world software 可以通过 ARM SMC(Secure Monitor Call) 指令来访问 ARM Trusted Firmware 服务
  • Jailhouse 本身和每个 Cell 都需要一块连续的 RAM,这个必须在 Kernel 启动之前配置。在 ARM 平台上,这可以通过减少内核看到的内存量(通过 mem = 内核启动参数)或修改 Device Tree(即保留内存节点)来实现。

源码

Jailhouse 是由西门子的 Jan Kiszka 在 2013年以 GPLv2 协议开源的一个虚拟化解决方案。并很快得到了官方 Linux 内核的支持,在开源社区中获得了知名度和吸引力。本文及后续博文以目前最新提交(e57d1eff6d55aeed5f977fe4e2acfb6ccbdd7560)版作为学习对象。

源码文件

Jailhouse 作为一个极度精简的虚拟化实现方案,其代码量还是非常小的,几万行代码量就实现了一个功能强大的虚拟机。

  • .githubci:这两个目录下是用来持续集成环境中使用的构建 Jailhouse 所需的 Linux Kernel 的相关配置文件,目前仅支持 GitHub Actions。

  • config:这个目录下就是针对 armarm64x86 架构下用于生成 Jailhouse 的 Cell 的配置文件对应的源文件。在对应架构下构建 Jailhouse 时,对应架构目录下的每个 .c 就会被构建为对应的 .cell 文件。

    1. 配置文件的内容其实就是一个 C 语言的结构体变量,因此使用的就是 .c 扩展名
    2. 该目录下的 .c 文件实际上都是一些示例,在真正使用时,我们需要提供自己的配置文件!
  • Documentation:Jailhouse 的文档对应的源码,它使用的是 Doxygen 文档系统。使用 sudo apt install doxygen 后,使用命令 make docs 就可以在 Documentation/generated/ 构建出对应的文档。

  • driver:Jailhouse 的驱动源码,最终会被编译为 jailhouse.ko,并被放到 /lib/modules/$(uname -r)/extra/driver/ 目录下来使用!

    • cell.c/h:实现 Cell 相关命令的处理
    • pci.c/h:实现对 PCI 设备的处理。在将 PCI 设备分配给 Non-root Cell 时,我们需要确保 Root Cell 中的 Linux 不再使用这些设备。只要设备被分配给了其他 Cell ,Root Cell 就不得再访问这些设备。不幸的是,我们不能仅仅使用 PCI 热插拔来在运行时删除/重新添加设备,因为,Linux 将重新编程 BAR(Base Address Registers)并定位到我们不希望/不允许的资源位置。
        所以,Jailhouse 充当了一个 PCI 虚拟驱动程序,在其他 Cell 使用设备时它会声明这些设备。在创建 Cell 时,设备将从其驱动程序中解绑,并绑定到 Jailhouse。当 Cell 被销毁时,Jailhouse 将释放其设备。当禁用 Jailhouse 时,它将释放所有已分配的设备。
        当释放设备时,它们将不再绑定到任何驱动程序,从 Linux 的角度来看,Jailhouse 虚拟驱动程序仍然会被视为有效的驱动程序。将设备重新分配给原始驱动程序必须手动完成。
    • sysfs.c/h:还通过 sysfs 虚拟文件系统(/sys/devices/jailhouse)向用户空间暴露 Jailhouse 的数据结构。
    • main.c/h:驱动入口
  • hypervisor:Jailhouse 用于管理各个虚拟机的工具的源码代码,最终会被编译为 jailhouse*.bin,被放到 /lib/firmware 目录下

    • arch:再实际使用中,架构相关的代码先被执行,其中调用架构无关的代码
    • 其他:架构无关的代码,其中的接口被 arch 中对应的接口调用
  • include:Jailhouse 对外提供的各种 C 头文件,其中包含了各种数据结构的定义,例如,cell-config.h 中就定义了各种设备的数据结构、Cell 的描述符 jailhouse_cell_desc 等等

    Jailhouse 允许用户定义一些在编译时启用的特定于平台的设置或者调试配置参数。方法是新增 include/courahouse/config.h 这个文件,然后在该文件中将对应的配置项定义为 1。如下是当前可用的配置项:

    c 复制代码
    /* Print error sources with filename and line number to debug console */
    #define CONFIG_TRACE_ERROR 1
    
    /*
     * Set instruction pointer to 0 if cell CPU has caused an access violation.
     * Linux inmates will dump a stack trace in this case.
     */
    #define CONFIG_CRASH_CELL_ON_PANIC 1
    
    /* Enable code coverage data collection (see Documentation/gcov.txt) */
    #define CONFIG_JAILHOUSE_GCOV 1
    
    /*
     * Link inmates against a custom base address.  Only supported on ARM
     * architectures.  If this parameter is defined, inmates must be loaded to
     * the appropriate location.
     */
    #define CONFIG_INMATE_BASE 0x90000000
    
    /*
     * Only available on x86. This debugging option that needs to be activated
     * when running mmio-access tests.
     */
    #define CONFIG_TEST_DEVICE 1
  • inmates:这里面是 Jailhouse 虚拟机本身的固件源码,他们会被编译为 .bin 文件。

    • demos:这里面就是一些 inmate 的源码,编译之后就是一个个的 inmate 镜像(.bin 文件)。其都有一个对应的 .cell 文件,位于 configs/ 目录下。
    • lib:该目录下是一些可以在 inmate 的源码中使用的库函数的实现。
    • tests:该目录下是一些验证 Jailhouse 的用例
    • tools:该目录下是一些用来辅助处理 inmate 的工具的源码,每个 .c 文件经过编译之后成为一个 xxx.bin,最终所有的 .bin 会被安装到 /usr/local/libexec/jailhouse 目录下。目前该目录下就只有一个 linux-loader 的源码。
  • pyjailhouse:这里面是用来处理在 Non-root Cell 中运行 Linux 虚拟机时对 Linux Kernel 进行处理的一个 Python 脚本库。

  • scripts:编译系统使用的相关脚本

  • tools:这里面是一些 Jailhouse 实用工具(jailhouse )的源码(其中有些是 Python 脚本)。其中,Python 脚本会被放到 /usr/local/libexec/jailhouse 目录下;jailhouse 则被放到 /usr/local/sbin 目录下。

  • Makefile:构建系统的入口

  • Kbuild:也是个 Makefile 文件

  • setup.py:用来打包及安装 pyjailhouse 的脚本文件。

  • 其他:其他

构建

Jailhouse 的构建过程非常简单,但是由于不同的平台下的虚拟化技术的差异,在构建时遇到的问题也不一样,受限于博文篇幅,我们将在后续博文中详细学习在不同平台下的构建及使用。

  1. 虚拟化 之二 详解 jailhouse(x86 平台)的构建过程、配置及使用
  2. 虚拟化 之三 详解 jailhouse(ARM 平台)的构建过程、配置及使用

基本组件

完整的 Jailhouse 组件主要由内核模块(jailhouse.ko)、虚拟机管理程序固件(jailhouse*.bin)、管理工具(jailhouse 命令行程序及一些 Python 脚本)以及配置文件(.cell)这四部分组成。用户使用它们来启用虚拟机管理程序、创建 Cell、加载 inmate 二进制文件以及运行和停止它等。

jailhouse.ko

jailhouse.ko 由源码根目录中的 driver 目录中源码在构建之后生成,最终会被安装到 /lib/modules/$(uname -r)/extra/driver/ 目录下。它就是一个标准的 Linux Driver 程序,实现为一个 struct miscdevice 设备(主设备号 MISC_MAJOR(10))。使用命令 cat /proc/misc 可以查看各杂项设备。

Linux 中将设备分为字符设备(I2C、USB、SPI等)、块设备(存储器相关的设备如EMMC、SD卡、U盘等)和网络设备(网络相关的设备WIFI等)三大类,其中,杂项设备归属于字符设备。每个设备节点都有主设备号和次设备号 ,杂项设备的主设备号固定为10,次设备号根据设备不同而不同。

jailhouse_init()

当加载 jailhouse.ko 之后,驱动源码 driver/main.c 中的 static int __init jailhouse_init(void) 函数就会进行各种初始化,主要就干了以下几个事:

  1. 开头的这一堆的宏定义主要就是为了解决 Jailhouse 用的一些符号 Linux Kernel 没有导出的问题。其核心就是通过 kallsyms_lookup_name 这个内核接口来查找需要的符号。

      但是,5.7.0 以上版本的内核不再导出 kallsyms_lookup_name,对于在高版本内核不在导出的原因请参考 https://lwn.net/Articles/813350/。实际上,在 5.7.0 以上仍旧可以用 struct kprobe 来获取 kallsyms_lookup_name 函数的地址,然后再进一步获取到想要的符号。

  2. 通过 root_device_register("jailhouse") 创建 /sys/devices/jailhouse 这个设备,然后调用 jailhouse_sysfs_init(jailhouse_dev) 初始化其中的内容,此后,用户空间就可以通过 /sys/devices/jailhouse 访问 Jailhouse 的数据结构。

    c 复制代码
    zcs@zcs-MassDatas-GXXA203:~/WORKSPACE/Jailhouse/jailhouse$ tree -L 3 -p /sys/devices/jailhouse
    /sys/devices/jailhouse
    ├── [drwxr-xr-x]  cells									# 这个目录中包含了我们创建的那些 Cell 的信息
    │   ├── [drwxr-xr-x]  0									# 这个是 Cell 的 ID,Root Cell 的 ID 为 0,后续每创建一个 Cell ,ID 自动增 1
    │   │   ├── [-r--r--r--]  cpus_assigned					# 这个是我们在 Cell 的配置文件中分配给 Cell 的 CPU 原始的配置参数(按位使用置 1 表示使用,例如,fffb)
    │   │   ├── [-r--r--r--]  cpus_assigned_list			# 这个是分配给 Cell 的 CPU 的方便我们阅读的列表。例如 0-1,3-15
    │   │   ├── [-r--r--r--]  cpus_failed					# 这个是分配给 Cell 的 CPU 中失败的那些
    │   │   ├── [-r--r--r--]  cpus_failed_list				# 这个是分配给 Cell 的 CPU 中失败的那些的列表
    │   │   ├── [-r--r--r--]  name							# Cell 的名字
    │   │   ├── [-r--r--r--]  state							# Cell 的状态。"running", "running/locked", "shut down", 或 "failed" 之一
    │   │   └── [drwxr-xr-x]  statistics					# Cell 的统计数据
    │   │       ├── [drwxr-xr-x]  cpu0						
    │   │       ├── [drwxr-xr-x]  cpu1
    │   │       ├── [drwxr-xr-x]  cpu10
    │   │       ├── [drwxr-xr-x]  cpu11
    │   │       ├── [drwxr-xr-x]  cpu12
    │   │       ├── [drwxr-xr-x]  cpu13
    │   │       ├── [drwxr-xr-x]  cpu14
    │   │       ├── [drwxr-xr-x]  cpu15
    │   │       ├── [drwxr-xr-x]  cpu3
    │   │       ├── [drwxr-xr-x]  cpu4
    │   │       ├── [drwxr-xr-x]  cpu5
    │   │       ├── [drwxr-xr-x]  cpu6
    │   │       ├── [drwxr-xr-x]  cpu7
    │   │       ├── [drwxr-xr-x]  cpu8
    │   │       ├── [drwxr-xr-x]  cpu9						# 以上这些是分配给当前 Cell 使用的所有逻辑 CPU。每个 CPU 节点展开后的内容和下面这些是一样,只不过表示的是单个 CPU 的,下面这些是以上所有 CPU 的汇总
    │   │       ├── [-r--r--r--]  vmexits_cpuid
    │   │       ├── [-r--r--r--]  vmexits_cr
    │   │       ├── [-r--r--r--]  vmexits_exception
    │   │       ├── [-r--r--r--]  vmexits_hypercall
    │   │       ├── [-r--r--r--]  vmexits_management
    │   │       ├── [-r--r--r--]  vmexits_mmio
    │   │       ├── [-r--r--r--]  vmexits_msr_other
    │   │       ├── [-r--r--r--]  vmexits_msr_x2apic_icr
    │   │       ├── [-r--r--r--]  vmexits_pio
    │   │       ├── [-r--r--r--]  vmexits_total				# 全部 CPU 上发生的 VM Exits 总次数,其他的 _xxx 则表示由于 xxx 原因产生的 VM Exits 数量。例如,vmexits_xapic 就表示由于 xapic 产生的 VM Exits 次数
    │   │       ├── [-r--r--r--]  vmexits_xapic
    │   │       └── [-r--r--r--]  vmexits_xsetbv
    │   └── [drwxr-xr-x]  1									# 第二个 Cell,其中的内容与上面的一样
    │       ├── [-r--r--r--]  cpus_assigned
    │       ├── [-r--r--r--]  cpus_assigned_list
    │       ├── [-r--r--r--]  cpus_failed
    │       ├── [-r--r--r--]  cpus_failed_list
    │       ├── [-r--r--r--]  name
    │       ├── [-r--r--r--]  state
    │       └── [drwxr-xr-x]  statistics
    │           ├── [drwxr-xr-x]  cpu2						# 分配给当前 Cell 使用的所有逻辑 CPU。每个 CPU 节点展开后的内容和下面这些是一样
    │           ├── [-r--r--r--]  vmexits_cpuid
    │           ├── [-r--r--r--]  vmexits_cr
    │           ├── [-r--r--r--]  vmexits_exception
    │           ├── [-r--r--r--]  vmexits_hypercall
    │           ├── [-r--r--r--]  vmexits_management
    │           ├── [-r--r--r--]  vmexits_mmio
    │           ├── [-r--r--r--]  vmexits_msr_other
    │           ├── [-r--r--r--]  vmexits_msr_x2apic_icr
    │           ├── [-r--r--r--]  vmexits_pio
    │           ├── [-r--r--r--]  vmexits_total				# 全部 CPU 上发生的 VM Exits 总次数,其他的 _xxx 则表示由于 xxx 原因产生的 VM Exits 数量
    │           ├── [-r--r--r--]  vmexits_xapic
    │           └── [-r--r--r--]  vmexits_xsetbv
    ├── [-r--r--r--]  console								# 这个是 Jailhouse 的终端,我们可以从中直接读取 Jailhouse 的 Log
    ├── [-r--------]  core									# 这里面是 Jailhouse 固件以及配置信息,可以使用 tools/jailhouse-gcov-extract 来解析。访问时,确保是 `jailhouse disable` 状态!
    ├── [-r--r--r--]  enabled								# 指示 Jailhouse 是否启用。 1 表示启用,0 表示未启用
    ├── [-r--r--r--]  mem_pool_size							# 内存池中的页数
    ├── [-r--r--r--]  mem_pool_used							# 内存池中已用的页数
    ├── [lrwxrwxrwx]  module -> ../../module/jailhouse		# 这是一个由内核机制自动创建的符号链接,指向当前目录的所有者(创建者)
    ├── [drwxr-xr-x]  power									# 这个是与电源管理相关的内容
    │   ├── [-rw-r--r--]  async
    │   ├── [-rw-r--r--]  autosuspend_delay_ms
    │   ├── [-rw-r--r--]  control
    │   ├── [-r--r--r--]  runtime_active_kids
    │   ├── [-r--r--r--]  runtime_active_time
    │   ├── [-r--r--r--]  runtime_enabled
    │   ├── [-r--r--r--]  runtime_status
    │   ├── [-r--r--r--]  runtime_suspended_time
    │   └── [-r--r--r--]  runtime_usage
    ├── [-r--r--r--]  remap_pool_size						# 重映射池中的页数
    ├── [-r--r--r--]  remap_pool_used						# 重映射池中已用的页数
    └── [-rw-r--r--]  uevent								# 各种事件
    1. Root 设备是一个虚拟设备,以该 Root 设备为父设备调用 kobject_create_and_add 就可以让其他设备可以挂在它的下面
    2. 其中有些节点需要在执行相应的命令后才会有具体的内容
  3. 通过 misc_register(&jailhouse_misc_dev); 注册 struct miscdevice 设备。加载驱动之后,就会创建 /dev/jailhouse 这个设备。用户空间的 Jailhouse 的管理工具使用 ioctl() 系统调用通过 jailhouse.ko 创建的 /dev/jailhouse 这个文件向 jailhouse.ko 发送各种请求。

  4. 调用 jailhouse_pci_register() 将自身注册为一个虚拟的 PCI 设备驱动程序,以便它可以获取分配的设备。

  5. 调用 register_reboot_notifier(&jailhouse_shutdown_nb); 注册重启回调接口,当内核出现 Kernel Halt、Kernel Restart 或 Kernel Power Off 时,就会调用我们注册的回调函数。Jailhouse 注册之后主要用来关闭自身!

jailhouse_ioctl()

各种请求通过内核最终到达 driver/main.c 中的 jailhouse_ioctl 这个函数。static long jailhouse_ioctl(struct file *file, unsigned int ioctl, unsigned long arg) 这个函数解析收到的请求,然后调用对应的接口来进一步处理。

c 复制代码
static long jailhouse_ioctl(struct file *file, unsigned int ioctl, unsigned long arg)
{
	long err;

	switch (ioctl) {
	case JAILHOUSE_ENABLE:
		err = jailhouse_cmd_enable(
			(struct jailhouse_system __user *)arg);
		break;
	case JAILHOUSE_DISABLE:
		err = jailhouse_cmd_disable();
		break;
	case JAILHOUSE_CELL_CREATE:
		err = jailhouse_cmd_cell_create(
			(struct jailhouse_cell_create __user *)arg);
		break;
	case JAILHOUSE_CELL_LOAD:
		err = jailhouse_cmd_cell_load(
			(struct jailhouse_cell_load __user *)arg);
		break;
	case JAILHOUSE_CELL_START:
		err = jailhouse_cmd_cell_start((const char __user *)arg);
		break;
	case JAILHOUSE_CELL_DESTROY:
		err = jailhouse_cmd_cell_destroy((const char __user *)arg);
		break;
	default:
		err = -EINVAL;
		break;
	}

	return err;
}

jailhouse*.bin

jailhouse*.bin 是由源码根目录中的 hypervisor 目录中的源码在构建之后会生成,针对不同的架构名字会有些区别(它最终会被安装到 /lib/firmware 目录下)。jailhouse*.bin 接收 jailhouse.ko 发来的超级调用,用于硬件资源的分配。

内存布局

jailhouse*.bin 是一个具体特定结构的二进制文件,在 jailhouse*.bin 的开头是一个 struct jailhouse_header 结构。这个 BIN 文件中的部分内容是在构建时就填充好的,还有一部分是在驱动加载它时有驱动程序动态填充的。如下是 jailhouse*.bin 在内存中的布局:

.header 定义于 hypervisor/setup.c 中,并被链接文件强制放到了 BIN 的开头。链接脚本文件 hypervisor.lds 会再构建时被构建系统根据hypervisor/hypervisor.lds.S 自动创建生成(就是简单的展开各种宏(架构不同,宏值不同))。

JAILHOUSE_BASE

JAILHOUSE_BASEjailhouse*.bin 的链接地址,它的具体值被定义到了 hypervisor/arch 中不同架构的 jailhouse_header.h 中。针对同一架构,它一个固定的虚拟地址。

  1. x86平台反汇编 objdump --source --all-headers --demangle --line-numbers --wide hypervisor/hypervisor-intel.o > hypervisor/hypervisor-intel.lst 查看

  2. ARM64 平台反汇编 aarch64-none-linux-gnu-objdump --source --all-headers --demangle --line-numbers --wide hypervisor/hypervisor.o > hypervisor/hypervisor.lst 查看

入口点

jailhouse*.bin 本身不是一个可以直接运行的程序,所以它没有显示定义入口点。作为虚拟机管理程序,它本身来处理虚拟化相关的问题。无论何种架构,都是通过特定架构 hypervisor/arch/*/entry.S 中的 int arch_entry(unsigned int cpu_id) 这个接口来开启虚拟化配置。

arch_entry()

arch_entry() 函数必须在每个在线的 CPU 上被调用,以便将系统控制权交给 Jailhouse。Jailhouse 将等待指定数量(由 struct jailhouse_header 中的 .online_cpus 指定)的 CPU 都完成初始化,并且在所有启动初始化的 CPU 都完成之前,该函数不会返回。在虚拟机监控程序激活期间未初始化的 CPU 在 Jailhouse 再次停用之前不能被任何单元使用。

  • 函数原型: int arch_entry(unsigned int cpu_id)
  • 参数:
    • cpu_id:调用方 CPU 的唯一逻辑 ID
  • 返回值:0 表示成功;其他值表示失败,通常取值如下:
    • -EIO (-5):lacking hardware capabilities or unsupported hardware state (as configured by Linux)
    • -ENOMEM (-12): insufficient hypervisor-internal memory
    • -EBUSY (-16): a required hardware resource is already in use
    • -ENODEV (-19): CPU or I/O virtualization unit missing
    • -EINVAL (-22): invalid system configuration
    • -ERANGE (-34): a resource ID is out of supported range

对于一次初始化尝试,初始化函数将始终在所有 CPU 上返回相同的代码。

entry()

arch_entry() 内部最终通过调用定义于 hypervisor/setup.c 中的架构无关的 int entry(unsigned int cpu_id, struct per_cpu *cpu_data) 函数最终实现启动虚拟化功能。

Hypervisor 与 Cell 间接口

Jailhouse 虚拟机管理程序在运行时提供了三种与 Cell 交互的接口。第一种是只读检测接口。第二种是一组超调用,Cell 可以通过执行特定于体系结构的指令来同步调用这些超调用,从而切换到 Hypervisor 模式。第三种接口由位于每个 Cell 内存区域中的变量组成,该内存区域在 Hypervisor 和特定 Cell 之间共享。

只读检测接口

这种接口对于那些不仅仅在 Jailhouse 的 Cell 内部工作的 Cell 代码非常有用。该 ABI 是特定于体系结构的,到目前为止,它仅适用于 x86 架构。在 x8 6架构上,Jailhouse 在执行 cpuid 指令时修改返回的寄存器值如下所示:

Hypercalls

超调用通常通过指定的指令发出,该指令会导致从客户模式切换到虚拟机管理程序模式。在引起模式切换之前,Cell 必须在预定义的寄存器或已知的内存位置准备好调用的参数。完成的超调用的返回码通过类似的通道传递。超调用 ABI 的详细信息是特定于体系结构的,将在以下部分中定义。

这些调用会由定义于 hypervisor/control.c 中的 long hypercall(unsigned long code, unsigned long arg1, unsigned long arg2) 来进行分发然后进一步来处理。

c 复制代码
long hypercall(unsigned long code, unsigned long arg1, unsigned long arg2)
{
	struct per_cpu *cpu_data = this_cpu_data();

	cpu_data->public.stats[JAILHOUSE_CPU_STAT_VMEXITS_HYPERCALL]++;

	switch (code) {
	case JAILHOUSE_HC_DISABLE:
		return hypervisor_disable(cpu_data);
	case JAILHOUSE_HC_CELL_CREATE:
		return cell_create(cpu_data, arg1);
	case JAILHOUSE_HC_CELL_START:
		return cell_start(cpu_data, arg1);
	case JAILHOUSE_HC_CELL_SET_LOADABLE:
		return cell_set_loadable(cpu_data, arg1);
	case JAILHOUSE_HC_CELL_DESTROY:
		return cell_destroy(cpu_data, arg1);
	case JAILHOUSE_HC_HYPERVISOR_GET_INFO:
		return hypervisor_get_info(cpu_data, arg1);
	case JAILHOUSE_HC_CELL_GET_STATE:
		return cell_get_state(cpu_data, arg1);
	case JAILHOUSE_HC_CPU_GET_INFO:
		return cpu_get_info(cpu_data, arg1, arg2);
	case JAILHOUSE_HC_DEBUG_CONSOLE_PUTC:
		if (!CELL_FLAGS_VIRTUAL_CONSOLE_PERMITTED(
			cpu_data->public.cell->config->flags))
			return trace_error(-EPERM);
		printk("%c", (char)arg1);
		return 0;
	default:
		return -ENOSYS;
	}
}
通信区域

通信区域是一个每个单元内的内存区域,默认情况下,虚拟机管理程序和特定单元都可以对其进行读写。这是一种可选的通信机制。如果某个单元需要使用该区域,则必须通过其配置将该区域映射到单元的地址空间。如果单元配置为在通信区域方面是被动的(单元标志 JAILHOUSE_CELL_PASSIVE_COMMREG)并且该区域已被映射,那么必须在单元配置中将其声明为只读。

管理工具

管理工具主要由源码 tools 目录下的 jailhouse.c 在构建之后生成的 jailhouse 可执行程序以及该目录下的一些 Python 脚本 jailhouse-* 组成。jailhouse 会被放到 /usr/local/sbin/jailhouse 目录下,而那些 Python 脚本最终会被安装到 usr/local/libexec/jailhouse 目录下。

可执行程序 jailhouse

jailhouse 这个可执行程序就是所有管理命令的入口,它就是一个标准的用户空间 Linux C 程序。当我们执行 Jailhouse 命令时,命令首先来到了 tools/jailhouse.cint main(int argc, char *argv[])函数,它负责解析传入的各个选项及参数,然后调用相应的接口进一步处理。

c 复制代码
int main(int argc, char *argv[])
{
	int fd;
	int err;

	if (argc < 2)
		help(argv[0], 1);

	if (strcmp(argv[1], "enable") == 0) {
		err = enable(argc, argv);
	} else if (strcmp(argv[1], "disable") == 0) {
		fd = open_dev();
		err = ioctl(fd, JAILHOUSE_DISABLE);
		if (err)
			perror("JAILHOUSE_DISABLE");
		close(fd);
	} else if (strcmp(argv[1], "cell") == 0) {
		err = cell_management(argc, argv);
	} else if (strcmp(argv[1], "console") == 0) {
		err = console(argc, argv);
	} else if (strcmp(argv[1], "config") == 0 ||
		   strcmp(argv[1], "hardware") == 0) {
		call_extension_script(argv[1], argc, argv);
		help(argv[0], 1);
	} else if (strcmp(argv[1], "--version") == 0) {
		printf("Jailhouse management tool %s\n", JAILHOUSE_VERSION);
		return 0;
	} else if (strcmp(argv[1], "--help") == 0) {
		help(argv[0], 0);
	} else {
		help(argv[0], 1);
	}

	return err ? 1 : 0;
}

Python 脚本 jailhouse-*

对于那些 Python 脚本,我们也不直接使用,而是则由 jailhouse 来帮我们调用的。具体就在 tools/jailhouse.c 中的 static void call_extension_script(const char *cmd, int argc, char *argv[]) 函数中通过 Linux 系统的进程调用函数 execvp 来实现。

c 复制代码
static void call_extension_script(const char *cmd, int argc, char *argv[])
{
	const struct extension *ext;
	char new_path[PATH_MAX];
	char script[64];

	if (argc < 3)
		return;

	for (ext = extensions; ext->cmd; ext++) {
		if (strcmp(ext->cmd, cmd) != 0 ||
		    strcmp(ext->subcmd, argv[2]) != 0)
			continue;

		snprintf(new_path, sizeof(new_path), "PATH=%s:%s:%s",
			dirname(argv[0]), JAILHOUSE_EXEC_DIR,
			getenv("PATH") ? : "");
		putenv(new_path);

		snprintf(script, sizeof(script), "jailhouse-%s-%s",
			 cmd, ext->subcmd);
		execvp(script, &argv[2]);

		perror("execvp");
		exit(1);
	}
}

jailhouse-gcov-extract

Jailhouse 支持在运行时收集代码覆盖率信息(gcov)。gcov(GNU Coverage) 是一个测试代码覆盖率的工具,工作原理是基于代码插桩(code instrumentation)技术。在编译源代码时,通过添加 -ftest-coverage-fprofile-arcs 选项这两个 GCC 编译器选项,编译器会在生成的可执行文件中插入特殊的监控代码。这些监控代码将跟踪源代码中的每个执行路径,并记录下来它们被执行的次数。

  1. 为了使用该特性,必须新建 include/jailhouse/config.h 文件,并在文件中将 CONFIG_JAILHOUSE_GCOV 定义为 1
  2. 首先正常运行一个 Jailhouse 虚拟机,最后 sudo jailhouse disable 禁用 Jailhouse,但是不要卸载 jailhouse.ko
  3. 执行 ./tools/jailhouse-gcov-extract 提取数据生成 *.gcda 文件
  4. 使用其他上层工具(例如,lcov)来处理生成 *.gcda 文件即可

配置文件

在 Jailhouse 中,所有的 Cell 的硬件资源必须是静态分配的。因此,在启动 Cell 之前,我们必须要有一个配置文件,这个配置文件告诉 Jailhouse 每个 Cell 可以使用哪些硬件资源。

Jailhouse 采用以 .cell 为扩展名的二进制文件作为配置文件,而 .cell 文件是由一个包含一个 C 语言结构体变量来描述硬件资源的 .c 文件生成的。而我们需要根据 Jailhouse 给出的一些示例(configs)目录下书写自己的 .c 文件,并进一步编译为 .cell 文件来使用。

  1. 使用命令 jailhouse config check [-h] SYSCONFIG [CELLCONFIG [CELLCONFIG ...]] 可以检查我们的配置文件

SYSCONFIG

SYSCONFIG(全局配置文件)就是 Root Cell 对应的配置文件,它告诉 Jailhouse 当前系统下所有可用的资源有哪些。当我们执行 jailhouse enable SYSCONFIG 时,Jailhouse 就会将 SYSCONFIG 中描述符的资源放到 Root Cell 中。

  1. 对于 x86 架构,Jailhouse 提供了 sudo jailhouse hardware check 命令来自动检测当前系统配置,并提供了 sudo jailhouse config create sysconfig.csysconfig.c 名字可自定义) 来自动生成针对当前系统的配置文件。
  2. 注意,通过以上命令生成的 sysconfig.c 文件的用户是 root,我们可以使用命令 sudo chown zcs:zcs sysconfig.c 更改为自己的用户名和用户组,这样再后续编辑是比较方便。

我们需要将生成的 sysconfig.c 文件放在 Jailhouse 源码的 configs/x86/ 目录中,重新构建 Jailhouse 时,构建系统会将自动为其中的 .c 生成一个相应的 .cell 文件。

CELLCONFIG

CELLCONFIG(CELL 配置文件)就是 Non-root Cell 使用的配置文件,定义了 Non-root Cell 可以使用的物理资源,当我们创建 Non-root Cell 时,Jailhouse 就会根据 Non-root Cell 的配置文件,从全局配置文件(Root Cell)中分离出指定的资源。

对于 Non-root Cell 的配置文件需要参考 configs 目录下对应架构下的 .c 文件来手动创建。同样,写好的 .c 文件需要放到 configs/x86/ 目录中,重新构建 Jailhouse 时,构建系统会将自动为其中的 .c 生成一个相应的 .cell 文件。

虚拟机固件

Jailhouse 虚拟机中运行的系统镜像或者是一个裸机程序固件被称为 inmate,目前可以是 Linux、FreeRTOS、ERIKA3 RTOS、Zephyr 其中之一。对于 Linux,Jailhouse 无法运行未经修改的 Linux 内核!

参考

  1. https://software-dl.ti.com/processor-sdk-linux/esd/docs/06_03_00_106/linux/Foundational_Components/Virtualization/Jailhouse.html
  2. https://www.elecfans.com/d/2338769.html
  3. https://variwiki.com/index.php?title=Jailhouse_Guide
  4. https://blog.csdn.net/v6543210/article/details/113890847
  5. https://www.21ic.com/a/933932.html
  6. https://blog.csdn.net/v6543210/article/details/118031563
  7. https://www.21ic.com/a/933932.html
相关推荐
A.A呐6 分钟前
【Linux第一章】Linux介绍与指令
linux
Gui林6 分钟前
【GL004】Linux
linux
ö Constancy10 分钟前
Linux 使用gdb调试core文件
linux·c语言·vim
tang_vincent12 分钟前
linux下的spi开发与框架源码分析
linux
xiaozhiwise15 分钟前
Linux ASLR
linux
wellnw16 分钟前
[linux] linux c实现共享内存读写操作
linux·c语言
a_安徒生35 分钟前
linux安装TDengine
linux·数据库·tdengine
追风赶月、41 分钟前
【Linux】线程概念与线程控制
linux·运维·服务器
小字节,大梦想43 分钟前
【Linux】重定向,dup
linux
blessing。。2 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式