lguest 三步曲之三 (源码分析) 收藏
lguest上的guest os启动的过程
根据linux启动流程的分析,在执行到jmp *0xc0100000时,系统将会根据是压缩内核还是未压缩的内核来决定跳转的方向:
(1)如果是未压缩的内核,就直接跳到/kernel/head_32.S的入口开始执行
(2)如果是压缩的内核,就要先解压,整个解压的过程在/boot/compressed/head_32.S中,解压完成后跳到解压内核的起始地址开始执行其实解压后的起始地址,也是/kernel/head_32.S的入口。
因此不管是压缩的内核还是未压缩的内核,都会执行/kernel/head_32.S中的代码。这是可以确定0xc0100000处的第一条代码就是startup_32.
现在我们就从/kernel/head_32.S开始分析。
(1)重新加载boot_gdt_desc和段寄存器的值
(2)清空bss段
(3)把实模式下的boot_params,拷贝到保护模式下的boot_params结构体中
(4)movl pa(boot_params) + NEW_CL_POINTER,%esi // %esi指向了 setup_header.cmd_line_ptr
判断一下命令行参数指针是否为空,如果不为空,就要把命令行的参数拷贝过来,拷贝到boot_command_line数组中。
下面就开始判断,如果定义了CONFIG_PARAVIRT的话,即支持虚拟化对的话,就要选择到底执行哪条内核路径,这里有三条路径:
1 default_entry:这是默认的系统启动路径
2 bad_subarch: 当前内核不支持的启动路径
3当前内核支持的两种虚拟化启动路径:lguest_entry 和 xen_entry
相当于定义了一个内核路径数组,数组名是subarch_entries,元素个数是num_subarch_entries,然后在寻址的时候是这样的:
eax = 0 + eax*4 + pa(subarch_entries)。因为我们分析的是lguest , 因此这里的跳到lguest_entry。
有个很重要的数据结构,在这里必须要介绍一下: boot_params 结构体,它就是传说中的"zero page".
这个数据结构几乎保存了启动过程中所需要的所有的信息,比如屏幕显示信息,hdr,e820返回的内存信息等等,在后面的启动程序中,很多地方都用到了这个数据结构中的参数,
很明显这是个循环复制的一组指令,目的地址就是boot_params, 那么源地址是哪里呢?其实,当跳到startup_32时,%esi还是执行实模式下的数据boot_params。应该豁然开朗了,
实模式下的数据在保护模式下是不可用的,因此要拷贝过来。为了验证esi到底是不是指向boot_params,看下面的代码:
但是,boot_params到底是什么时候被初始化的呢?这也是我们比较关心的一个问题。
大部分的初始化的代码包含在arch\x86\boot\Main.c中
(1)copy_boot_params(); 初始化的boot_params.hdr
(2)detect_memory(void);
初始化了boot_params.e820_map 和boot_params.e820_entries
(3)query_apm_bios(); 初始化了apm_bios_info
(4)query_apm_bios(); 初始化了screen_info
到此,boot_params这个数据结构介绍完了,后面很多代码都会从这个结构体里面取数据。继续分析lguest执行流程lguest_entry.
在arch\x86\lguest\i386_head.S中找到了ENTRY(lguest_entry)
我们把这段代码总整体上来看,就是实现了这么一个操作,%eax = $LHCALL_LGUEST_INIT %ebx=lguest_data 数据结构的物理地址
后面又创建了一个堆栈,然后又调用了一个c函数lguest_init .把以上的信息串联起来,岂不就相当于在汇编里调用c函数的整个准备过程,
先设置好参数(%eax,%ebx),然后设置好堆栈。
LHCALL_LGUEST_INIT是 lguest实现的hypercall 调用号,就想我们熟悉的linux的其他的系统调用,只不过是lguest的系统调用而已。在调用
系统调用前,要把系统调用号保存在eax中,把参数保存在ebx等其他的几个寄存器中。然后.byte 0×0f ,0×01,0xc1就是执行系统调用,相当于
int $0×80,这么做的目的无非就是通知host os,当前运行的是一个guest os.
这里也涉及到一个非常重要的数据结构lguest_data, 分析下这个结构体。
这个数据结构实现了Host和Guest之间进行交流的一种方法
/*G:032 The second method of communicating with the Host is to via "struct
* lguest_data". Once the Guest's initialization hypercall tells the Host where
* this is, the Guest and Host both publish information in it. :*/
详细分析一下每一个数据成员的含义:
(1)irq_enabled
/* 512 == enabled (same as eflags in normal hardware). The Guest
* changes interrupts so often that a hypercall is too slow. */
相当于Host里面的eflags, 512=2^9,即第10位置1,就表示开中断。这么做的原因是:如果通过hypercall 来实现中断的使能的话,太慢了!
(2)DECLARE_BITMAP(blocked_interrupts, LGUEST_IRQS);
定义了一个bitmap,用来做中断屏蔽的???
(3)CR2 :Guest缺页中断时会在CR2中保存一个hypercall,Host在这里写上一次page fault的虚拟地址
(4)time : Host设置的时间
(5)hcall_status[LHCALL_RING_SIZE] LHCALL_RING_SIZE=64
/* Async hypercall ring. Instead of directly making hypercalls, we can
* place them in here for processing the next time the Host wants.
* This batching can be quite efficient. */
/* 0xFF == done (set by Host), 0 == pending (set by Guest). */
并不是每产生一个hypercall,Host就对它进行处理,可以先让这些hypercall 排队,主机在某一个时间来对他们进行处理。0xFF表示Host处理完了所有的pending的hypercall, 0表示有Guest的hypercall在pending.
(6)reserve_mem: 指明给switcher保留的空间的大小(主机初始化)
(7)设置TSC的 频率(Host初始化)
(以下变量guest 在初始化时设置)
(8)noirq_start,noirq_end: 不允许中断的一段指令的范围,即使此时是开中断的;
(9)kernel_address = PAGE_OFFSET
(10)syscall_vec: 系统调用号0×80
下面我们就跳到arch\x86\lguest\boot.c 中的lguest_init()函数来执行。
这个函数主要是对内核的一些敏感操作进行的封装或这说是替换,主要有一下几个方面的封装:
(1)中断相关的操作
(2)cpu指令的封装
(3)页表管理
(4)apic的读写操作
(5)时间相关操作
对以上部分封装之后,接着又执行了下面一系列的操作:
(1)reserve_top_address(lguest_data.reserve_mem);
为Host<->Guest Switcher 保留一段内存空间,这段内存空间的大小由lguest_data.reserve_mem 指定。
(2)lockdep_init()
这个函数分别申请4096个classhash_table和8192个chainhash_table.这两个hashtable到底是用来he干什么的,现在还不清楚?
(3)para_virt_disable_iospace()
禁止所有的非虚拟驱动程序去扫描它所支持的硬件设备,减少启动时间
(4)cpu_detect(&new_cpu_data)
(5)add_preferred_console(“hvc”,0,NULL);
注册hvc这个虚拟终端的驱动
(6)virtio_cons_early_init(early_put_chars);
(7)pm_power_off = lguest_power_off
machine_ops.restart=lguest_restart
(9)i386_start_kernel
剖析 Linux hypervisor KVM 和 Lguest 简介
Linux® 的最重要创新之一就是转变为hypervisor(或运行其他操作系统的操作系统)。现在涌现许多使用 Linux 作为内核的 hypervisor 解决方案。本文探索 hypervisor 背后的原理,以及两个使用 Linux 作为平台的 hypervisor(KVM 和 Lguest)。
hypervisor 之于操作系统类似于操作系统之于进程。它们为执行提供独立的虚拟硬件平台,而虚拟硬件平台反过来又提供对底层机器的虚拟的完整访问。但并不是所有 hypervisor 都是一样的,这是件好事,因为 Linux 就是以灵活性和选择性著称。本文首先简要介绍虚拟化和 hypervisor,然后探索两个基于 Linux 的 hypervisor。
虚拟化和 hypervisor
我们首先花一点时间理解为什么虚拟化很重要,以及 hypervisor 的扮演的角色。(要更多地了解这两个主题,请参见 参考资料)。
在本文中,虚拟化 就是通过某种方式隐藏底层物理硬件的过程,从而让多个操作系统可以透明地使用和共享它。这种架构的另一个更常见的名称是平台虚拟化。在典型的分层架构中,提供平台虚拟化的层称为 hypervisor (有时称为虚拟机管理程序 或 VMM)。来宾操作系统称为虚拟机(VM),因为对这些 VM 而言,硬件是专门针对它们虚拟化的。图 1 简单的展示了这个分层架构。
图 1. 显示常用硬件虚拟化的简单分层架构
平台虚拟化的好处很多。美国环境保护署(EPA)报告的一组有趣的统计数据就证明了其好处。EPA 研究服务器和数据中心的能源效率时发现,实际上服务器只有 5% 的时间是在工作的。在其他时间,服务器都处于 “休眠” 状态。在单个服务器上的虚拟化平台能够改善服务器的利用率,但是减少服务器的数量才是它的最大功用。减少服务器数量意味着减少不动资产、能耗、冷却和管理成本。使用更少的硬件还能提高可靠性。总之,平台虚拟化不仅带来技术优势,还能创造成本和能源优势。
在图 1 中可以看到,hypervisor 是提供底层机器虚拟化的软件层(在某些情况下需要处理器支持)。并不是所有虚拟化解决方案都是一样的,您可以在 参考资料 中了解更多的虚拟化方式。继续讨论进程,操作系统将对机器的底层资源的访问虚拟化为进程。hypervisor 也做一样的事情,但其对象不是进程,而是整个来宾操作系统。
hypervisor 分类
hypervisor 可以划分为两大类。首先是类型 1,这种 hypervisor 是直接运行在物理硬件之上的。其次是类型 2,这种 hypervisor 运行在另一个操作系统(运行在物理硬件之上)中。类型 1 hypervisor 的一个例子是基于内核的虚拟机(KVM —— 它本身是一个基于操作系统的 hypervisor)。类型 2 hypervisor 包括 QEMU 和 WINE。
hypervisor 的构成
hypervisor(不管是什么类型)仅是一个从其来宾操作系统抽象机器硬件的分层应用程序。通过这种方式,每个来宾操作系统看到的仅是一个 VM 而不是真实的硬件机器。我们大致看一下 hypervisor 的内部组成,以及它在 VM(来宾操作系统)上的表示。
在较高级别上,hypervisor 需要少量设施启动来宾操作系统:一个需要驱动的内核映像、一个配置(比如 IP 地址和所需的内存量)、一个磁盘盒一个网络设备。磁盘和网络设备通常映射到机器的物理磁盘和网络设备(如图 2 所示)。最后,需要使用一组来宾操作系统工具启动和管理来宾操作系统。
图 2. 在假设 hypervisor 中的最小资源映射
然后,一个简化的 hypervisor 架构实现最后的关键功能,从而使来宾操作系统可以和宿主操作系统同时运行。实现这个功能需要一些特定的要素,如图 3 所示。首先,类似于将用户空间应用程序和内核函数连接起来的系统调用,一个通常可用的虚拟化调用(hapercall,hypervisor 对操作系统进行的系统调用)层允许来宾系统向宿主操作系统发出请求。可以在内核中虚拟化 I/O,或通过来宾操作系统的代码支持它。故障必须由 hypervisor 亲自处理,从而解决实际的故障,或将虚拟设备故障发送给来宾操作系统。hypervisor 还必须处理在来宾操作系统内部发生的异常。(毕竟,来宾操作系统发生的错误仅会停止该系统,而不会影响 hypervisor 或其他来宾操作系统)。hypervisor 的核心要素之一是页映射器,它将硬件指向特定操作系统(来宾或 hypervisor)的页。最后,需要使用一个高级别的调度器在hypervisor和来宾操作系统之间传输控制。
图 3. 简化的基于 Linux 的hypervisor
Linux hypervisor
本文探索两个基于 Linux 的 hypervisor 解决方案。首先是 KVM,它是首个被集成到 Linux 内核的 hypervisor 解决方案,并且实现了完整的虚拟化。其次是 Lguest,这是一个实验 hypervisor,它通过少量的更改提高准虚拟化。
KVM
KVM 针对运行在 x86 硬件硬件上的、驻留在内核中的虚拟化基础结构。KVM 是第一个成为原生 Linux 内核(2.6.20)的一部分的 hypervisor,它是由 Avi Kivity 开发和维护的,现在归 Red Hat 所有。
这个 hypervisor 提供 x86 虚拟化,同时拥有到 PowerPC® 和 IA64 的通道。另外,KVM 最近还添加了对对称多处理(SMP)主机(和来宾)的支持,并且支持企业级特性,比如活动迁移(允许来宾操作系统在物理服务器之间迁移)。
KVM 是作为内核模块实现的,因此 Linux 只要加载该模块就会成为一个hypervisor。KVM 为支持 hypervisor 指令的硬件平台提供完整的虚拟化(比如 Intel® Virtualization Technology [Intel VT] 或 AMD Virtualization [AMD-V] 产品)。KVM 还支持准虚拟化来宾操作系统,包括 Linux 和 Windows®。
这种技术由两个组件实现。第一个是可加载的 KVM 模块,当在 Linux 内核安装该模块之后,它就可以管理虚拟化硬件,并通过 /proc 文件系统公开其功能(见图 4)。第二个组件用于 PC 平台模拟,它是由修改版 QEMU 提供的。QEMU 作为用户空间进程执行,并且在来宾操作系统请求方面与内核协调。
图 4. KVM hypervisor 的高级别视图
当新的操作系统在 KVM 上启动时(通过一个称为 kvm 的实用程序),它就成为宿主操作系统的一个进程,因此就可以像其他进程一样调度它。但与传统的 Linux 进程不一样,来宾操作系统被 hypervisor 标识为处于 “来宾” 模式(独立于内核和用户模式)。
每个来宾操作系统都是通过 /dev/kvm 设备映射的,它们拥有自己的虚拟地址空间,该空间映射到主机内核的物理地址空间。如前所述,KVM 使用底层硬件的虚拟化支持来提供完整的(原生)虚拟化。I/O 请求通过主机内核映射到在主机上(hypervisor)执行的 QEMU 进程。
KVM 在 Linux 环境中以主机的方式运行,不过只要底层硬件虚拟化支持,它就能够支持大量的来宾操作系统。您可以在 参考资料 部分找一个到受支持的来宾操作系统的列表。
Lguest(以前的 lhype)
Lguest hypervisor 由澳大利亚 IBM 的 Rusty Russell 开发,它采用完全不同的方式实现虚拟化。Lguest 并没有为运行任意操作系统提供完整的虚拟化支持,而是为支持 x86 的 Linux 来宾操作系统(也称为Linux-on-Linux 虚拟化)提供轻量级准虚拟化。这意味着来宾操作系统知道自己正在被虚拟化,并且这同时还会改进性能。但是,Lguest 不需要 QEMU 提供平台虚拟化(像在 KVM 中一样)来改进性能。使用 Lguest 这种方法还减少了总代码需求,仅需在来宾操作系统和宿主操作系统中使用一个瘦层。现在,我们探索这些变化,并查看 Lguest 环境的高级别架构。
如图 5 所示,来宾操作系统包含一个 Lguest 代码瘦层(根据定义,就是准虚拟化)。这段代码提供许多服务。在最高的级别,有一些代码可以决定正在启动的内核是否被虚拟化。此外,还有一个通过虚拟化调用将特权操作发送给宿主操作系统的抽象层(通过 paravirt_ops 实现)。例如,来宾操作系统不能禁用中断,以使这些请求在宿主操作系统中执行。您还可找到一个为来宾操作系统实现设备抽象的总线,以及一组实现控制台、虚拟块驱动器和虚拟网络驱动器(允许与其他来宾通信)的简单驱动器。
图 5. 实现 x86 准虚拟化的 Lguest 的架构
内核部分被实现为可加载的模块,即lg.ko。这个模块包含来宾操作系统通向宿主内核的接口。第一个组件是切换器,它实现一种方法,让来宾操作系统在执行时根据上下文进行切换。这个模块还实现 /proc 文件系统代码(针对 /dev/lguest),该代码实现到内核和驱动器(包括虚拟化调用)的用户空间接口。还有一些代码通过使用影子页表(shadow page-table)和管理 x86 区段来提供内存映射。
最后,内核中的 Documentation 子目录包含启动实用程序(lguest),用于启动新的来宾操作系统实例。这个文件负责两项任务,即使用和记录。
Lguest 从 2.6.23(2007 年 10 月)开始就成为主流内核,并且由 Rusty Russell 开发和维护。它大约包含 5000 行源代码,包括用户空间实用程序。尽管 Lguest 很简单(据说是这样的),但它能提供真正的准虚拟化。不过简单性往往与局限性相随。例如,Lguest 仅虚拟化其他支持 Lguest 的来宾操作系统,并且目前仅能用于 x86 架构。尽管存在这些限制,Lguest 仍然提供一种有趣的虚拟化方式,并且对任何希望研究 Rusty 的代码的人员公开。
Linux hypervisor 的益处
使用 Linux 作为内核开发 hypervisor 有实实在在的好处。最明显的是,以 Linux 为基础开发 hypervisor 受益于稳步前进的 Linux,以及为改进 Linux 投入的大量工作。从典型的优化、bug 修复、调度和内存管理创新到支持不同处理器架构,Linux 都是一个不断进步的平台(引自 Salisbury 市的 John 的 “站在巨人的肩膀上” 一文)。
不久前已经证明,通过向 KVM 添加一个内核模块,就可以将 Linux 内核转变为 hypervisor。Lguest 进一步改进了这种方法,并且通过受限制的准虚拟化进一步简化了该解决方案。
使用 Linux 作为平台的另一个奇特好处是,除了可以将该平台用作 hypervisor 之外,您还可以将其用作操作系统。因此,除了可以在 Linux hypervisor 上运行多个来宾操作系统之外,您还可以在该级别上运行其他传统的应用程序。所以,不必担心带有新的应用编程接口(API)的新平台,因为您拥有用于开发应用程序的标准 Linux 平台(如果需要监控应用程序或 hypervisor)。标准协议(TCP/IP)和其他有用的应用程序(Web 服务器)和来宾操作系统都是可用的。回顾一下讨论 KVM 时的 图 4:除了来宾操作系统之外,还使用了修改了 KVM 的 QEMU。这是一个标准进程,并展示了 Linux 作为 hypervisor 的强大之处。KVM 在平台虚拟化中利用 QEMU,并使用 Linux 作为hypervisor,因此实现了这个构思,即让来宾操作系统能够和其他 Linux 应用程序协调执行。
结束语
在 hypervisor 开发的过程中,hypervisor 就是新开辟的战场。3 年以前,操作系统是战场的主线,并且控制了一小部分据点。然而,今天战场已转移到 hypervisor,并且 Linux 担任一个明确的角色。
但是,也有声音反对使用 Linux 作为 hypervisor,并且最剧烈的批评来自于夸夸其谈的空论。在数年以前这种情况曾出现在嵌入式领域。今天,作为嵌入式操作系统的 Linux 已是不曾止步的强者。但是我们也有对付批评的办法,通过一些架构上的改进可以让 Linux 成为最强大、最普遍和更灵活的操作系统。(责任编辑:A6)