2017-08-24


今天咱们聊聊KVM中断虚拟化,虚拟机的中断源大致有两种方式,来自于用户空间qemu和来自于KVM内部。

中断虚拟化起始关键在于对中断控制器的虚拟化,中断控制器目前主要有APIC,这种架构下设备控制器通过某种触发方式通知IO APIC,IO APIC根据自身维护的重定向表pci irq routing table格式化出一条中断消息,把中断消息发送给local APIC,local APIC局部与CPU,即每个CPU一个,local APIC 具备传统中断控制器的相关功能以及各个寄存器,中断请求寄存器IRR,中断屏蔽寄存器IMR,中断服务寄存器ISR等,针对这些关键部件的虚拟化是中断虚拟化的重点。在KVM架构下,每个KVM虚拟机维护一个Io APIC,但是每个VCPU有一个local APIC。

核心数据结构介绍:

kvm_irq_routing_table

struct kvm_irq_routing_table {
/*ue->gsi*/
int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS];
struct kvm_kernel_irq_routing_entry *rt_entries;
u32 nr_rt_entries;
/*
* Array indexed by gsi. Each entry contains list of irq chips
* the gsi is connected to.
*/
struct hlist_head map[];
};

这个是一个中断路由表,每个KVM都有一个, chip是一个二维数组,表示三个芯片的各个管脚,每个芯片有24个管脚,每个数组项纪录对应管脚的GSI号;rt_entries是一个指针,指向一个kvm_kernel_irq_routing_entry数组,数组中共有nr_rt_entries项,每项对应一个IRQ;map其实可以理解为一个链表头数组,可以根据GSi号作为索引,找到同一IRQ关联的所有kvm_kernel_irq_routing_entry。具体中断路由表的初始化部分见本文最后一节

struct kvm_kernel_irq_routing_entry {
u32 gsi;
u32 type;
int (*set)(struct kvm_kernel_irq_routing_entry *e,
struct kvm *kvm, int irq_source_id, int level,
bool line_status);
union {
struct {
unsigned irqchip;
unsigned pin;
} irqchip;
struct msi_msg msi;
};
struct hlist_node link;
};

gsi是该entry对应的gsi号,一般和IRQ是一样,set方法是该IRQ关联的触发方法,通过该方法把IRQ传递给IO-APIC,;link就是连接点,连接在上面同一IRQ对应的map上;

中断注入在KVM内部流程起始于一个函数kvm_set_irq

int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,
bool line_status)
{
struct kvm_kernel_irq_routing_entry *e, irq_set[KVM_NR_IRQCHIPS];
int ret = -, i = ;
struct kvm_irq_routing_table *irq_rt; trace_kvm_set_irq(irq, level, irq_source_id); /* Not possible to detect if the guest uses the PIC or the
* IOAPIC. So set the bit in both. The guest will ignore
* writes to the unused one.
*/
rcu_read_lock();
irq_rt = rcu_dereference(kvm->irq_routing);
if (irq < irq_rt->nr_rt_entries)
hlist_for_each_entry(e, &irq_rt->map[irq], link)
irq_set[i++] = *e;
rcu_read_unlock();
/*依次调用同一个irq上的所有芯片的set方法*/
while(i--) {
int r;
/*kvm_set_pic_irq kvm_set_ioapic_irq*/
r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level,
line_status);
if (r < )
continue; ret = r + ((ret < ) ? : ret);
} return ret;
}

kvm指定特定的虚拟机,irq_source_id是中断源ID,一般有KVM_USERSPACE_IRQ_SOURCE_ID和KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID;irq是全局的中断号,level指定高低电平,需要注意的是,针对边沿触发,需要两个电平触发来模拟,先高电平再低电平。回到函数中,首先要收集的是同一irq上注册的所有的设备信息,这主要在于irq共享的情况,非共享的情况下最多就一个。设备信息抽象成一个kvm_kernel_irq_routing_entry,这里临时放到irq_set数组中。然后对于数组中的每个元素,调用其set方法,目前大都是APIC架构,因此set方法基本都是kvm_set_ioapic_irq,在传统pic情况下,是kvm_set_pic_irq。我们以kvm_set_ioapic_irq为例进行分析,该函数没有实质性的操作,就调用了kvm_ioapic_set_irq函数

int kvm_ioapic_set_irq(struct kvm_ioapic *ioapic, int irq, int irq_source_id,
int level, bool line_status)
{
u32 old_irr;
u32 mask = << irq;//irq对应的位
union kvm_ioapic_redirect_entry entry;
int ret, irq_level; BUG_ON(irq < || irq >= IOAPIC_NUM_PINS); spin_lock(&ioapic->lock);
old_irr = ioapic->irr;
/*判断请求高电平还是低电平*/
irq_level = __kvm_irq_line_state(&ioapic->irq_states[irq],
irq_source_id, level); entry = ioapic->redirtbl[irq];
irq_level ^= entry.fields.polarity;
/*模拟低电平*/
if (!irq_level) {
ioapic->irr &= ~mask;
ret = ;
} else {
/*判断触发方式*/
int edge = (entry.fields.trig_mode == IOAPIC_EDGE_TRIG); if (irq == RTC_GSI && line_status &&
rtc_irq_check_coalesced(ioapic)) {
ret = ; /* coalesced */
goto out;
}
/*设置中断信号到中断请求寄存器*/
ioapic->irr |= mask;
/*如果是电平触发且旧的irr和请求的irr不相等,调用ioapic_service*/
if ((edge && old_irr != ioapic->irr) ||
(!edge && !entry.fields.remote_irr))
ret = ioapic_service(ioapic, irq, line_status);
else
ret = ; /* report coalesced interrupt */
}
out:
trace_kvm_ioapic_set_irq(entry.bits, irq, ret == );
spin_unlock(&ioapic->lock); return ret;
}

到这里,中断已经到达模拟的IO-APIC了,IO-APIC最重要的就是它的重定向表,针对重定向表的操作主要在ioapic_service中,之前都是做一些准备工作,在进入ioapic_service函数之前,主要有两个任务:1、判断触发方式,主要是区分电平触发和边沿触发。2、设置ioapic的irr寄存器。之前我们说过,电触发需要两个边沿触发来模拟,前后电平相反。这里就要先做判断是对应哪一次。只有首次触发才会进行后续的操作,而二次触发相当于reset操作,就是把ioapic的irr寄存器清除。在电平触发模式下且请求的irq和ioapic中保存的irq不一致,就会对其进行更新,进入ioapic_service函数。

static int ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx,
bool line_status)
{
union kvm_ioapic_redirect_entry *pent;
int injected = -;
/*获取重定向表项*/
pent = &ioapic->redirtbl[idx]; if (!pent->fields.mask) {
/*send irq to local apic*/
injected = ioapic_deliver(ioapic, idx, line_status);
if (injected && pent->fields.trig_mode == IOAPIC_LEVEL_TRIG)
pent->fields.remote_irr = ;
}
return injected;
}

该函数比较简单,就是获取根据irq号,获取重定向表中的一项,然后向本地APIC传递,即调用ioapic_deliver函数,当然前提是kvm_ioapic_redirect_entry没有设置mask,ioapic_deliver主要任务就是根据kvm_ioapic_redirect_entry,构建kvm_lapic_irq,这就类似于在总线上的传递过程。构建之后调用kvm_irq_delivery_to_apic,该函数会把消息传递给相应的VCPU ,具体需要调用kvm_apic_set_irq函数,继而调用__apic_accept_irq,该函数中会根据不同的传递模式处理消息,大部分情况都是APIC_DM_FIXED,在该模式下,中断被传递到特定的CPU,其中会调用kvm_x86_ops->deliver_posted_interrupt,实际上对应于vmx.c中的vmx_deliver_posted_interrupt

static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
int r;
/*设置位图*/
if (pi_test_and_set_pir(vector, &vmx->pi_desc))
return;
/*标记位图更新标志*/
r = pi_test_and_set_on(&vmx->pi_desc);
kvm_make_request(KVM_REQ_EVENT, vcpu);
#ifdef CONFIG_SMP
if (!r && (vcpu->mode == IN_GUEST_MODE)); else
#endif
kvm_vcpu_kick(vcpu);
}

这里主要是设置vmx->pi_desc中的位图即struct pi_desc 中的pir字段,其是一个32位的数组,共8项。因此最大标记256个中断,每个中断向量对应一位。设置好后,请求KVM_REQ_EVENT事件,在下次vm-entry的时候会进行中断注入。

具体注入过程:

在vcpu_enter_guest (x86.c)函数中,有这么一段代码

if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
kvm_apic_accept_events(vcpu);
if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED) {
r = ;
goto out;
}
/*注入中断在vcpu加载到真实cpu上后,相当于某些位已经被设置*/
inject_pending_event(vcpu);//中断注入
……

即在进入非跟模式之前会检查KVM_REQ_EVENT事件,如果存在pending的事件,则调用kvm_apic_accept_events接收,这里主要是处理APIC初始化期间和IPI中断的,暂且不关注。之后会调用inject_pending_event,在这里会检查当前是否有可注入的中断,而具体检查过程时首先会通过kvm_cpu_has_injectable_intr函数,其中调用kvm_apic_has_interrupt->apic_find_highest_irr->vmx_sync_pir_to_irr,vmx_sync_pir_to_irr函数对中断进行收集,就是检查vmx->pi_desc中的位图,如果有,则会调用kvm_apic_update_irr把信息更新到apic寄存器里。然后调用apic_search_irr获取IRR寄存器中的中断,没找到的话会返回-1.找到后调用kvm_queue_interrupt,把中断记录到vcpu中。

static inline void kvm_queue_interrupt(struct kvm_vcpu *vcpu, u8 vector,
bool soft)
{
vcpu->arch.interrupt.pending = true;
vcpu->arch.interrupt.soft = soft;
vcpu->arch.interrupt.nr = vector;
}

最后会调用kvm_x86_ops->set_irq,进行中断注入的最后一步,即写入到vmcs结构中。该函数指针指向vmx_inject_irq

static void vmx_inject_irq(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
uint32_t intr;
int irq = vcpu->arch.interrupt.nr;//中断号 trace_kvm_inj_virq(irq); ++vcpu->stat.irq_injections;
if (vmx->rmode.vm86_active) {
int inc_eip = ;
if (vcpu->arch.interrupt.soft)
inc_eip = vcpu->arch.event_exit_inst_len;
if (kvm_inject_realmode_interrupt(vcpu, irq, inc_eip) != EMULATE_DONE)
kvm_make_request(KVM_REQ_TRIPLE_FAULT, vcpu);
return;
}
intr = irq | INTR_INFO_VALID_MASK;//设置有中断向量的有效性
if (vcpu->arch.interrupt.soft) {//如果是软件中断
intr |= INTR_TYPE_SOFT_INTR;//内部中断
vmcs_write32(VM_ENTRY_INSTRUCTION_LEN,
vmx->vcpu.arch.event_exit_inst_len);//软件中断需要写入指令长度
} else
intr |= INTR_TYPE_EXT_INTR;//标记外部中断
vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);
}

最终会写入到vmcs的VM_ENTRY_INTR_INFO_FIELD中,这需要按照一定的格式。具体格式详见intel手册。0-7位是向量号,8-10位是中断类型(硬件中断或者软件中断),最高位是有效位,12位是NMI标志。

#define INTR_INFO_VECTOR_MASK           0xff            /* 7:0 */
#define INTR_INFO_INTR_TYPE_MASK 0x700 /* 10:8 */
#define INTR_INFO_DELIVER_CODE_MASK 0x800 /* 11 */
#define INTR_INFO_UNBLOCK_NMI 0x1000 /* 12 */
#define INTR_INFO_VALID_MASK 0x80000000 /* 31 */

 中断路由表的初始化

用户空间qemu通过KVM_CREATE_DEVICE API接口进入KVM的kvm_vm_ioctl处理函数,继而进入kvm_arch_vm_ioctl,根据参数中的KVM_CREATE_IRQCHIP标志进入初始化中断控制器的流程,首先肯定是注册pic和io APIC,这里我们就不详细阐述,重点在于后面对中断路由表的初始化过程。中断路由表的初始化通过kvm_setup_default_irq_routing函数实现,

int kvm_setup_default_irq_routing(struct kvm *kvm)
{
return kvm_set_irq_routing(kvm, default_routing,
ARRAY_SIZE(default_routing), );
}

首个参数kvm指定特定的虚拟机,后面default_routing是一个全局的kvm_irq_routing_entry数组,就定义在irq_comm.c中,该数组没别的作用,就是初始化kvm_irq_routing_table,看下kvm_set_irq_routing

int kvm_set_irq_routing(struct kvm *kvm,
const struct kvm_irq_routing_entry *ue,
unsigned nr,
unsigned flags)
{
struct kvm_irq_routing_table *new, *old;
u32 i, j, nr_rt_entries = ;
int r;
/*正常情况下,nr_rt_entries=nr*/
for (i = ; i < nr; ++i) {
if (ue[i].gsi >= KVM_MAX_IRQ_ROUTES)
return -EINVAL;
nr_rt_entries = max(nr_rt_entries, ue[i].gsi);
}
nr_rt_entries += ;
/*为中断路由表申请空间*/
new = kzalloc(sizeof(*new) + (nr_rt_entries * sizeof(struct hlist_head))
+ (nr * sizeof(struct kvm_kernel_irq_routing_entry)),
GFP_KERNEL); if (!new)
return -ENOMEM;
/*设置指针*/
new->rt_entries = (void *)&new->map[nr_rt_entries]; new->nr_rt_entries = nr_rt_entries;
for (i = ; i < KVM_NR_IRQCHIPS; i++)
for (j = ; j < KVM_IRQCHIP_NUM_PINS; j++)
new->chip[i][j] = -;
/*初始化每一项kvm_kernel_irq_routing_entry*/
for (i = ; i < nr; ++i) {
r = -EINVAL;
if (ue->flags)
goto out;
r = setup_routing_entry(new, &new->rt_entries[i], ue);
if (r)
goto out;
++ue;
}
mutex_lock(&kvm->irq_lock);
old = kvm->irq_routing;
kvm_irq_routing_update(kvm, new);
mutex_unlock(&kvm->irq_lock); synchronize_rcu();
/*释放old*/
new = old;
r = ;
out:
kfree(new);
return r;
}

可以参考一个宏:

#define IOAPIC_ROUTING_ENTRY(irq) \
{ .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP, .u.irqchip.irqchip = KVM_IRQCHIP_IOAPIC, .u.irqchip.pin = (irq) }

这是初始化default_routing的一个关键宏,没一项都是通过该宏传递irq号(0-23)64位下是0-47,可见gsi就是irq号,所以实际上,回到函数中nr_rt_entries就是数组中项数,接着为kvm_irq_routing_table分配空间,注意分配的空间包含三部分:kvm_irq_routing_table结构、nr_rt_entries个hlist_head和nr个kvm_kernel_irq_routing_entry,所以kvm_irq_routing_table的大小是和全局数组的大小一样的。整个结构如下图所示

根据上图就可以理解new->rt_entries = (void *)&new->map[nr_rt_entries];这行代码的含义,接下来是对没项的table的chip数组做初始化,这里初始化为-1.接下来就是一个循环,对每一个kvm_kernel_irq_routing_entry做初始化,该过程是通过setup_routing_entry函数实现的,这里看下该函数

static int setup_routing_entry(struct kvm_irq_routing_table *rt,
struct kvm_kernel_irq_routing_entry *e,
const struct kvm_irq_routing_entry *ue)
{
int r = -EINVAL;
struct kvm_kernel_irq_routing_entry *ei; /*
* Do not allow GSI to be mapped to the same irqchip more than once.
* Allow only one to one mapping between GSI and MSI.
*/
hlist_for_each_entry(ei, &rt->map[ue->gsi], link)
if (ei->type == KVM_IRQ_ROUTING_MSI ||
ue->type == KVM_IRQ_ROUTING_MSI ||
ue->u.irqchip.irqchip == ei->irqchip.irqchip)
return r;
e->gsi = ue->gsi;
e->type = ue->type;
r = kvm_set_routing_entry(rt, e, ue);
if (r)
goto out;
hlist_add_head(&e->link, &rt->map[e->gsi]);
r = ;
out:
return r;
}

之前的初始化过程我们已经看见了,.type为KVM_IRQ_ROUTING_IRQCHIP,所以这里实际上就是把e->gsi = ue->gsi;e->type = ue->type;然后调用了kvm_set_routing_entry,该函数中主要是设置了kvm_kernel_irq_routing_entry中的set函数,APIC的话设置的是kvm_set_ioapic_irq函数,而pic的话设置kvm_set_pic_irq函数,然后设置irqchip的类型和管脚,对于IOAPIC也是直接复制过来,PIC由于管脚计算是irq%8,所以这里需要加上8的偏移。之后设置table的chip为gis号。回到setup_routing_entry函数中,就把kvm_kernel_irq_routing_entry以gsi号位索引,加入到了map数组中对应的双链表中。再回到kvm_set_irq_routing函数中,接下来就是更新kvm结构中的irq_routing指针了。

中断虚拟化流程

kvm_set_irq
  kvm_ioapic_set_irq
     ioapic_service
      ioapic_deliver
        kvm_irq_delivery_to_apic
          kvm_apic_set_irq
            __apic_accept_irq
              vmx_deliver_posted_interrupt

具体注入阶段
vcpu_enter_guest
   kvm_apic_accept_events
      inject_pending_event
        kvm_queue_interrupt
          vmx_inject_irq
            vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);

 中断路由表初始化

x86.c kvm_arch_vm_ioctl
  kvm_setup_default_irq_routing irq_common.c
    kvm_set_irq_routing irq_chip.c
      setup_routing_entry irq_chip.c
        kvm_set_routing_entry irq_chip.c
          e->set = kvm_set_ioapic_irq; irq_common.c

以马内利!

参考资料:

LInux3.10.1源码

KVM中断虚拟化浅析的更多相关文章

  1. Xen,VMware ESXi,Hyper-V和KVM等虚拟化技术的原理解析

    Xen,VMware ESXi,Hyper-V和KVM等虚拟化技术的原理解析 2018年04月03日 13:51:55 阅读数:936   XEN 与 VMware ESXi,Hyper-V 以及 K ...

  2. 【原创】Linux虚拟化KVM-Qemu分析(六)之中断虚拟化

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: KVM版本:5.9 ...

  3. KVM 内存虚拟化

    内存虚拟化的概念     除了 CPU 虚拟化,另一个关键是内存虚拟化,通过内存虚拟化共享物理系统内存,动态分配给虚拟机.虚拟机的内存虚拟化很象现在的操作系统支持的虚拟内存方式,应用程序看到邻近的内存 ...

  4. 基于KVM的虚拟化研究及应用

    引言 虚拟化技术是IBM在20世纪70年代首先应用在IBM/370大型机上,这项技术极大地提高了大型机资源利用率.随着软硬件技术的迅速发展,这项属于大型机及专利的技术开始在普通X86计算机上应用并成为 ...

  5. KVM的虚拟化研究及应用

    引言 虚拟化技术是IBM在20世纪70年代首先应用在IBM/370大型机上,这项技术极大地提高了大型机资源利用率.随着软硬件技术的迅速发展,这项属于大型机及专利的技术开始在普通X86计算机上应用并成为 ...

  6. KVM 存储虚拟化 - 每天5分钟玩转 OpenStack(7)

    KVM 的存储虚拟化是通过存储池(Storage Pool)和卷(Volume)来管理的. Storage Pool 是宿主机上可以看到的一片存储空间,可以是多种类型,后面会详细讨论.Volume 是 ...

  7. KVM 网络虚拟化基础 - 每天5分钟玩转 OpenStack(9)

    网络虚拟化是虚拟化技术中最复杂的部分,学习难度最大. 但因为网络是虚拟化中非常重要的资源,所以再硬的骨头也必须要把它啃下来. 为了让大家对虚拟化网络的复杂程度有一个直观的认识,请看下图 这是 Open ...

  8. KVM嵌套虚拟化nested之CPU透传

    嵌套式虚拟nested是一个可通过内核参数来启用的功能.它能够使一台虚拟机具有物理机CPU特性,支持vmx或者svm(AMD)硬件虚拟化.该特性需要内核升级到Linux 3.X版本 ,所以在cento ...

  9. KVM+QEMU虚拟化概念

    概念: KVM,即Kernel-basedvirtual machine,由redhat开发,是一种开源.免费的虚拟化技术.对企业来说,是一种可选的虚拟化解决方案. 定义:基于Linux内核的虚拟机 ...

随机推荐

  1. linux下常用FTP命令 1. 连接ftp服务器

    1. 连接ftp服务器 格式:ftp [hostname| ip-address] a)在linux命令行下输入: ftp 192.168.1.1 b)服务器询问你用户名和密码,分别输入用户名和相应密 ...

  2. Java线程之Callable和Future

    本篇说明的是Callable和Future,它俩很有意思的,一个产生结果,一个拿到结果.        Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结 ...

  3. iOS7入门开发全系列教程新地址

    包括了系列1所有.系列2所有,系列3部分(进行中) 由于大家都知道的原因,换了github保存: https://github.com/eseedo/kidscoding 假设下载有问题能够留言,请在 ...

  4. 有限状态机FSM详解及其实现

    有限状态机,也称为FSM(Finite State Machine),其在任意时刻都处于有限状态集合中的某一状态.当其获得一个输入字符时,将从当前状态转换到另一个状态,或者仍然保持在当前状态.任何一个 ...

  5. html5--移动端视频video的android兼容,去除播放控件、全屏等

    html5 中的video 在手机浏览器中的总结所有页面播放时, 如果选择全屏播放, 播放画面将浮动到屏幕的最上层 IOS 手机   自动播放 播放界面浮动文字 播放时是否自动全屏 能否嵌入在页面中播 ...

  6. sublime text 2 破解

    本文是介绍sublime text 2.0.2 build 2221 64位 的破解 在你使用sublime时可能经常出现下图: 这是在提醒你注册 在工具栏上点击help->Enter Lice ...

  7. 详解JavaScript的splice()方法

    from:http://www.jquerycn.cn/a_10447 在javascript中splice()方法,是一个很强的数组方法,它有多种用法.splice()主要用途是向数组的中部插入项. ...

  8. ios 调用系统应用的方法 应用间跳转的方法

    声明一个私有方法: #pragma mark - 私有方法 -(void)openUrl:(NSString *)urlStr{ //注意url中包含协议名称,iOS根据协议确定调用哪个应用,例如发送 ...

  9. shell基础篇(五)条件判断

    写脚本时:有时要判断字符串是否相等,数字测试.这对后面学习的shell语句,循环,条件语句做好基础. 条件判断格式  1. test condition : test命令  2. [ conditio ...

  10. 【渗透测试学习平台】 web for pentester -8.XML

    example1: http://192.168.91.139/xml/example1.php?xml=%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%2 ...