Chinaunix首页 | 论坛 | 博客
  • 博客访问: 169105
  • 博文数量: 123
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 11
  • 用 户 组: 普通用户
  • 注册时间: 2015-06-20 19:04
文章分类

全部博文(123)

文章存档

2015年(123)

我的朋友

分类: 虚拟化

2015-06-21 11:49:58

原文地址:Guest OS Time Tick Source 作者:embeddedlwp

一个操作系统要跑起来,必须有time tick,它就像是身体的脉搏。普通情况下,OS time tick由PIT(i8254)或APIC Timer设备提供—PIT定期(1ms in Linux)产生一个timer interrupt,作为global tick,APIC Timer产生一个local tick。在虚拟化情况下,必须为guest OS模拟一个PIT和APIC Timer。
    模拟的PIT和APIC Timer不能像真正硬件那样物理计时,所以一般用host的某种系统服务或软件计时器来为这个模拟PIT提供模拟"时钟源"。
    在QEMU中,用SIGALARM信号来实现—QEMU利用某种机制,使timer interrupt handler会向QEMU process发送一个SIGALARM信号,处理该信号过程中再模拟PIT中产生一次时钟。QEMU再通过某种机制,将此模拟PIT发出的模拟中断交付给kvm,再由kvm注入到虚拟机中去。
    目前的kvm版本支持内核PIT、APIC和内核PIC,因为这两个设备是频繁使用的,在内核模式中模拟比在用户模式模拟性能更高。这里重点是讲内核PIT的模拟实现,弄清楚它是如何为guest OS提供时钟的。
      首先,创建内核PIT的路径如下所示,由qemu-kvm在创建虚拟机时向内核驱动请求创建内核模拟PIT,内核驱动根据请求创建PIT。通过hrtimer_init调用可以看出,这个内核模拟PIT的时钟实际上是由host Linux的hrtimer来提供的,hrtimer_init还只是初始化一个hrtimer timer,还没有开始计时。kvm_iodevice_init & kvm_io_bus_register_dev调用的作用是注册此内核PIT,到时候在VMExit时模拟处理guest io instructions,如果是in/out PIT的端口,那么就可以直接在内核中完成了,不需要返回到用户空间(kernel_pio函数会遍历所有注册的内核模拟设备,如果有端口匹配,则说明由内核模拟此设备)。
qemu-kvm:
main -> kvm_create_context -> kvm_create(qemu-kvm.c) -> kvm_arch_create(qemu-kvm-x86.c) -> kvm_create_pit(q-k-x86.c) -> kvm_vm_ioctl(KVM_CREATE_PIT)
kvm-mod:
kvm_arch_vm_ioctl(x86.c) -> case KVM_CREATE_PIT: kvm_create_pit(i8254.c) -> hrtimer_init_p & kvm_iodevice_init & kvm_io_bus_register_dev
     其次,开始设置内核PIT使其开始计时的路径如下所示,其中kvm_pit_post_load实际上不是kvm_pit_init调用的,只是有一句"vmstate_pit.post_load = kvm_pit_post_load",具体它是什么时候调用的还不太清楚。
qemu-kvm:
pc_init1(hw/pc.c) -> kvm_pit_init(hw/i8254-kvm.c) ---> kvm_pit_post_load -> kvm_set_pit -> ioctl(KVM_SET_PIT)
kvm-mod:
kvm_arch_vm_ioctl(x86.c) -> case KVM_SET_PIT -> kvm_vm_ioctl_set_pit(x86.c) -> kvm_pit_load_count(i8254.c) -> pit_load_count(i8254.c) -> create_pit_timer(i8254.c) -> hrtimer_start_p 
    接着,来看内核PIT是如何利用这个hrtimer实际时钟源来提供虚拟时钟的。hrtimer具体如何实现不知道,但是它应该是用物理的timer interrupt来实现的(见Hrtimer机制一文)。当hrtimer计数器到时,内核会回调在hrtimer_start时注册的回调函数——在create_pit_timer函数中有一句"pt->timer.function = kvm_timer_fn"。因此,这个kvm_timer_fn函数就是PIT hrtimer的回调函数。kvm_timer_fn又调用__kvm_timer_fn。__kvm_timer_fn中有一句"set_bit(KVM_REQ_PENDING_TIMER,&vcpu_request)"。那么,kvm只要检测这个bit就可以知道是否有时钟中断了。不过,kvm好像并没有检测这个bit,而是去检测pit_timer.pending。因为在__kvm_timer_fn中也有一句"atomic_inc(&ktimer->pending)",而这个ktimer地址记录在pit->pit_state结构中,pit结构地址由记录在kvm结构中。另一方面,在__vcpu_run中,在vcpu_enter_guest返回后,有一句"kvm_cpu_has_pending_timer(vcpu)"实际上就是检测vcpu->kvm->arch.vpit->pit_state.pit_timer.pending。
     综合以上机制介绍,来想像一个场景,就可以理解物理pit timer interrupt是如何经为虚拟pit提供时钟,进而产生虚拟timer interrupt提交给guest OS。假如:guest vcpu正在执行,来了一个时钟物理中断,而这个物理时钟中断也要传递给guest OS,过程如下:
    1. 物理中断导致VMExit,guestvcpu线程返回kvm_x86_ops->run,接着vcpu_enter_guest开中断,host timer interrupt handler处理这个物理中断。
    2.timer interrupt handler发现内核PIT使用的hrtimer到时,调用kvm_pit_fun回调函数,将pit_state.pit_timer.pending置位
    3.timer interrupt handler返回,vcpu_enter_guest调用kvm_x86_ops->handle_exit处理VMExit,发现exit_reason是外部中断,返回1,vcpu_enter_guest传递返回值回到__vcpu_run。
    4.__vcpu_run接着调用kvm_cpu_has_pending_timer检测内核PIT的timer是否pending。现在将检测到是,那么调用kvm_inject_pending_timer_irqs将虚拟时钟中断注入到内核模拟的LAPIC中,产生一个中断触发。    
    5.中断注入机制之前一篇笔记介绍过,将vmcs相应位按规范设置好即可。由于vcpu_enter_guest返回1,所以不需要切换到用户模式,而是再次循环调用vcpu_enter_guest。
    6.在kvm_x86_ops->run之前,调用inject_pending_event,它会将LAPIC中触发的中断真正地注入到vcpu中(通过调用kvm_x86_ops->set_irq)。再次VMEntry时,硬件检测到虚拟时钟中断,然后调用guest OS timer interrupt handler进行处理。
      至于内核APIC Timer的模拟实现,与内核PIT的原理是差不多的。同样用hrtimer_init和hrtimer_start初始化和启动一个hrtimer,然后将其对应的kvm_timer结构的pending位置1,在kvm_cpu_has_pending_timer中同样会检测内核APIC Timer的pending。kvm_inject_pending_timer_irqs也同样可以注入对应的irq。
    最后一个问题是,在调用hrtimer_start之前,要设定hrtimer的过期时间,每次的过期时间减去当前时间now就是PIT和APIC Timer的时钟频率。按道理,这个时钟频率应该是由guest OS来设定的。那么来看两个时钟周期的设定是怎样的:
    对内核PIT来讲,应该是kvm_vm_ioctl_set_pit的val参数来确定它的周期的,这个val好像是由用户态的qemu-kvm传递进来的,所以具体在哪设置还没搞清楚。
       对内核APIC Timer来讲,是在start_apic_timer(lapic.c)中设定。注意有一个语句"apic->lapic_timer.period = apic_get_reg(apic,APIC_TMICT)",周期应该就是从模拟APIC的某个寄存器中读取。那么,首先是guest OS设定周期到模拟APIC的TMICT寄存器中,kvm再读出来以此来设定hrtimer的到期时间,就可以精确地模拟这个APIC Timer的时钟行为了。
A timer interrupt would send a SIGALRM to qemu, which would > eventually cause KVM to emulate a timer interrupt and execute the > guest handler in this case   
上一篇文章已经讨论过了在有内核模拟时钟情况下,kvm是如何给guest OS提供时钟源的。后面,我对Linux时间子系统做了一定的研究,体现在博文[Timer学习]系列,为了进一步加深理解,我就想,能不能把没有内核模拟时钟时,即原来Qemu用户空间模拟时钟的机制搞清楚。通过研究qemu-kvm-0.12.3代码,有了一定的认识,下面总结一下。
 还是先来看一下Qemu用户态模拟时钟是从哪里开始初始化的。代码路径如下图所示,在main中调用init_timer_alarm开始。还是那句话,Qemu模拟时钟是软件的,不能像硬件时钟那样自己产生时钟计数,所以必须要求助于host的各种timer服务来提供实际时钟。Qemu在vl.c定义了一个全局数组alarm_timers[],里面定义了几种host timer服务的封装实例,有dynticks, hpet, rtc。这三种timer服务是Linux内核提供给用户程序使用的。下面看Qemu是如何具体利用这些服务的,就能有更深的认识了。
 如果是选用dynticks,那么在init_timer_alarm中调用t->start即dynticks_start_timer。dynticks_start_timer首先用sigaction注册SIGALARM信号的handler为host_alarm_handler,然后调用timer_create创建一个时钟。这就很清楚了,timer_create实际上就属于Posix Timer API。应用程序创建了一个Posix Timer后,一旦timer到期,那么内核将给调用进程发送一个SIGALARM信号,应用进程通过signal handler再去做处理。
QEMU timer emulation path:
main(vl.c)  ---> init_timer_alarm(vl.c) ---> dynticks_start_timer ---> sigaction(SIGALARM, host_alarm_handler) --> timer_create
             (or  ---> hpet_start_timer ---> open(/dev/hpet) ---> enable_sigio_timer --> sigaction(SIGIO, host_alarm_handler)
 刚开始仔细一想发现不对了,在内核模拟时钟时,是调用hrtimer_init和hrtimer_start来初始化hrtimer,并且可以指定一个回调函数,一旦hrtimer到期,内核会调用回调函数,为什么这里不能这样,而只是发送一个信号呢? 其实问题很简单,内核调用回调函数是在内核空间调用,所以内核模拟时钟可以注册回调函数。但是,提供给用户程序的timer API当然不能让用户程序直接注册回调函数,不然启不是允许用户函数到时可以在内核态下运行了?因此,对于用户程序,当timer到期后,只能是发送一个信号给用户进程,进程实际上用回调函数注册为signal handler。
 Posix Timer是怎样实现的呢?在hrtimer.txt中说了,Posix Timer是基于hrtimer机制实现的,我想大概是这样的:用户进程调用timer_create创建Posix Timer,实际上内核用调用hrtimer_init初始化一个hrtimer,当hrtimer到期时,内核执行其回调函数,回调函数将会发送一个SIGALARM给用户进程。
 结合以上论述,当dynticks timer到期后,内核发送一个SIGALARM信号给Qemu进程,之前介绍过,Qemu进程block了SIGALARM信号,信号是由创建的signalfd去接受;之后,io thread用select检测到signalfd read ready,然后就调用sigfd_handler;sigfd_handler根据signal number,调用相应的handler,就是之前用sigaction注册过的host_alarm_handler。
 可以看出,dynticks实际上是内核提供的软件timer服务,那么hpet和rtc又是怎样的呢?通过研究hpet_start_timer,发现原来这里是直接去调用hpet driver,使hpet硬件直接产生一个Qemu需要的时钟源。当hpet timer到期时,hpet硬件发送一个interrupt给host,host hpet driver handler发现这个中断是由Qemu进程"引起"的,所以发送一个SIGIO信号给Qemu进程。同样,在hpet_start_timer调用的enable_sigio_timer函数中用sigaction注册host_alarm_handler为SIGIO信号的handler。rtc也跟hpet是一样的,实际上是去调用rtc driver,不用多讲了。
 因此,可以发现dynticks是利用软件timer提供时钟计数,而hpet和rtc是直接利用硬件提供时钟计数。现在可以总结一下Linux下的各种timer服务了:
 首先,内核态程序可以直接使用hrtimer API来使用timer,可以到期时可直接执行回调函数。至于hrtimer可以使利用hpet,也可以是利用local APIC timer去实现的。
 其次,用户态程序可以有三大类timer服务——第一类是,nanosleep()、sleep()这样的系统调用,内核实际上在内核创建一个临时的timer,当到期时将用户进程唤醒;第二类是,Posix Timer API,内核实现上是用hrtimer来实现Posix Timer的,当到期时将发送一个SIGALARM信号给用户进程;第三类是,/dev/hpet和/dev/rtc,即直接利用硬件timer,因为这些硬件timer可以支持很多个不同频率的timer,所以用户进程可以直接使用其中的一个,当到期时内核将发送SIGIO信号给应用进程。
 最后说一下,这里还有个重要问题没有讨论,就是Qemu用户态模拟时钟如何将产生一个虚拟时钟中断,将其最后注入到guest里面去。这就要从host_alarm_handler开始研究,以后再总结吧!
在(1)中详细阐述了KVM是如何模拟内核PIT以及如何将timer中断注入到guest中去的。但是,那时对这一整套的机制理解还不够深,现在通过研究代码,理解又上升了一层,所以加深的总结一下。
 核心想搞清楚的问题是如下几个:
 1. 假如guest中设定PIT的频率为1000Hz,那么KVM如何为模拟PIT产生这个1000Hz的时钟源
 2. 当时钟周期到时,会产生物理中断,KVM是如何将这个物理中断最终注入到guest中,使guest产生一个虚拟时钟中断的
 第一个问题在(1)中讲的比较清楚,总的来讲就是,KVM的内核模拟PIT实际是用host Linux的hrtimer机制来产生时钟源。模拟PIT时钟源周期是1ms,那么KVM就创建一个hrtimer,过期时间是now+1ms。
 第二个问题比较复杂。首先,当now+1ms到时,host hrtimer所利用的物理时钟设备,如物理PIT,会产生一个物理中断。这里就要分两种情况来讨论了:
 1. 第一种情况是,物理中断产生的这一刻是guest在运行,这种情况下又有两种方式可以将虚拟时钟中断注入guest中:
  1.1 直接注入进去
  1.1.1 先要发生VM exit,在vmx_vcpu_run的最后调用complete_interrupt,会将vcpu.arch.interrupt_pending位置1,并从VMCS中取出中断信息保存到vcpu控制结构中去。
  1.1.2 下一次迭代重新进入vcpu_guest_enter后,在vmx_vcpu_run之前会调用inject_pending_event。它会检测vcpu->arch.interrupt_pending位,发现是1,那么调用kvm_x86_ops->set_irq将中断真正注入VMCS中去,在VM entry时guest就能响应虚拟中断了。
  1.2 通过模拟PIC或模拟APIC注入虚拟中断
   1.2.1 先要发生VM exit,随后vmx_vcpu_run返回到vcpu_guest_enter中后将开中断,立刻进入host中断处理流程。
  1.2.2 host ISR发现为模拟PIT创建的hrtimer到期,将在softirq中调用它的回调函数kvm_timer_fn。
  1.2.3 kvm_timer_fn将设置ktimer.pending为1,其实就是vcpu.kvm->arch.pit->pit_state.pit_timer. pending,这是模拟了PIT设备产生一次时钟信号的操作。
  1.2.4 之后vcpu_guest_enter返回到__vcpu_enter中,将调用kvm_cpu_has_pending_timer,其实就是去检测那个pending位,将发现已经有了pending timer,那么会调用kvm_inject_pending_timer_irqs将这个时钟中断注入到模拟PIC或模拟APIC中去,这是模拟了一次PIT设备向PIC或APIC发送一个中断信号的操作。
  1.2.5 下一次迭代重新进入vcpu_guest_enter后,在vmx_vcpu_run之前会调用inject_pending_event。它中间会调用kvm_cpu_has_interrupt去检测模拟PIC或模拟APIC中是否有中断触发,发现有,将首先设置vcpu->arch.interrupt_pending位为1,再调用kvm_x86_ops->set_irq将中断真正注入VMCS中去,在VM entry时guest就能响应了,这是模拟了一次PIC或APIC向CPU发送一个中断信号的操作。
 上述两种情况会同时发生,只有在最后inject_pending_event中,如果前一种方式存在,那么后面这种方式就不用注入了,因为检测vcpu->arch.interrupt_pending在前,如果为1后面会直接返回了。
 2. 第二种情况时,物理中断发生时guest并没有在运行。那么将直接进入host ISR,此时只能是通过上述第二种方式将虚拟时钟中断注入到guest中去。这应该就是为什么要设计两种注入方式的原因吧。
1. Interrupt occurs when guest code is running
    1. vmx_vcpu_run--> complete_interrupt --> vcpu.arch.interrupt.pending = 1
    2. vcpu_enter_guest (before vmx_vcpu_run) --> inject_pending_event --> kvm_x86_ops->set_irq (vmx_inject_irq) --> vmcs_write (VM_ENTRY_INTR_INFO_FIELD)
2. Timer ISR --> hrtimer_func (kvm_timer_fn) --> timer->pending --> __vcpu_run (after vcpu_enter_guest return) --> 

阅读(647) | 评论(0) | 转发(0) |
0

上一篇:KVM: Posted Interrupt

下一篇:qemu-kvm io线程

给主人留下些什么吧!~~