首页 1.2ARM hypervisor的cpu虚拟化 - vneo

1.2ARM hypervisor的cpu虚拟化 - vneo

举报
开通vip

1.2ARM hypervisor的cpu虚拟化 - vneo1.2ARM hypervisor的cpu虚拟化 - vneo 1ARM hypervisor ............................................................................................................................. 1 1.1ARM hypervisor的启动原理...............................................................

1.2ARM hypervisor的cpu虚拟化 - vneo
1.2ARM hypervisor的cpu虚拟化 - vneo 1ARM hypervisor ............................................................................................................................. 1 1.1ARM hypervisor的启动原理.............................................................................................. 1 1.1.1设置页目录和页表 .................................................................................................. 2 1.1.2初始化堆栈 .............................................................................................................. 3 1.1.3平台初始化 .............................................................................................................. 4 1.1.4内存初始化 .............................................................................................................. 4 1.1.5中断初始化 .............................................................................................................. 5 1.2ARM hypervisor的cpu虚拟化 .......................................................................................... 5 1.2.1有关cpu特权级的虚拟化 ...................................................................................... 5 1.2.2有关vcpu的调度 .................................................................................................... 6 1.2.3DomainU的创建和加载 .......................................................................................... 8 1.3ARM hypervisor的内存管理虚拟化 .................................................................................. 9 1.3.1ARM虚拟化中的地址空间的划分 ......................................................................... 9 1.3.2hypervisor中的内存管理 ....................................................................................... 11 1.3.3关于GuestOS的内存分配 ................................................................................... 13 1.4ARM hypervisor的中断虚拟化........................................................................................ 15 1.4.1ARM中断的原理 ................................................................................................... 15 1.4.2hypervisor在中断处理过程中与DomainU的交互 ............................................. 16 1.5ARM hypervisor的设备虚拟化........................................................................................ 19 1.5.1hypervisor的设备初始化 ....................................................................................... 19 1.5.2goldfish部分驱动在hypervisor中的实现 ............................................................ 20 1.6ARM hypervisor的控制台................................................................................................ 21 1.6.1控制台的实现原理 ................................................................................................ 21 1.6.2 控制台的各个命令 ............................................................................................... 22 2.Android虚拟化实现 ................................................................................................................... 23 2.1 CPU及平台虚拟化 .......................................................................................................... 23 2.1.1启动初始化过程的虚拟化 .................................................................................... 23 2.1.2平台初始化过程的虚拟化 .................................................................................... 24 2.1.3cpu虚拟化的主要工作 .......................................................................................... 24 2.2Android内存虚拟化 ......................................................................................................... 26 2.3 Android中断虚拟化......................................................................................................... 26 2.4 Android设备驱动虚拟化 ................................................................................................. 26 2.5其他更改代码 ................................................................................................................... 26 1ARM hypervisor 1.1ARM hypervisor的启动原理 开发平台为linux上运行的Android模拟器,模拟器虚拟的硬件平台是ARM架构下的 goldfish平台,这个平台是一个虚拟的平台,意思就是说不存在真实的goldfish平台的ARM 开发板,goldfish平台是专为开发者 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 的。当然hypervisor就是在模拟器上运行的,如果 仅仅是在模拟器上启动ARM的hypervisor,只需要用模拟器运行一个叫做kernel-qemu的文 件,这个文件就是hypervisor的内核文件。在我们的ARM实验平台上,cpu是单核的,内存为128M。 正常在ARM开发板上启动操作系统是需要Bootloader的,较为普遍的Bootloader是U-boot,而在模拟器上运行时实际上是模拟器自身完成了Bootloader的工作,于是在Android模拟器上启动ARM Hypervisor是不需要Bootloader的。模拟器加载kernel的时候直接将kernel加载到物理内存的0x10000处,然后直接跳到内核的第一条语句执行。 内核启动初始化主要做了这样几个工作:设置页目录和页表、初始化堆栈、平台初始化、内存初始化、中断初始化。 1.1.1设置页目录和页表 在刚启动的时候设置页目录和页表是为了hypervisor切换到页式寻址做的准备工作,这一部分工作主要是在xen/arch/arm/arch_goldfish/start.S这个文件中完成的。实际上刚开始的时候hypervisor仅仅只是设置了页目录,因为在ARM架构下有一种内存映射模式叫做块映射(section),只需要页目录就可以完成内存映射,如下图所示: 高12位页目录index低20位页目录index 虚拟地址 31190 页目录物理地址+ 01 31-20位10 11 图1-1-1 ARM的内存块映射 在初始化的过程中,hypervisor将0xFF000000到0xFF200000的虚拟地址空间映射到了0x0到0x200000的物理内存空间上,访问权限是内核态可读可写,用户态不可读不可写,这样做之后hypervisor拥有特权可以通过高端虚拟地址访问到物理内存前2M。同时,hypervisor将0x0到0x6000000的虚拟地址空间映射到了0x0到0x6000000的物理内存空间上,访问权限是内核态可读可写,用户态不可读不可写,这样做是为了让hypervisor能够暂时通过低端虚拟地址的访问到所有的物理内存(实际上在启动Domain之后,这一段的内存块映射会被清除)。另外hypervisor还将0xF0000000到0xF0800000的虚拟地址空间映射到了0xFF000000到0xFF800000的物理内存空间上,访问权限是内核态可读可写,用户态可读可写,在ARM平台上,访问I/O和访问内存一样是通过内存地址来访问的,而在goldfish平台上,0xFF000000到0xFF800000是设备I/O端口所对应的物理地址,于是这段映射就是I/O端口的映射。 另外hypervisor的页目录是存放在物理地址的0xc000处,因为ARM的页式寻址的方式与x86略有不同,ARM系统的一张页目录表的大小是16k,所以在hypervisor启动之后页目录表占用了物理内存0xc000到0x10000的空间。 物理内存 Hypervisor页目录内核 0x00xc0000x6000000x10000 图1-1-2 物理内存分布 最后,当以上的匹配都做完之后,hypervisor将物理内存地址0xc000装入ARM协处理器cp15的寄存器c2(可以认为等同于x86下的cr3)并启用mmu,之后的内存访问就都是建立在页式寻址的基础上了。 1.1.2初始化堆栈 ARM体系结构拥有7种运行模式,在ARM程序的运行过程中会在这7种模式中切换,在ARM架构下一共有6个堆栈指针寄存器,在7种模式中,除了用户模式和系统模式供公用一个堆栈指针寄存器外,其它5种模式每个模式都单独对应自己的一个堆栈指针寄存器: 运行模式 堆栈指针寄存器 用户 R13 系统 管理 R13_svc 中止 R13_abt 未定义 R13_und 中断 R13_irq 快中断 R13_fiq 表1-1-1 运行模式对应的对战指针寄存器 当ARM程序运行时如果发生了模式切换则对应的堆栈也将被切换,比如说当程序运行在用户模式时发生了中断,这个时候模式切换到中断模式,则堆栈指针寄存器从R13变成R13_irq,于是系统之后用到的会是另一个堆栈,这样用户模式下的堆栈在中断返回之前会得到完好的保存,而中断处理过程会完全建立在中断模式所对应的堆栈上。几乎每个模式都拥有自己专有的堆栈,这样保证了模式之间的隔离。 Hypervisor在初始化页目录之后便开始在内存中分别为管理模式、中止模式、未定义模式和中断模式预留了4k的空间,然后将4个堆栈指针分别指向四个堆栈的栈顶,这部分的工作也是在xen/arch/arm/arch_goldfish/start.S这个文件中完成的。之所以没有设定用户模式下的堆栈,原因有两个:第一是因为在ARM架构下要设定某个模式所独有的寄存器首先需要进入那个模式,但是如果说系统进入了用户模式的话是不能够主动的切换到其它模式的,但是我们的hypervisor又是必须运行在管理模式,于是这样做的话势必会妨碍到我们hypervisor初始化得工作;第二是因为在ARM虚拟化中用户模式实际上是需要留给DomainU去使用的,所以只有当hypervisor为DomainU们在虚拟化环境下运行做好准备工作并准备开始调度它们时才需要为它所选择调度的那个DomainU设置用户模式下的堆栈(有关调度以及模式和特权级在hypervisor和DomainU之间的分配会在cpu调度中给与详细的说明)。 1.1.3平台初始化 平台初始化的工作是在xen/arch/arm/arch-goldfish/platform.c文件中完成的,在goldfish_platform_setup 函数 excel方差函数excelsd函数已知函数     2 f x m x mx m      2 1 4 2拉格朗日函数pdf函数公式下载 中hypervisor完成了系统内存的初始化、串口初始化、irq的初始化以及时钟的初始化: 1) 系统内存的初始化 系统内存的初始化比较简单,主要是记录一下物理内存的起始位置和大小。实际上在ARM架构下是用norflash来担当内存(RAM)的,而norflash是以bank为单位来划分的,而每个bank有自己对应的物理内存起始地址,而诸如I/O设备之类的其实都和norflash的地位相同都拥有自己的物理内存起始地址,所以系统在初始化的时候需要用bank数据结构记录每个bank的norflash的起始物理内存地址和大小。而我们简单起见设置单个bank的大小为96M的norflash作为内存,其实物理地址为0,这样看起来就和x86上的物理内存比较相似了。 2) 串口初始化 串口初始化主要是为串口注册了一个输出的驱动,同时也设置了串口输出的波特率 3) irq的初始化。 主要为所有的irq设置一个总的irq处理函数level_irq_handler,当中断处理程序进入这个函数后再根据具体的irq号调用相应的irq处理函数。 4) 时钟的初始 为time的irq注册一个处理函数goldfish_timer_interrupt,该函数主要功能是更新时间和产生一个time的softirq,而这个softirq将直接导致调度、upcall等等事件,具体的过程的在cpu虚拟化和中断虚拟化中介绍。 1.1.4内存初始化 Hypervisor在内存初始化中干的工作主要是为hypervisor和DomainU准备一个大致的物理内存分配方式,主要在xen/arch/arm/xen/xensetup.c文件中的memory_setup函数中实现的。Hypervisor首先物理内存中kernel结束的位置建立了bitmap,然后再物理内存2M的地方建立了和物理页一一对应的page_info结构体数组,接着初始化了内核中堆指针数组,并将2M之后空闲的物理页归入到属于DOM的堆中,最后hypervisor将bitmap之后以及2M以前的空闲物理页归入到属于内核的堆中,最后的物理内存分布图如下图所示: Kernel Page_infoHypervisorDomainU bitmap页目录heap数据结构内核heap0x00xc0000x100000x2000000x6000000 图1-1-3 物理内存分布图 其中,bitmap中的每一个bit对应一个物理页,每次hypervisor需要分配或者释放某个物理页时都会修改相应的bit位。 每个Page_info数据结构对应一个物理页,物理页的基本信息都可以从这个数据结构中获取。 Hypervisor中的heap是一组指向空闲物理页对应的Page_info数据结构的指针,指向与空闲内存相关的链表,比如heap[x][y],x代表的是域(属于hypervisor还是DomainU),y代表的是每一块内存区域的大小,那么heap[x][y]链表上的节点对应的是一系列可供x域使用 的连续物理内存块,其中每一块的大小为4K*(2^y)。且每一块必须按4K*(2^y)对齐。图1-1-3中的kernel heap就是物理内存中可以供hypervisor分配使用的部分,这一部分所对应的空闲连续物理内存块也会放在hypervisor所拥有的相应的heap链表中。 1.1.5中断初始化 中断初始化的代码主要集中在xen/arch/arm/xen/entry.S中,主要负责把中断向量表以及后续的部分通用的保存现场的中断处理程序的可执行程序拷贝到物理内存从0x0开始的低端位置。在ARM架构下,默认设置的情况下物理中断来临的时候会首先去跳到物理内存0x0处寻找中断向量表,所以需要把中断向量表放到物理地址0x0这个地方,于是最终内存的分布如下图: Kernel Page_infoHypervisorDomain bitmap页目录中断表heap数据结构内核heap0x00xc0000x100000x2000000x6000000 图1-1-4 物理内存分布 1.2ARM hypervisor的cpu虚拟化 1.2.1有关cpu特权级的虚拟化 在x86体系结构中,cpu拥有四个特权级,所以在虚拟化中便可以将hypervisor放在最高特权级,GuestOS内核放在次高特权级,而用户程序则放在最低的特权级。然而在ARM体系结构中,虽然有7种模式,但是实际上cpu的特权级只有两个,用户模式是处于低的特权级,而除了用户模式以外的其它6种模式都是处于高的特权级,也就是说用户模式以外的模式都可以进行系统的特权操作(比如说读写协处理器的寄存器),而在用户模式则不行。 由于特权级分布与x86不同,ARM虚拟化中的cpu虚拟化采用的方式是将hypervisor置于高特权级下,也就是hypervisor在管理模式、中止模式、中断模式等6种特权模式下运行,而GuestOS内核与GuestOS用户程序则统统都运行在非特权的用户模式下,而实现的GuestOS内核和用户程序在同一特权级下共存的方法是在GuestOS中设定一个虚拟的特权级,即假设GuestOS的内核是运行在一个较高的特权级上。在hypervisor中会在vcpu数据结构中记录对应的vcpu上运行的程序的虚拟特权级(内核态或者用户态),存放在vcpu数据结构中一个叫做vcpu_guest_context的结构体中,这样hypervisor就可以通过查阅vcpu数据结构就可以得知每个vcpu上当前运行程序的虚拟特权级,于是便可以据此来赋予GuestOS内核程序与用户程序不一样的功能,具体可以参见例如像xen/arch/arm/xen/entry.S中ENTRY(restore)下的代码。 为了保证hypervisor中存贮的每个vcpu上当前运行程序的虚拟特权级的有效性,在GuestOS中运行的程序每次想要从内核态切换台用户态时,都会在真正切换之前向hypervisor发送一个请求(hypercall)作为切换虚拟特权级的申请,当hypervisor接收到这个申请后便会修改相应的vcpu数据结构中的虚拟特权级的值(具体的hypercall处理程序在xen/arch/arm/xen/hypercalls.S的ENTRY(do_restore_guest_context)下的代码中),同理,当用 户态切换到内核态时也需要发送类似的请求。这样hypervisor就能保证了解正确的虚拟特权级,这样当DomainU被硬件中断打断进入hypervisor时hypervisor便能知道DomainU是在哪个虚拟特权下被打断的,于是可以据此进行正确的操作,当然这个详细的会在中断虚拟化中说到。 SyscallGuestOS 用户层 处于非特权模式用户模式ret_to_userGuestOS 内核层 返回GuestOS用户态 进入GuestOS内核态 被hypervisor截获ARM Hypervisor Hypercall修改当前vcpu虚拟特权级信息处于特权模式,管理模 式,中止模式等 图1-2-1 cpu特权级分层 尽管为GuestOS的内核程序分配了一个虚拟的内核层,但实际上由于GuestOS的内核程序还是运行在真实机器的用户模式,所以还是无法直接执行特权操作,执行特权操作的代码需替换为hypercall,这样做只是为了区别GuestOS的内核程序和用户程序。 1.2.2有关vcpu的调度 在模拟器当中我们仅仅只设置了一个物理的cpu,而在每个GuestOS中,也设置最大的vcpu个数不能超过一个,所以说hypervisor在调度vcpu的时候就相当于在调度DomainU本身。调度主要在xen/common/schedule.c文件中的__enter_scheduler函数中执行。 Hypervisor为每个物理cpu(在这里只有一个)保留了一个schedule_data的数据结构,这个数据结构的定义可以在xen/include/xen/sched-if.h中找到,它主要记录了当前物理cpu上运行的vcpu以及一个很重要的调度时钟s_timer。实际上每次调度的触发过程都是首先硬件发生时钟中断,时钟中断处理程序设置一个time的softirq(软中断),软中断处理程序再检查s_time的到期时间(也就是数据结构中的一个叫做expires的变量)是否小于当前的真实时间,如果小于则启动调度程序,并更新调度时钟的到期时间。 硬件发生设置时钟 时钟中断软中断 软中断处理程序 返回被中断程序Ns_time到期 Y 调度程序 图1-2-2 hypervisor中的调度触发过程 在schedule_data数据结构中有一个叫做sched_priv的指针指向了一块包含每个物理cpu运行时的信息的内存区域,信息中包含了当前物理cpu上运行的vcpu的一个队列,当调度时钟到期的时候,hypervisor便会从这个队列中取一个vcpu放到真实的cpu上运行。在开始调度之前,hypervisor会创建一个idle DomainU,为这个Domain分配一个vcpu并把这个v它加入到唯一的物理cpu上的vcpu队列上。实际上idle DomainU的vcpu不会运行任何的程序,当不存在任何其它DomainU的vcpu可供调度时才会调度这个vcpu,结果就是不会干任何的事情。 在vcpu数据结构中的arch结构体中存储了一个叫做guest_context的结构体,这个结构体用来存储该vcpu上当前运行的程序的信息。 guest_context结构体中的user_regs本身可以用来记录用户程序开始运行时的所有寄存器的值,包括从r0到r15以及cpsr状态寄存器,并不需要保存spsr寄存器的值,因为所有vcpu上的程序(GuestOS中的内核和用户程序)都是运行在用户模式的,而在用户模式下是没有专属的spsr寄存器的(spsr寄存器是为了在中断来临之时记录被中断程序当前cpsr寄存器的值,从而让中断处理程序能够准确的知道被中断程序所处的状态和模式,而中断处理程序又必定是运行在特权模式下的,所以非特权模式是不需要spsr寄存器的),实际上,user_regs在hypervisor中是用来保存hypervisor中准备调度某个GuestOS的内核线程的上下文,当然如果还原这段程序的上下文实际上也就等同了还原某个GuestOS的上下文。 sys_regs记录了当前在在每个vcpu上运行的程序的模式(虚拟特权级:内核级别和用户级别),还记录了为该vcpu虚拟的某些的协处理器寄存器的值,比如说像真实的cp15寄存器c3中存放的域的访问权限(这个会在内存虚拟化中说到),另外还记录了一个比较重要的值那就是GuestOS内核态的堆栈指针,因为在ARM体系结构下,7种模式拥有6个不同的堆栈指针寄存器,而显然特权模式与非特权模式会使用不同的堆栈,当操作系统仅仅运行在物理的ARM硬件平台上时,非特权模式切换到特权模式时会自动切换所使用的堆栈指针寄存器所以不用专门在内存中记录特权模式下堆栈的地址,但是在虚拟化的条件下由于GuestOS中内核态和用户态实际上都是处于用户模式的,所以它们会共用一个堆栈指针寄存器,于是hypervisor便需要记录下虚拟内核态的堆栈地址,而GuestOS用户程序想要通过syscall切换到GuestOS内核态时会被hypervisor所截获然后通过upcall(会在中断虚拟化中详细说到)返回到GuestOS的内核态,在这个过程中会从sys_regs中获取虚拟内核态的堆栈指 针并赋值给用户模式的堆栈指针寄存器。 SyscallGuestOS 用户层 共用用户模式 堆栈指针寄存器ret_to_userGuestOS 内核层 返回GuestOS用户态 赋值给用户模式sp 被hypervisor截获ARM Hypervisor 读取虚拟内核态更新虚拟内核态Hypercall使用特权模式堆栈指针堆栈指针堆栈指针寄存器 图1-2-3 GuestOS内核态和用户态堆栈的处理 Event_callback记录了hypervisor需要向对应的GuestOS发送upcall的时候所需要的入口地址。另外对于每一个不是idle DomainU的DomainU都对应一个管理模式下的堆栈,这个堆栈的地址就是存放在之前提到的user_regs中,在每一次hypervisor准备调度某个DomainU时,都会先切换到对应的管理模式堆栈下,这样做的目的是为了可以让不同hypervisor为不同DomainU所保存的上下文现场可以被隔离起来。 总之hypervisor的调度就是在物理cpu上的vcpu运行队列中选择一个vcpu,然后根据vcpu相应的guest_context数据结构中的数据恢复DomainU的上下文。在调度的过程中hypervisor使用的是bvt算法,简单来说就是每个vcpu对应一个虚拟运行时间evt,它可以简单的认为是某个vcpu(DomainU)运行的时间乘上一个权重,每次调度的时候更新当前vcpu的evt然后选择队列中一个拥有最小evt值的vcpu执行。 1.2.3DomainU的创建和加载 在我们所设计的ARM虚拟化模式中,并没有设定像PC上虚拟化中的Domain0这样一个特权的Domain,而是把所有在hypervisor上运行的GuestOS都看作是DomainU,所有的Domain都是平等的,当它们想要执行特权操作时都必须向hypervisor发出申请,在这种情况下我们创建和启动任何一个Domain都基本上是一样的过程。主要代码在xen/common/domain.c文件中的domain_create函数中。 在ARM虚拟化环境下创建一个DomainU和在PC上创建一个DomainU是没有太大的区别的,具体的是由以下的几个步骤完成的: 1)从hypervisor的heap中分配一块内存区域创建一个domain数据结构并为其指定一个domid,初始化与该Domain相关的一些链表。 2)当创建的Domain不是idle DomainU的话,hypervisor会为其初始化事件通道的端口(这个会在终端虚拟化中提到)以及授权表。 3)为该Domain在hypervisor的heap中分配一个物理页作为该Domain和hypervisor以及和其它所有Domain共享的页面,这个页面会记录一些很重要的共享信息,之后会在内存虚拟化中有详细的解释。 4)创建vcpu,在这里我们每个DomainU仅限创建一个vcpu。 5)将该Domain添加到hypervisor中的Domain有关的链表(domain_list)中,在hash表中注册domid。 关于DomainU的加载,由于实验环境是在模拟器下,所以和从nand flash上加载略有 不同,模拟器会在启动的时候把DomainU的kernel的elf文件存放在物理内存(nor flash)的某个位置,于是hypervisor在加载的时候是从内存加载DomainU的。加载的过程在xen/arch/arm/xen/domain_build.c文件中的construct_dom0函数中,主要分为以下几个步骤: 1)鉴别elf文件是否合乎标准,主要是检查DomainU的elf文件中是否有一个名字叫做“__xen_guest”的节,如果存在这么一个节,则代表GuestOS的kernel是专门为hypervisor制定的。 2)计算出DomainU的虚拟地址空间,为其分配物理内存,并建立页目录页表以及基本的地址映射,这部分的详细的原理和实现步骤会在内存虚拟化中描述。 3)向内存中正确的位置拷贝kernel的代码段和数据段,建立一个专门负责启动这个DomainU的线程。 4)将DomainU的vcpu加入到物理cpu的调度队列中。 1.3ARM hypervisor的内存管理虚拟化 总的上来说,ARM hypervisor对内存所进行虚拟化是一个相对静态的虚拟化机制,也就是说某一个GuestOS所拥有的物理内存的大小以及位置是固定的,GuestOS的物理内存在hypervisor看来就是它所管理的一块分配给GuestOS使用的物理地址连续的内存,在这个基础上所进行的内存虚拟化相比PC上而言要简单一些,但是仍然保留了PC上虚拟化中虚拟地址、物理地址和机器地址三种地址的概念。 1.3.1ARM虚拟化中的地址空间的划分 在ARM内存虚拟化中是通过地址空间的隔离来保证hypervisor和GuestOS访问内存权限不同的。大致的划分如下:虚拟地址0xFF000000到0xFF200000是hypervisor的地址空间,0xF0000000到0xF0800000是hypervisor与DomainU共用的I/O地址空间,0xC0000000到0xF0000000是DomainU内核的地址空间,低地址是DomainU用户地址空间。 在hypervisor初始化的时候是拥有0x0到0x6000000的虚拟地址空间的,这段地址映射到物理内存的0到96M,这样hypervisor在一开始的时候能够访问到所有的物理内存,但是当hypervisor开始调度DomainU时,这段地址空间的映射就不复存在了,只留下虚拟地址空间的第一页到物理地址空间的第一页的映射(为了映射中断向量表,具体的在中断虚拟化中解释),而其它的部分都留给GuestOS了。 在开始调度后,真正属于hypervisor内核的地址空间只有0xFF000000到0xFF200000,可以访问物理内存的0到2M,这也是内核所存放的位置。I/O空间是非特权可读可写的,也就是说hypervisor和GuestOS都可以直接对I/O端口进行操作,都可以操纵硬件设备,实际上在我们的ARM虚拟化中,有一部分的设备驱动是放在hypervisor中的,GuestOS需要发出请求才能使用某些设备,而GuestOS本身又拥有部分设备的驱动,于是可以直接访问部分的设备(关于I/O的虚拟或后面会说到)。 虚拟地址空间分布 0xFFFF1000GuestOS虚拟中断向量表0xFFFF0000 0xFFE00000 DMA内存映射空间 0xFFC00000 0xFF200000 Hypervisor内核 0xFF000000 0xFDC00000特权可读可写M2P表0xFD800000 0xFC000000非特权可读特权可读可写M2P表0xFBC00000 0xF0800000 I/O映射空间(非特权可读可写) 0xF0000000 GuestOS内核地址空间 0xC0000000 GuestOS用户地址空间 0x1000真实中断表映射图1-3-1 虚拟地址空间分布 0x0 M2P表就是Machine to Physics表,也就是机器地址转换到物理地址,机器地址是hypervisor看到的物理地址,而物理地址则是GuestOS所看到的物理地址,显然是经过虚拟化过后的物理地址,这个表主要是告诉hypervisor某个物理页在GuestOS看来物理地址是多少。在ARM虚拟化中给两种权限的M2P表都保留了4M的虚拟地址空间,M2P表需要的大小给平台的内存大小有关,每4K物理内存对应一个4字节的表项,一般大小不会超过1M。 DMA区域的访问权限是非特权可读特权可读可写(URO_SRW),当DMA区域的内存作为现实输出的缓存时,GuestOS由于无权写这块内存,于是只能通过hypercall向hypervisor请求显示输出。 在0xFFFF0000处映射的一页虚拟中断向量表是专门提供给GuestOS的,它的访问权限是URO_SRW,每一个DomainU都会在这个位置匹配一个物理页用作虚拟中断的实现。 0xC0000000到0xF0000000是GuestOS内核程序的地址空间,而0xC0000000以下除去第一页的部分都是GuestOS用户程序的地址空间。 这样的地址空间划分是基于这样的一个原则的:在开始调度DomainU之后,每个DomainU的每个进程拥有各自的一个页目录表,而hypervisor所使用的页目录表是当前的DomainU(current domain)的当前运行的进程的页目录表,就是说在从DomainU切换到hypervisor的时候是不会切换页目录的。 1.3.2hypervisor中的内存管理 在初始化完毕之后,hypervisor再想要做内存映射时将不会再使用块(section)映射,因为这种映射虽然简便,但是由于对1M的连续物理内存进行映射所以不够实用,于是采用小页(small)映射,小页映射的原理如下图: 低12位页内偏移高12位页目录index 虚拟地址 3119011中间8位粗页页表index 物理地址页目录粗页页表+ 31-12位1031-20位01 1110 11 图1-3-2 小页映射 由于在小页映射中,32位的虚拟地址只有其中的8位是用来做页表的索引的,所以一张页表并不是想x86平台上那样需要4K,而是只需要1K的大小(8位索引代表最多256个表项,每个表项4字节,总共1K),但是hypervisor每次分配内存的时候一般都是以页(4K)为单位来分配,于是这样会造成物理内存的浪费。为了减少避免这种浪费,有时候会采取让两个小页映射的页表共用一个物理页的措施,在GuestOS的内存映射中都会采用这种 方案 气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载 ,但即使这样做了仍然会有2K内存的浪费,实际上这空闲2K的在GuestOS中还是利用到了,它存储了另一份页表,这个后面会详细说到。 另外,在ARM的内存映射规则中有一点与x86有显著的不同,那就是在ARM的虚拟地址映射中有一个“域”(domain)的概念,这个域的概念是由页目录表项中的4个bit和cp15协处理器中的c3寄存器共同实现的。 4bit代表域号页目录表项 31805 选中一个域 c3寄存器 域15域2域1域0 3140305312 图1-3-3 域的判定 如上图所示,页目录表项中的5-8位4个bit决定了这1M的虚拟地址空间是属于哪个域,而域所对应的域值则是c3寄存器中的两个bit,域值的意义如下表所示: 域值 类型 描述 0b00 No access 访问则报错 0b01 Client 访问则需要检查常规的访问权限 0b10 Reserved 保留 0b11 Manager 访问则不需要检查常规的访问权限 表1-3-1 域值的描述 当调度时切换当前DomainU的时候会更换c3寄存器的值,同时在GuestOS中,若出现了内核态(虚拟内核态)向用户态(虚拟用户态)切换或者相反的情况,也要更换c3的值,在ARM虚拟化中我们设定了4个域(c3寄存器的低8位有效):DOMAIN_HYPERVISOR、DOMAIN_KERNEL、DOMAIN_IO和DOMAIN_USER(见xen/include/asm-arm/cpu-domain.h 中的定义)。虽然有四个域,但实际上只有第一个域是专属hypervisor的,是特权的,GuestOS中无论是内核程序还是用户程序访问这个域的虚拟地址空间都需要检查权限,其它的域,GuestOS的内核程序都可以无需检查权限就访问,但是GuestOS用户程序则需要检查权限,这样通过域的改变就可以实现GuestOS中内核和用户地址空间的隔离。 具体的可以看看xen/arch/arm/xen/entry.S中ENTRY(restore)下的代码,这段代码是为了恢复某个DomainU的现场,在恢复之前,hypervisor会判断当前要恢复的DomainU是处于用户态还是内核态,然后根据判断来设置c3寄存器的值。 域 值 描述 HYPERVISOR 0b01 访问hypervisor空间需检查权限 KERNEL 0b11 访问GuestOS内核空间不需检查权限 IO 0b11 访问IO空间不需检查权限 USER 0b11 访问GuestOS用户空间不需检查权限 表1-3-2 GuestOS内核态c3的域值 从上表可以看出GuestOS的虚拟内核态可以任意访问除了hypervisor专属的地址空间之外的任何虚拟地址空间,所以属于这3个域的虚拟地址空间中所设置的访问权限对GuestOS的内核态来说是无用的。 域 值 描述 HYPERVISOR 0b01 访问hypervisor空间需检查权限 KERNEL 0b01 访问GuestOS内核空间需检查权限 IO 0b01 访问IO空间需检查权限 USER 0b01 访问GuestOS用户空间需检查权限 表1-3-2 GuestOS用户态c3的域值 从上表又可以看出GuestOS的虚拟用户态访问所有域的地址空间都需要检查权限,可以明显看出虽然GuestOS的虚拟用户态和虚拟内核态虽然本质上是处于同一模式下(用户模式),但却拥有不同的访问内存的权限,比如说把属于KERNEL域的虚拟地址空间的访问权限设置为内核态可读可写用户态禁止访问,GuestOS用户态程序访问时会检查权限从而发现权限不够并且报错,但是当GuestOS内核态程序访问时由于不用检查权限于是可以成功访问。所以ARM虚拟化通过域的设置实现了GuestOS中内核态和用户态地址空间的隔离。另 外hypervisor是会共享DomainU当前的c3的域值的,因为进入hypervisor时系统一定是处于特权模式的,拥有最高的特权级,从而无需更改c3的值。 在hypervisor分配内存的时候会从堆中分配,而为了服务虚拟化的实现,hypervisor划分了3种堆,分别是hypervisor、DomainU和DMA的堆,定义在xen/common/page_alloc.c 中,命名为MEMZONE_XEN、MEMZONE_DOM和MEMZONE_DMADOM。MEMZONE_XEN的堆代表着供hypervisor使用的物理内存,MEMZONE_DOM代表着供DomainU使用的物理内存,MEMZONE_DMADOM代表着供DMA使用的内存。在内存初始化的时候,我们将64K到2M之间的空闲内存归到了hypervisor的堆中,将2M之后的空闲内存归到了DMA的堆中,而没有为DomainU的堆分配任何的物理内存,虽然2M之后的空闲内存都归给了DMA的堆,但是在ARM虚拟化的设计中我们实际上是让DomainU使用了3M之后的所有物理内存,所以DMA的堆中应该只有2M到3M之间的空闲物理内存,而又由于DMA有时候需要较多的物理内存,所以为DMA分配内存的时候实际上从hypervisor的堆中分配的。 Hypervisor的内存管理的初始化过程主要在start_xen中调用的paging_init()和consistent_init()函数中。 在paging_init()函数中,hypervisor首先从DMA的堆中分配了一块连续的物理内存(页对齐)用来建立了M2P表(记录真实机器的物理页到GuestOS的物理页的匹配),然后将这些物理页按小页映射匹配到高位虚地址处(0xFBC00000),并将访问权限设置为内核可读可写用户不可读也不可写,所属域设为hypervisor域,这部分占用的物理内存大小是随着机器的物理内存大小的变化而变化(每一个物理页对应一个4字节的表项)。随后hypervisor重新为0-1M的物理内存建立了映射,我们知道在hypervisor刚开始启动的时候,为了能够访问到所有的物理内存,用较为简单的块映射匹配了所有的物理内存,现在hypervisor将开头的1M的物理内存的映射改为小页映射,这也是为了内存的映射更为灵活,实际上在DomainU开始调度的时候,之前所匹配的块映射都将不复存在,hypervisor在低位虚拟地址中仅仅只占用前4K(用于中断向量表),而我们尤为重视这前4K的映射,hypervisor将物理内存的0-4K映射到了虚拟地址的0-4K,并将访问权限设置为了内核可读可写用户只读,所属域设为hypervisor域。最后,hypervisor又将刚分配的M2P表映射到了虚拟地址0xFC000000处,这次的访问权限设置为了内核可读可写用户只读,所属域设为hypervisor域,目的就是为了能够让GuestOS也能够访问这个表。 在consistent_init()函数中,hypervisor主要干了一件事情,就是为DMA分配了页表,DMA所占用的虚拟地址是0xFFC00000到0xFFE00000,于是在这里需要分配两个物理页作为页表,分配来源是hypervisor的堆,另外所属域设为I/O域。之后再初始化驱动的时候会为DMA分配相应的物理页,那时会将访问权限设为内核态可读可写用户态无权访问。 1.3.3关于GuestOS的内存分配 ARM虚拟化中对GuestOS物理内存的分配实行的是静态的分配,也就是说DomainU在启动之前就拥有了hypervisor为它固定分配的物理内存,而且为它分配的物理内存还必须是连续的。就拿第一个DomainU来说,它所拥有的物理内存是物理地址3M-97M这94M内存,在确定后就不会再有变化,而其它的DomainU也不会占用这94M内存。在初始化的过程中,虽然2M之后的空闲物理页都被归到了DMA的堆中,但是实际上3M之后的物理内存DMA都是无法用到的。 之所以要对DomainU进行静态和连续的物理内存分配是因为在ARM虚拟化中使用了简化的内存虚拟化方案,也就是没有采用影子页表的策略,GuestOS在建立映射的时候会直接映射机器地址,然而理论上GuestOS只能够看到物理地址,这就需要机器地址实际上和 物理地址是相同的,于是就需要静态且连续的物理内存(详细的原理会在Android虚拟化中解释)。 Hypervisor针对GuestOS的内存管理主要集中在xen/arch/arm/xen/domain_build.c文件中的construct_dom0函数中。在这里有一点比较重要的是hypervisor在加载DomainU的kernel时候是从内存加载的,因为我们的hypervisor是运行在模拟器上的,模拟器在hypervisor运行之前就已经将DomainU kernel的elf文件放在了物理内存的某个位置,这样hypervisor只需要从物理内存的某个地址去解析elf文件就可以了。我们加载的第一个DomainU的elf就是放在物理内存的93M的位置,大小约是3.2M,在成功加载完这个elf文件后,93M-97M这部分的物理内存就会被清零来给GuestOS使用。 在加载DomainU之前,首先要为每个DomainU都要新建一个页目录,并且将原本hypervisor的页目录的内容拷贝过来,这样之前建立的页表还可以继续使用。在DomainU开始调度的时候,之前为hypervisor专门建立的页目录将不会再用到,当系统进入GuestOS内核虚拟地址空间物理地址 0x61000000xC5E00000 0xC041B000 堆栈 0xC041A000 页目录页表 0xC03E4000 GuestOS启动信息 0xC03E3000 P2M表 0xC03CB000 Ramdisk 0xC03A4000 GuestOS内核执行程 序以及数据 0xC0008000 0x3000000xC0000000 图1-3-4 GuestOS内核虚拟地址空间 Hypervisor的时候实际上用到的是当前DomainU的页目录。然后hypervisor首先根据elf镜像和DomainU被分配的物理内存来为DomainU建立内核地址空间,就拿我们第一个DomainU来说,它的内核地址空间如上图所示: 从上图来看,GuestOS内核的地址空间从0xC0000000到0xC5E00000,这实际上是hypervisor为GuestOS建立好映射的地址空间,hypervisor将物理内存3M到97M顺序的映射到了GuestOS的虚拟地址0xC0000000到0xC5E00000处,这样GuestOS可以通过这段地址空间操作它所拥有的所有物理内存,所以在上图看来在虚拟地址空间上连续的部分在物理地址空间上也是连续的。 从图1-3-4中可以看到内核程序和数据之后ramdisk,ramdisk和内核elf文件一样也是由模拟器事先放在物理内存中的,具体的物理地址是0x800000,也就是8M的位置,hypervisor会将它复制到相应的位置,计算所需的大小。再之后是P2M表,也就是GuestOS物理地址到真实机器地址的对应表,和M2P表是相反的,P2M表是hypervisor为GuestOS赋值的(具体的操作在xen/arch/arm/xen/domain_build.c文件中的setup_m2p_tables函数中),实际上就 是将GuestOS的0到94M的物理地址对应到机器地址的3M到97M,这个表在GuestOS实际上并没有用到。GuestOS启动信息也是hypervisor赋值的,这些信息包括了GuestOS物理内存大小,页目录起始地址(虚拟地址),P2M表起始地址等等,这个启动信息作为一个数据结构写入到GuestOS的内存中,然后hypervisor通过r12寄存器将启动信息起始地址传给GuestOS。页目录页表这个区域包含了为了在内核地址空间映射所有GuestOS物理页所需的页表以及内核的页目录,映射采用小页映射,映射的时候相邻的2M虚拟地址空间所用的页表共用一个物理页,如下图所示: 页目录 第2K+1个表项 第2K个表项 页表2物理页页表1 01K2K4K 图1-3-5 共用物理页的页表 Hypervisor还做了一件很重要的事情就是从hypervisor的堆中分配了一个物理页作为该DomainU和hypervisor以及其它DomainU共享的一个页面,这个页面映射在内核数据的地址空间中(见xen/arch/arm/xen/arch_domain.c文件中的arch_domain_create函数)。 在以上这些事情都完成了以后,hypervisor再需做一件事便可以将这个为DomainU新建的页目录的首地址加载到c2寄存器中,那就是清空低地址中不需要的映射。可以看到在hypervisor初始化的时候映射0M-128M到低端虚拟地址空间0M-128M,此时hypervisor会将除了第一兆以外所有映射都清空,这是为了将低端地址空间留给GuestOS的用户程序,同时保留第一兆的映射是为了匹配中断向量表。 1.4ARM hypervisor的中断虚拟化 1.4.1ARM中断的原理 在ARM中,中断向量表可以匹配在低端虚拟地址(0x0)处,也可以匹配在高端虚拟地址(0xFFFF0000)处,这是由c1寄存器中的第13位决定的,在这里我们设置的是将中断向量表匹配到0x0处。ARM的中断向量表和x86有些许的不同,ARM的中断分7个大类,而中断向量表有8个表项(其中第5个表项被保留)。 其中软中断异常就相当于x86中的INT指令造成的异常,只不过在ARM中是由swi指令引起的,数据中止异常就是在读写内存的出现的错误引起的,像x86中的缺页中断在这里就应该属于数据中止异常,而诸如键盘、鼠标之类的设备发出的中断都属于硬件中断,在进入中断处理程序后再具体的识别是哪个设备的中断。 表项 中断类别 进入的模式 0 复位异常 管理模式 1 未定义指令异常 未定义模式 2 软中断异常(swi) 管理模式 3 指令预取异常 中止模式 4 数据中止异常 中止模式 5 保留 — 6 硬件中断 中断模式 7 快速中断 快中断模式 表1-4-1 ARM的中断向量表 ARM中断处理和x86上另一个显著的不同是ARM程序被打断后会根据中断的类型进入不同的模式,这样由于有些模式有自己独占的寄存器,而且不同的模式会拥有不同的堆栈,于是对于现场的保护就会方便一些。从表1-4-1中可以看出只要发生中断,系统进入的模式都属于特权模式,这也就是说当DomainU被中断时,一定会进入到hypervisor中进行处理。 1.4.2hypervisor在中断处理过程中与DomainU的交互 由于DomainU是一个完整的操作系统,所以一定会有自己的中断处理机制,比如说每个DomainU都会认为自己有一个中断向量表,然而实际上每个中断都是由hypervisor截获并处理的,这样就需要hypervisor与DomainU建立一个交互,这也是中断虚拟化的关键。 从图1-3-1中可以看出虚拟地址空间0x0处匹配的是中断向量表,而在0xFFFF0000处匹配的是GuestOS的虚拟中断向量表,这就是说在一个虚拟地址空间中存在两个中断向量表。在一开始中断来临的时候cpu会跳到0x0处执行,当hypervisor截获这个中断之后判断出这个中断需要交给某个DomainU去处理的时候便在该DomainU与hypervisor的共享页中设置一个标记,然后当hypervisor决定要恢复这个DomainU的现场的时候通过读取这个标记判断出有中断需要该DomainU去处理于是并不直接恢复现场而是跳转到0xFFFF0000处匹配的虚拟中断表中执行,这样在DomainU看来就好像是DomainU的中断向量表自己截获了中断(如图1-4-1),当然即使这样DomainU也不能够完全使用原有的机制去处理这个hypervisor传上来的中断,仍需要修改部分代码。 由于中断事先已经被hypervisor处理过了,所以DomainU不可能通过直接访问硬件来处理中断,必须由hypervisor将处理中断所需的信息传递给DomainU,这就需要由DomainU向hypervisor申请建立一个事件通道(event_channel,实际上就是在DomainU与hypervisor间的共享页面中申请一个内存空间负责记录hypervisor向上传递的中断的状态),然后将这个事件通道与譬如某一个irq的处理函数绑定,这样当hypervisor传递中断到DomainU的时候DomainU就可以通过读取共享页面的信息来判断具体需要调用哪个中断处理函数。而hypervisor向DomainU传递中断的过程也就是发送event(事件)的过程。 在hypervisor向DomainU发送event的时候,有两个数据结构比较关键,一个就是xen/include/public/xen.h文件中的vcpu_info: typedef struct vcpu_info { uint8_t evtchn_upcall_pending; uint8_t evtchn_upcall_mask; unsigned long evtchn_pending_sel; arch_vcpu_info_t arch; vcpu_time_info_t time; } vcpu_info_t; 虚拟地址 0xFFFF1000 GuestOS虚拟中断表 0xFFFF0000 cpu跳转 Hypervisor处理程序 cpu跳转 0x1000 cpu跳转真实中断向量表中断来临 0x0 图1-4-1 中断虚拟化中hypervisor向上传递中断的过程 这个是存放在hypervisor中的数据结构,每个vcpu对应一个。hypervisor通过查询这个数据结构判断需不需要向DomainU发送event,evtchn_upcall_pending这一项代表是否有还未发送的event,evtchn_upcall_mask代表是否屏蔽该DomainU的所有event,意思就是即使有事件需要发送hypervisor也不会给与理会,evtchn_pending_sel与发送事件的类型有关。 另一个关键数据结构是同一个文件中的shared_info,在这里只列出它的部分关键成员: typedef struct shared_info { vcpu_info_t vcpu_info[MAX_VIRT_CPUS]; unsigned long evtchn_pending[sizeof(unsigned long) * 8]; unsigned long evtchn_mask[sizeof(unsigned long) * 8]; ……………………. } shared_info_t 每个DomainU与hypervisor之间的共享页面就会按照这个数据结构来进行解析,其中MAX_VIRT_CPUS的值为1,所以实际上shared_info与vcpu_info是一一对应的,evtchn_pending数组中的每一个bit对应一个event_channel,这个bit为1代表着该DomainU当前在对应的事件通道中有一个未处理的事件,而evtchn_mask对应的bit代表是否屏蔽该事件通道的事件。 下面以时钟中断为例来看看hypervisor是如何来发送event的,首先时钟中断来临,DomainU被打断,进入到hypervisor的真实中断向量表,开始执行irq的routine。Hypervisor识别出irq号,判断出中断类型是时钟中断,在处理时钟中断的过程中,hypervisor首先会更新自己的时间,然后产生一个时钟的softirq。当hypervisor试图去恢复DomainU的现场的时候会发现还有时钟的softirq没有处理,时钟的softirq处理函数会检查t_timer时钟是否超时(t_timer时钟是hypervisor设定的一个虚拟的计时器,计时器到时了就会向当前DomainU发送一个time的事件),若超时了,则找到time事件对应的事件通道,并将这个通道在evtchn_pending数组中对应的bit位置位,将vcpu_info中的evtchn_upcall_pending标记为当前存在未发送的事件,然后更新t_timer的到期时间以便下一次发送event。函数返回的时候发现当前有未发送的事件,于是会调用ENTRY(upcall)(在xen/arch/arm/xen/entry.S中)。在 upcall过程中,hypervisor需要代替DomainU来保存中断现场,实际上中断现场的保存分几个步骤:1)中断来临保存现场到irq模式的堆栈内;2)从irq进入到svc模式后拷贝现场到svc模式堆栈;3) hypervisor读出DomainU内核态堆栈指针,再将自己之前保存的现场拷贝到DomainU的堆栈中去。保存好现场后hypervisor判断出时钟中断是属于irq,于是跳到DomainU虚拟中断表中irq的那一项中,接下来就是DomainU来处理这个事件了。 时钟中断进入真实中设置时钟 断表软中断 保存现场到irq模式堆 栈,接着拷贝到svc模式时钟软中断处理程序堆栈 恢复DomainU现场t_time到期N Y 设置共享页面中的事 件通道 帮助DomainU保存中断将svc堆栈中保存的现场 拷贝到DomainU的堆栈中现场 跳转至DomainU虚拟中 断表irq这一项 图1-4-1发送时钟event的过程 Hypervisor除了要向DomainU发送事件以传递中断之外还需要处理DomainU向它发出的请求,也就是hypercall,在ARM体系架构中,hypercall是由0x82号软中断实现的(见xen/arch/arm/xen/hypercalls.S中的ENTRY(vector_swi)下的代码),下面是关键代码中的一段: ldr r10, [lr, #-4] bic r10, r10, #0xff000000 cmp r10, #HYPERCALL_VECTOR_NO moveq r12, #0x18 movne r12, #0x08 str r12, [sp, #S_CONTEXT] beq process_hypercalls b do_upcall lr寄存器存储了发生软中断的指令的后一条指令的地址,hypercall于是通过这个地址得到发生软中断的32位指令。32位中的低24位代表着软中断号,HYPERCALL_VECTOR_NO的值为0x82,若软中断号等于0x82则代表当前的DomainU发送给了hypervisor一个hypercall的请求。将0x18存放到r12寄存器中代表hypervisor若需要向当前DomainU发送事件的话则通过传递一个虚拟的irq中断来实现(irq中断在中断表中的偏移是0x18)。在确认软中断代 表hypercall后,hypervisor会通过r7寄存器的值来判断具体属于哪个hypercall。 若软中断号不等于0x82,则hypervisor不会将其当作hypercall来处理,而是当作 DomainU中的用户程序向内核发出的系统调用的申请,于是hypervisor会直接将其传递给 DomainU,将0x8存放到r12寄存器中代表将向上传递一个虚拟的软中断(软中断在中断表 中的偏移是0x8),这样DomainU的内核就能正确的处理系统调用。 1.5ARM hypervisor的设备虚拟化 1.5.1hypervisor的设备初始化 原本在x86上的虚拟化中,hypervisor中不会有驱动的,所有的驱动都会放在Domain0中,这样所有对设备的操作最终都会由Domain0去执行,而hypervisor并不会去理会设备操 作的问题,但是在我们ARM虚拟化中由于没有Domain0的概念(所有Domain都是 DomainU),所以不存在这么一个特权的Domain去进行设备操作,于是hypervisor需要承担 原来Domain0所做的工作,也就是需要将设备放到hypervisor中。 也就是说需要在hypervisor中去做设备初始化,ARM平台上设备初始化是由总线初始 化开始的下面的代码来自xen/arch/arm/arch-goldfish/pdev_bus.c中的goldfish_pdev_bus_probe函数中。 ret=request_irq(pdev_bus_irq,goldfish_pdev_bus_interrupt,IRQF_SHARED,"goldfish_pdev_bus", pdev); __raw_writel(PDEV_BUS_OP_INIT, pdev_bus_base + PDEV_BUS_OP); 代码中的request_irq用来将总线中断处理函数绑定到总线中断上,绑定之后调用__ raw_writel函数是向总线设备端口写一个数据,这个数据写入后,总线设备就会产生一个的 irq的中断,总线中断的中断号是1。当hypervisor接收到这个中断后便会随之初始化其它的 设备。 while(1) { uint32_t op = __raw_readl(pdev_bus_base + PDEV_BUS_OP); i++; switch(op) { case PDEV_BUS_OP_DONE: return ret; case PDEV_BUS_OP_REMOVE_DEV: goldfish_pdev_remove(); break; case PDEV_BUS_OP_ADD_DEV: goldfish_new_pdev(); break; } ret = IRQ_HANDLED; } 代码来自于xen/arch/arm/arch-goldfish/pdev_bus.c中的goldfish_pdev_bus_interrupt函数, 当总线中断发生时,总线设备端口中会存放着一些信息来告诉hypervisor是否还有新的设备 要初始化,如果有,便调用goldfish_new_pdev函数。而这个函数同样会从总线设备端口中 读取一些与要初始化的设备相关的信息,比如说设备I/O地址空间的首地址、地址空间的长 度、设备名字、设备中断号等等,得到这些信息后便可以建立所需要的数据结构。 在初始化设备后便会为各个设备的中断绑定中断处理函数,目前为止,在hypervisor中为三个设备绑定了中断处理函数,分别是总线、键盘和帧缓存设备。也就是说DomainU要用到这些设备是必须经过hypervisor的。 1.5.2goldfish部分驱动在hypervisor中的实现 主要说说hypervisor中键盘和帧缓存的驱动,这两个设备是需要供DomainU共享的,主要思路是将真实中断通过事件通道传递给前台的DomainU。 键盘的驱动是在xen/arch/arm/arch-goldfish/goldfish_events.c中的events_probe函数中加载的,这个函数会首先分配一个与设备有关的数据结构,然后将物理I/O地址匹配到hypervisor的虚拟地址空间中,然后最关键的是为键盘中断绑定了中断处理函数。 if(request_irq(edev->irq, events_interrupt, 0, "goldfish-events-keypad", edev) < 0) { xfree(edev); return -EINVAL; } events_interrupt就是处理键盘中断的函数,这个函数首先会从键盘设备的端口中读取键盘中断的各个参数,包括按键专属的按键号、标示当前中断是属于键盘的还是属于触摸屏的信息、标示当前按键的中断是代表按下按键还是松开按键,而如果是作为触摸屏的中断还会读取具体触摸的点在屏幕上的坐标。 在DomainU开始调度后,键盘的中断信号都会传递给在前端的DomainU,传递的方式是通过virq,在hypervisor中会定义几个virq,其中就包括键盘的virq,而改动过后的GuestOS是知道底层存在这么一个virq的,GuestOS在初始化的时候就会向hypervisor发出申请将自己的键盘中断处理函数绑定到这个virq上,有些时候GuestOS也可以直接绑定到真正的irq上,这个以后会说。绑定的过程实际上就是申请一个事件通道,并且在GuestOS中也申请一个irq,然后建立virq到事件通道的转换,以及事件通道到GuestOS中的irq的转换,最后在GuestOS中将键盘中断处理函数绑定到GuestOS的irq上。在这之后hypervisor便可以向DomainU发送键盘中断的信号了通过下面这条语句: send_guest_virq(current, VIRQ_KEYBOARD); 实际上这样做之后再DomainU看来就好像是来了一个真实的硬件中断,中断号为GuestOS中申请的irq的号,当然由于DomainU是不能直接读取硬件的,所以GuestOS中的键盘中断处理函数是要经过修改的,这个放在之后讲GuestOS的时候具体的说。 帧缓存的驱动是在xen/arch/arm/arch-goldfish/goldfishfb.c中的goldfish_fb_probe函数中加载的,帧缓存的作用就是屏幕输出的一个缓冲区,函数首先从端口中读取有关显示屏的参数,主要是显示屏的长和宽,长乘以宽就是像素的个数。然后要做的就是在hypervisor的堆里分配和像素个数相匹配的物理内存,并把这块物理内存映射到虚拟地址的0xFFC00000处,实际上帧缓存DMA的首地址就是0xFFC00000了,之后把这个地址注册到硬件端口,这样以后写屏幕就可以直接写到DMA的地址中了。最后要做的就是和键盘一样将中断处理函数绑定到帧缓存的中断上。 在帧缓存的中断处理函数中,同样也需要想键盘中断处理函数一样向前端的DomainU传递这个中断,同样也是用virq传递的,用的是VIRQ_FB。当帧缓存中断来临的时候,hypervisor会从硬件端口中读取一个状态信息,并通过写DomainU内存的方式将这个状态信息传递给DomainU。在这里虚拟化的方式是GuestOS将不再用去申请DMA的内存空间,而是直接写hypervisor分配好的DMA,当然这需要修改GuestOS的代码。 1.6ARM hypervisor的控制台 1.6.1控制台的实现原理 Hypervisor的控制台的就是在DomainU开始调度前或者DomainU开始调度之后hypervisor中用于输入控制命令的命令行,主要功能是在hypervisor初始化完毕之后供用户输入命令来控制相应的DomainU。控制台主要包括接受输入、命令处理、显示输出三个方面。 首先是接受输入,这一步是在键盘中断的中断处理函数中实现的,在hypervisor中设置了一个缓冲区,每当键盘中断来临的时候就会把这个按键的信息记录到缓存区中(只有当前标示信息表明按键中断来自于松开按键才做这件事)。而每次进入控制台的时候便会清空这个缓冲区,实际上进入控制台的途径是通过softirq,最开始hypervisor初始化完毕之后,在开始调度之前会从xen/common/softirq.c文中的do_softirq函数中进入monitor函数的调用,也就是进入控制台。而在DomainU开始调度的时候,前端被DomainU占据,要想再进入控制台就得通过特殊的按键,键盘中断处理函数接受到特殊按键时就会设置一个softirq,在softirq处理的时候便会进入控制台,这个专用的softirq在console_init函数中初始化。 Hypervisor初始化 进入控制台 调度到idle 调度DomainU特殊键DomainU DomainU运行 图1-6-1 控制台运行 流程 快递问题件怎么处理流程河南自建厂房流程下载关于规范招聘需求审批流程制作流程表下载邮件下载流程设计 当进入控制台后便会从缓冲区中读取字符数据,并通过xen/common/console.c中的runcmd函数识别字符串,将其转换成相应的命令。命令处理之后需要显示输出,hypervisor中在串口显示输出的接口是printk函数,而屏幕显示输出的接口是xen/drivers/char/printf.c 文件中print函数,这个函数负责将字符串输出到屏幕上,它最终调用drawcharbygh函数,这个函数在xen/arch/arm/arch-goldfish/drawchar.c当中: void drawcharbyqh(u8* ch,int dx ,int dy,u32 fgbgr,u32 bgbgr) 这个函数的第一个参数时输出的字符的ASC码,dx和dy代表字符所在方格左上角的像素在整个屏幕中的坐标,实际上这个参数在函数中并没有用到,因为在控制台中输出字符都是顺序的输出的,会有专门的数据结构记录下一个字符坐在的位置,fgbgr和bgbgr分别代表字符的颜色以及字符所在方格的背景色。 在这个文件中还有一些重要的函数,像chartopic函数作用是将字符转换成像素点的集合,scrollup函数负责屏幕的滚屏。 1.6.2 控制台的各个命令 控制台主要用来操控DomainU,目前的主要有以下的一些命令: 1)boot domid 这个命令的主要功能是启动一个DomainU,domid是这个DomainU的id,id从0,1,2开始这样顺序的分配,启动一个DomainU就是在hypervisor为这个DomainU分配具体的数据结构,包括domain和vcpu的数据结构,然后将DomainU的内核加载到内存中,并把该DomainU的状态置为runnable,就是可以被调度的。 运行这个命令主要调用domain_create和construct_dom0这两个函数。 2)start 执行这个命令便可以开始调度DomainU了,在执行这个命令之前,所有的DomainU都是处于一个等待被调度的状态(runnable),执行之后hypervisor从所有可以被调度的DomainU中用BVT调度算法选择一个DomainU执行。执行start之后hypervisor从控制台中退出,调用__enter_scheduler开始调度。 3)list 这个命令用来列出当前存在的DomainU,在hypervisor中有一个domain_list的指针,指向所有当前存在的DomainU对应的数据结构。 4)stop domid 用来使某个DomainU不参与调度,具体的就是将这个DomainU的vcpu的状态设置为offline,做法就是在hypervisor中调用domain_pause_by_systemcontroller函数,该命令不能针对当前的DomainU(current DomainU)。 5)recover domid 和stop命令相对应,将这个DomainU的vcpu的状态恢复成runnable,重新参与调度,具体做法是在hypervisor中调用domain_unpause_by_systemcontroller函数。 6)kill domid 与stop命令的区别在于执行kill命令会释放DomainU在hypervisor中的domain数据结构以及相对应的vcpu的数据结构,这样在hypervisor看来这个DomainU已经不存在了,具体做法是调用函数domain_kill,同样该命令不能针对当前的DomainU 7)cli 清空屏幕上的内容,就是将屏幕对应的帧缓存DMA清空,调用testfb函数。 要实现控制台的功能需要修改hypervisor的部分代码,如下所示: 1.xen/arch/arm/xen/arch_domain.c中的arch_domian_create函数加入了一行: pa_dom_shared_info[d->domain_id] = virt_to_phys(d->shared_info); 这是为了实现每个DomainU的共享页面地址在hypervisor中的备份。 在xen/common/domain.c添加函数find_domain_by_domid 2. 3.在xen/arch/arm/xen/domain_build.c中的setup_shared_info_mapping函数中修改了domain d匹配其他domain的sharepage的相关代码 4.在xen/arch/arm/xen/xensetup.c中的start_xen中的console_init()中添加了console_softirq 的初始化,在xen/include/xen/softirq.h中添加了console_softirq的定义 5.在xen/arch/arm/xen/domain_build.c中的setup_pg_tables函数中注释了zap_low_mappings(l2start),目的是保留GuestOS在低端虚拟地址空间的映射,这样从GuestOS返回到控制台时还可以使用低端的虚拟地址去启动其它的DomainU。 6.在xen/arch/arm/xen/domain_build.c中的setup_pg_tables函数中将x = *((unsigned long *)init_exception_table); *(unsigned long *)pte = x;替换成了memcpy((void*)pte, (void*)init_exception_table, 4*256); 前者的做法会使之后再想访问第一个页表项变得不可能,而后者不会存在这样的问题,因为页表项是用低端虚拟地址访问的。 7.注释掉create_dom0和create_guest_dom函数中的memzero((void *) (image_start - guest_phys_offset), image_size);为了保留DomainU的镜像可供下次启动时使用。 2.Android虚拟化实现 实现Android虚拟化需要更改原来操作系统内核中相关的代码,需要修改的地方很多,在这里只会讲解对于每一类的代码应该如何去修改。代码的修改也就是使上层操作系统能够与底层hypervisor进行交互,再就是替换掉操作系统的特权操作。 2.1 CPU及平台虚拟化 2.1.1启动初始化过程的虚拟化 首先来看看arch/arm/kernel/head.S这个文件该如何修改,这个文件中的汇编程序是Android内核加载到内存中后最开始执行的代码,没有虚拟化的Android执行的时候在这里会做很多事情,包括切换模式、判断处理器类型、创建页目录页表,使能mmu。这些操作有些事特权模式下才能做的,然而Android运行后自始至终都运行在用户模式下,还有些操作是hypervisor之前就已经做过了,所以不需要再重复一遍,所以在虚拟化情形下head.S中的代码都是要被替换掉的,替换之后实际的代码只剩下面这几行: ldr r10, start_info str r12, [r10] ldr sp, __init_sp b start_kernel start_info: .long xen_start_info __init_sp: .long init_thread_union+8192 @ sp .org 0x1000 .global guest_shared_info_base guest_shared_info_base: .fill 0x8000,1,0 @ 4KB * 8 domains 其中xen_start_info是内核中的一个指向启动信息数据结构的指针的存放地址,而这个启动信息数据结构是hypervisor在初始化DomainU时为其赋值的,相当于是hypervisor传递给DomainU的信息,r12寄存器中存放的是这个数据结构的地址,也是hypervisor传递上来的。这改动后head.S只干了两件事情,一个是将启动信息数据结构的地址存放到内核中,二是给堆栈指针赋初始值,然后便跳到start_kernl中执行了。guest_shared_info_base指向的32K的内核内存空间是预留给最多8个DomainU的共享页的匹配空间。 2.1.2平台初始化过程的虚拟化 跳到start_kernel执行后便首先开始平台的初始化,原本运行在真实硬件上的操作系统需要读取硬件端口得到平台的基本数据,由于Android是运行在虚拟化平台上的,现在则需要由hypervisor来将平台初始化的信息传递给Android,这个工作主要是由init/main-xen.c 中start_kernel函数调用的xen_guest_setup函数完成的。xen_guest_setup主要完成了下面几个功能操作: 1)设定CPU的主标示符,原本主标示符是存放在协处理器c0中,要得到它必须要进行协处理器相关操作,但处于用户模式的GuestOS并不具备这样的特权操作的权限,所以虚拟化的过程中需要将这个主标示符事先设置好然后赋值给Android中的全局变量: processor_id = XEN_PROCESSOR_ID; 同理,原本arch/arm/include/asm/cputype.h文件中读取cpuid的宏read_cpuid也需要被修改为直接读取全局变量processor_id。 2)设定Android中的页目录的基地址,在Android启动之前hypervisor已经为它分配了页目录和部分页表的内存,并且已经初始化了部分页目录和页表项,之后hypervisor会将这个基地址(虚拟地址)写入到Android中的一个数据结构中,并通过r12将这个数据结构的首地址传给Android。 3)设定P2M表的首地址,hypervisor之前同样在Android地址空间中为P2M表分配了一块虚存空间并匹配了相应的物理内存,设置好了表中的每一项,同样也是通过Android操作系统中的数据结构将其传递上来。 4)设定Android所拥有的物理内存的物理地址最低的物理页面的页面号,实际上每个GuestOS都会认为自己最低的物理地址首地址是0,只是机器地址首地址是不一样的。由于ARM内存虚拟化是一个静态的虚拟化过程,所以每一个GuestOS所拥有的机器页面必须是 的页面号和总共的页面数就知道GuestOS所拥有的机器内存区连续的,知道最小机器页面 域。所以物理地址到机器地址只需要一个偏移xen_guest_phys_offset的转换。 xen_min_mfn = xen_start_info->min_mfn; xen_guest_phys_offset = pfn_to_mfn(0) << PAGE_SHIFT; 5)设置回调函数的首地址,Android通过hypercall将hypervisor_callback这个函数的地址传给hypervisor。 6)设置Android与hypervisor共享页面的首地址,之前在head-xen.S中有一个叫做guest_shared_info_base的标号下预留了32K的虚存空间,这些空间就是用来匹配DomainU与hypervisor之间的共享页面的,一共有4K*8的虚存空间,于是可以匹配8个DomainU与hypervisor之间的共享页面,其中有一个就是Android与hypervisor之间的共享页面。 完成平台初始化还需要得到与机器相关的信息,由于修改过后的Android删掉了很多汇编的代码,需要在arch/arm/kernel/setup.c中的setup_machine函数中添加从.arch.info.init节中读取goldfish机器平台的信息的代码。 2.1.3cpu虚拟化的主要工作 cpu虚拟化首先要做就是需要把原本针对物理cpu的操作替换掉,这些操作主要集中在arch/arm/mm/proc-arm926-xen.S中的标号arm926_processor_functions下: v5tj_early_abort函数,这个函数是在发生数据中止异常的时候首先进行一个预处理,原本的函数在执行的时候会首先读取协处理器cp15的寄存器c5和c6的值,c5寄存器存放的是和中止异常相关的状态信息,而c6寄存器存放的是和数据中止有关的内存地址,就像x86中的缺页异常的出错地址(虚拟地址)。读取这些寄存器的操作是属于特权操作,用户模式是 不允许执行的,所以在虚拟化得过程中是需要将这些删除或者是转变成hypercall,在这里由于所有的数据中止异常发生后都会首先进入hypervisor,于是hypervisor会在返回Android之前把这两个协处理器寄存器的值读出来并赋值给相应的寄存器r0,r1,这样一来把原来Android中读取协处理器寄存器的操作删除掉,Android还是可以得到了这两个值。 cpu_arm926_proc_fin函数,这个函数是用来刷新缓存的,在原本的函数中需要用到很多的诸如关中断,读写协处理器寄存器的操作,所以这个函数被改写成一个hypercall,直接交给hypervisor去处理。最终调用xen/arch/arm/xen/core-arm926.S文件中的PRIVATE(arm926_flush_kern_cache_all)。 xen_dcache_clean_area函数,改动Android时添加的函数。功能是刷新部分内存地址空间的范围内的缓存,同样也是调用hypercall,最终调用xen/arch/arm/xen/core-arm926.S文件中的PRIVATE(arm926_clean_cache_range)。 xen_switch_mm函数,改动Android时添加的函数。作用是切换页目录,操作涉及到协处理器寄存器c2的读写,所以替换成hypercall,最终调用xen/arch/arm/xen/core-arm926.S 中的PRIVATE(arm926_switch_mm)。 cpu_arm926_set_pte_ext函数,这个函数是用来修改某个页表项(pte)的内容,这个函数被全面改动,需要先根据参数来重新构造一个pte,然后调用hypercall,hypervisor在接收到hypercall之后会判断pte是否存在,再判断Android是否有权限修改这个表项,最后用新的pte替换旧的。 除此之外还有一些零碎的与cpu操作相关的改动的代码,列举在下面: 1.arch/arm/mm/mmu-xen.c中build_mem_type_table函数调用的get_cr函数原本是读取协处理器寄存器c1的值,c1存储的是一些控制位,而实际上这些控制位在hypervisor启动的时候就已经设置好了,并且以后都不会在更改了,所以GuestOS想要得到c1寄存器的值时可以给它一个定值,定义如下: #define XEN_CR 0x0005317F 2. arch/arm/include/asm/cacheflush.h文件中涉及到cache操作的函数: __cpuc_flush_user_all函数,功能是刷新用户控件的缓存,改为调用hypercall,最终执行PRIVATE(arm926_flush_kern_cache_all),实际上刷新了所有的缓存。 __cpuc_flush_user_range函数,原本功能是刷新用户空间一部分的缓存,改为调用hypercall,最终执行PRIVATE(arm926_flush_user_cache_range)。 __cpuc_coherent_kern_range函数,原本功能是刷新连续内核空间的缓存,改为调用hypercall,最终执行ENTRY(arm926_coherent_user_range)。 __cpuc_coherent_user_range函数,原本功能是刷新连续用户空间的缓存,改为调用hypercall,最终执行ENTRY(arm926_coherent_user_range)。 __cpuc_flush_dcache_page函数,原本功能刷新一页地址空间的缓存,改为调用hypercall,最终执行PRIVATE(arm926_flush_kern_dcache_page)。 dmac_inv_range函数,改为调用hypercall,最终执行ENTRY(arm926_dma_inv_range)。 dmac_clean_range函数,改为调用hypercall,最终执行ENTRY(arm926_dma_clean_range) dmac_flush_range函数,改为调用hypercall,最终执行ENTRY(arm926_dma_flush_range)。 3arch/arm/kernel/setup-xen.c中的setup_arch函数中调用的cpu_init函数的部分应该被删除,首先初始化cpu的操作涉及到特权指令,其次cpu初始化的工作在hypervisor中已经完成了。 4arch/arm/mach-goldfish/include/mach/system.h中的arch_idle函数原本调用cpu_do_idle函数,涉及到操作cpu让其等待中断来临,现在改为调用hypercall,最终调用do_block函数,当中hypervisor将当前vcpu的flags中的blocked位置位,并检查当前vcpu有无还未响 应的event,如果有则重新将blocked位清零,如果没有则设置vcpu相应的时钟的到期时间,这样等到下次hypervisor发现该vcpu的时钟到期的时候就向该vcpu的DomainU发送一个虚拟时钟中断的事件便又可以唤醒这个vcpu了。 2.2Android内存虚拟化 2.3 Android中断虚拟化 2.4 Android设备驱动虚拟化 2.5其他更改代码
本文档为【1&#46;2ARM hypervisor的cpu虚拟化 - vneo】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_005190
暂无简介~
格式:doc
大小:96KB
软件:Word
页数:47
分类:
上传时间:2018-05-17
浏览量:91