文章目录
- 发起关机
- 标记关机
- 通知qemu主线程
- 主线程发起关机流程
- 响应关机流程
- 中断相关硬件初始化
- 中断模拟基本原理
- ARM平台GPIO中断模拟
- 中断控制器GIC设备模拟
- GIPO控制器芯片PL061设备模拟
- GPIO-KEY设备模拟
- 设备树
- 下发中断到KVM
- Q&A
- 关闭一个虚拟机有多种方式,包括:
- 从虚拟机内部关闭
- 主机侧通过
virsh shutdown
关闭 - 主机侧通过
virsh destroy
强制kill虚拟机进程
-
virsh shutdown
方式是一种安全的关闭方式,包括acpi
方式和agent
方式
-
acpi
就是硬件关闭 -
agent
就是通过qemu-guest-agent
(一个驻留在虚拟机内部的qemu代理)
-
agent
方式关闭需要在虚拟机内部安装agent
软件,有一定的依赖性,通常情况下,如果可以不依赖任何软件包,优雅的关闭虚拟机是我们最好的选择,因此最常用的关机方式是acpi
关机,这就是本文要介绍的关机流程,arm
架构下,这个流程通过硬件apio-key
发起中断来实现。 - 支持优雅的acpi关机,libvirt需要配置acpi特性,否则qemu无法实现
<features>
<acpi/>
</features>
发起关机
标记关机
- 主机侧进程发起关机调用的
qemu
的qmp system-powerdown
命令,其实现为qmp_system_powerdown
,流程如下
qmp_system_powerdown
qemu_system_powerdown_request
powerdown_requested = 1 /* 设置关机标志,主线程event loop时每次都会检查该标志,如果为1执行关机操作 */
qemu_notify_event
qemu_bh_schedule(qemu_notify_bh) /* 向主线程事件循环监听的eventfd发消息,以唤醒主线程 */
函数做了一个事情,设置全局的关机请求标识powerdown_requested
,然后通过eventfd消息机制唤醒qemu主线程,
通知qemu主线程
- 通知使用了qemu主事件循环的下半部功能,简单讲,
qemu_notify_bh
是一个下半部全局结构QEMUBH
,它通过qemu_bh_new
注册到qemu主线程的事件循环中,一个线程只能运行一个事件循环。下半部本身是一个钩子函数,它期望自己挂载的事件循环在poll到fd之后调度自己,下半部的本质是qemu其余线程在主线程上的异步延迟调用。
static void notify_event_cb(void *opaque)
{
/* No need to do anything; this bottom half is only used to
* kick the kernel out of ppoll/poll/WaitForMultipleObjects.
*/
}
qemu_init_main_loop
qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL) // 创建下半部,将下半部的回调设置为notify_event_cb
notify_event_cb函数什么没有做,那这个下半部注册的意义在哪呢?它是意义是唤醒qemu主线程(通过qemu_bh_schedule
),防止主线程因为没有poll到fd而一直不工作
下半部
主线程发起关机流程
- 如果主线程的事件循环感兴趣的fd没有准备好,主线程会一直在main_loop_wait睡眠,当eventfd的rfd准备好之后,主线程就被唤醒了,从而继续while循环,while循环中有判断虚拟机是否关机的函数,从而发起真正的硬件关机流程。所以真正的硬件关机流程是主线程做的,应用程序执行的qmp命令只是设置了关机的标记请求
/* 主线程事件循环检查 */
static void main_loop(void)
{
while (!main_loop_should_exit()) {
main_loop_wait(false);
}
}
/* 如果powerdown_requested全局变量被标记为1,执行关机操作 */
main_loop_should_exit
if (qemu_powerdown_requested()) {
qemu_system_powerdown();
}
/* 遍历所有关机通知链表上注册的每个节点,执行其notify操作 */
static void qemu_system_powerdown(void)
{
qapi_event_send_powerdown();
notifier_list_notify(&powerdown_notifiers, NULL);
}
void notifier_list_notify(NotifierList *list, void *data)
{
Notifier *notifier, *next;
QLIST_FOREACH_SAFE(notifier, &list->notifiers, node, next) {
notifier->notify(notifier, data);
}
}
- powerdown_notifiers是一个NotifierList类型的变量,它是一个链表头,链表上节点的类型为Notifier,每个Notifier可以包含一个函数用于实现关机通知操作,数据结构如下:
typedef struct Notifier Notifier;
struct Notifier
{
void (*notify)(Notifier *notifier, void *data);
QLIST_ENTRY(Notifier) node;
};
typedef struct NotifierList
{
QLIST_HEAD(, Notifier) notifiers;
} NotifierList;
- 不同平台关机通知的实现各有不同,ARM平台上通过virt_powerdown_req实现关机,X86平台上通过piix4_pm_powerdown_req实现关机通知
响应关机流程
-
qemu_system_powerdown
做的事情是将一个全局链表里面的所有节点上注册的回调执行一遍,这个全局链表shutdown_notifiers
上的挂载的节点包含很多回调,目的时是期望在虚拟机关机时qemu执行自己,ARM平台上gpio硬件会注册关机的回调,当有关机请求时,触发对应的钩子函数virt_powerdown_req
,X86平台由acpi的电源管理实现关机,注册的钩子函数为piix4_pm_powerdown_req
ARM:
static Notifier virt_system_powerdown_notifier = {
.notify = virt_powerdown_req
};
virt_machine_class_init
machvirt_init
create_gpio
qemu_register_powerdown_notifier(&virt_system_powerdown_notifier)
X86:
piix4_pm_class_init
k->realize = piix4_pm_realize
s->powerdown_notifier.notify = piix4_pm_powerdown_req
qemu_register_powerdown_notifier(&s->powerdown_notifier)
- ARM平台上,回调函数做了一个事情,注入中断,让虚拟机再次运行时触发apio-key中断
static void virt_powerdown_req(Notifier *n, void *opaque)
{
/* use gpio Pin 3 for power button event */
qemu_set_irq(qdev_get_gpio_in(gpio_key_dev, 0), 1);
}
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
通过以上方式,主机上发起的关机动作以中断的形式传递给了虚拟机,最终就能实现关机流程,当然前提是虚机内核驱动对这个gpio-key
中断引脚设置的isr
是关机动作。简单几行代码,就完成中断的注入了?是的,qemu的中断注入就从这里开始,在函数执行之前,qemu再启动时做了许多中断相关的硬件初始化准备工作,下一节会仔细分析。
- X86平台上,回调函数执行流程如下:
piix4_pm_powerdown_req
acpi_pm1_evt_power_down
ar->tmr.update_sci(ar)
中断相关硬件初始化
中断模拟基本原理
- qemu模拟中断,主要是模拟处理中断的引脚和芯片,考虑一个外部设备作为中断源,从中断触发,到CPU中断引脚断言,再到CPU响应中断,这中间的过程如下:
- 外部设备的输出引脚接中断处理芯片的输入引脚或直连到中断控制器的输入引脚,中断发生后,外部设备向它的上级设备提交中断信息。
- 上级设备可以是普通芯片,也可以是中断控制器,如果是普通芯片,就继续迭代提交中断信息,直到中断信息到达中断控制器(Intel架构就是IO APIC,ARM结构就是GIC)。
- 中断控制器的输入引脚断言到中断信息到达,会根据中断源的信息,判断这个中断应该投递到哪个CPU,或者哪组CPU,甚至全部的CPU
- 中断设备的模拟,核心是对中断信号处理芯片的模拟,无论是GPIO控制器还是中断控制器,这些芯片都会处理中断信号。如果把这类芯片想象成黑盒,它们最核心的实现可以抽象成三步:
- 接收中断信号
- 处理中断信号
- 更新芯片状态
- qemu对中断设备模拟的核心,就是实现上面提到的三步,中断信号的信息被qemu抽象为
qemu_irq
,数据结构如下:
struct IRQState {
Object parent_obj;
qemu_irq_handler handler; /* 该芯片断言到中断信号后的处理函数 */
void *opaque; /* 该芯片级联的上一级设备的实体信息 */
int n;
/* qemu_irq元素在gic的NamedGPIOList->in[]数组中的索引。
* 注意:这里的索引按照一定的顺序布局,在下发到kvm前会转换成真正的中断号,具体原因见下面的分析
*/
};
typedef struct IRQState *qemu_irq;
- 中断信号的处理qemu通过函数
qemu_set_irq
来模拟,如下:
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
- qemu_set_irq是逐级迭代的动作,模拟了中断信息逐级投递,直到最终到达CPU
ARM平台GPIO中断模拟
- ARM平台上,虚机使用的mach通常被配置为virt,它的主板并非现有的ARM芯片主板,因为现有主板硬件固定,无法根据用户配置动态生成,因此传递给内核的设备树也是固定的。而virt mach下QEMU可以根据用户配置动态生成设备树,该mach默认的硬件配置较少,但仍然配置了GPIO处理芯片pl061和gpio-key。连接方式如下:关机按键接到了GPIO芯片pl061的输入引脚上,pl061一共有8个引脚,每个引脚都可编程配置为输入引脚,输出引脚或者中断引脚,关机按键连接到GPIO芯片的第3个输入引脚上并配置为下降沿触发的中断模式。pl061芯片的输出引脚接到了GIC的第8根输入引脚上,GIC中断控制器的输出引脚最终连接到CPU。
- 用软件模拟中断信号处理的芯片,需要为芯片每个输入引脚定义处理函数,用于处理到来的中断信息,同时也要定义一组gpio引脚信息,用于对外提供输入接口。当有输入设备要连接芯片的某个管脚时,首先获取该芯片的gpio引脚信息,声明自己应该连接到芯片的哪个引脚上,然后获取该引脚信息关联的中断处理函数。因此,初始化中断相关的硬件时,顺序应该是从内向外的,以上面的硬件接线为例,硬件的初始化应该从GIC -> GPIO -> GPIO-KEY。下面我们以这个顺序介绍QEMU的中断处理硬件模拟
中断控制器GIC设备模拟
- GIC (Generic Interrupt Controller For ARM)
GIC的上级系统总线,GIC设备的初始化在初始化主板时进行,在machvirt_init
函数中,来看一下GIC设备数据结构初始化流程
machvirt_init
qemu_irq pic[NUM_IRQS] // 定义256个外部中断,这些都由GIC管理
/* 创建GIC设备,这个过程中会初始化没有256个中断的qemu_irq,注册handler函数
* 让所有的引脚在断言到中断后都可以执行handler回调函数
* 正常情况下,这个256个中断的handler都是同一个处理函数
* 但调用这些中断硬件处理函数的地方可能不同
* 比如一个中断的引脚是直连的外部设备,那么这个外设需要拿到这个qemu_irq
* 在外设想要模拟中断发生的时候,以GIC的qemu_irq为参数,调用qemu_set_irq
* 以此告诉GIC控制器中断来了,需要做处理了
* 而GIC的处理方式就是执行handler函数,我的环境中就是kvm_arm_gicv2_set_irq
* /
create_gic(vms, pic)
/* 获取GIC的版本,ARM规定了3个版本的GIC标准,我们的环境模拟的是v2版本 */
int type = vms->gic_version
/* 判断GIC的实现方式,GIC有两种实现方式
* 一种是在用户空间实现,中断可以由用户空间的程序发起,这需要硬件的支持
* 一种是在内核空间实现,由KVM实现,这是常规的实现方式。我的环境是这种
* 针对两种实现方式QEMU由两种设备初始化的执行流程
* 对于v2版本,内核空间实现GIC方式,GIC设备类型名字是"kvm-arm-gic"
* 用户空间GIC设备类型名字是"arm_gic"
* /
gictype = (type == 3) ? gicv3_class_name() : gic_class_name();
/* 这个接口会部分创建gic设备,什么意思?它只初始化设备的状态和数据结构
* 不会设置设备的属性,因此不会触发该设备关系树上parent链的所有回调(回调从parent到child依次执行)
* 所以,这个设备创建之后,实际还不能工作,并且它后续调用接口去设置自己的属性
* 本质上qdev_create只调用的parent链上所有设备的class_init回调,并加realize实现注册到TypeInfo中
* */
gicdev = qdev_create(NULL, gictype)
/* 正如上面说的,gicdev设备创建后,接着可以设置其属性,属性设置的本质是给QEMU的Device结构体赋值
* smp_cpus设置了GICState的num_cpu域
* NUM_IRQS + 32设置了GICState的num_irq域,如下
static Property arm_gic_common_properties[] = {
DEFINE_PROP_UINT32("num-cpu", GICState, num_cpu, 1),
DEFINE_PROP_UINT32("num-irq", GICState, num_irq, 32),
......
};
* */
qdev_prop_set_uint32(gicdev, "num-cpu", smp_cpus);
/* 设置GIC设备管理的中断号个数是 NUM_IRQS + 32 = 256 + 32 = 288 个
* 其中NUM_IRQS是外部中断,32是ARM GIC规范定义的内部中断。这两个中断都需要GIC来管理
* /
qdev_prop_set_uint32(gicdev, "num-irq", NUM_IRQS + 32)
/* 终于到了这里,这个,才是真正realizeGIC设备的地方 */
qdev_init_nofail(gicdev)
- qdev_init_nofail(gicdev)真正完成了GIC设备的初始化,这里面会设置每个中断引脚的硬件处理,布局QEMU管理的中断号,我们来看下流程
qdev_init_nofail
/* 最终触发GIC的realize回调 kvm_arm_gic_realize*/
object_property_set_bool(OBJECT(dev), true, "realized", &err)
kvm_arm_gic_realize
/* 这里我们终于看到handler作为参数传入 */
gic_init_irqs_and_mmio(s, kvm_arm_gicv2_set_irq, NULL, NULL)
/* 首先取出GIC管理的所有中断号个数,减去头32个内部中断(PPI和SGI),就是GIC可以向外设开放的中断引脚了
* 从这里我们可以看到,GICState的num_irq域确实仅仅表示管理了多少中断号,这里面既有内部的,又有外部的
*/
int i = s->num_irq - GIC_INTERNAL;
/* For the GIC, also expose incoming GPIO lines for PPIs for each CPU.
* GPIO array layout is thus:
* [0..N-1] SPIs
* [N..N+31] PPIs for CPU 0
* [N+32..N+63] PPIs for CPU 1
* ...
* 要管理i个外部中断和GIC_INTERNAL个内部中断,一共需要多少个qemu_irq数组,下面计算出来了
* i个外部中断,每一个中断都是所有CPU共享的,是SPI(Shared Peripheral Interrupt)类型的中断
* 是所有cpu共享的,所以只需要i个SPI只需要i个qemu_irq
* 但内部中断是针对特定的CPU,32个内部中断,每个CPU都需要一个qemu_irq,因此有了如下计算
*/
i += (GIC_INTERNAL * s->num_cpu)
/* 计算QEMU管理的中断需要的qemu_irq个数,目的就是设置GIC设备的gpio中断数组
* 一个Device,如果它模拟的中断处理信息或者中断控制器,它会暴露引脚给外设
* 维护这个引脚的数据结构就是DeviceState中的gpios,它是一个链表,上面的每个节点又是一个链表。
* 节点上维护的就是一组qemu_irq,但通常,这个链表只有一个节点
* struct NamedGPIOList {
char *name; // 名字,可以是NULL
qemu_irq *in; // 如果有输入引脚,这个qemu_irq是下级投递中断信息需要调用的处理函数
int num_in; // 有多少个输入引脚
int num_out; // 有多少个输出引脚
QLIST_ENTRY(NamedGPIOList) node;
};
* qdev_init_gpio_in的功能就是扩展设备的输入引脚为i个,没个输入引脚的qemu_irq都用handler来设置
* /
qdev_init_gpio_in(DEVICE(s), handler, i)
qdev_init_gpio_in_named(dev, handler, NULL, n)\
qdev_init_gpio_in_named_with_opaque
/* 首先找到name名下的管理的所有gpio,name可以是NULL,那么只要node节点的name也是NULL,还是可以匹配 */
NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, name)
/* 扩展gpio_list中的in数组,增加n个,每一个的哦用handler和opaque去初始化 */
gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in, handler, opaque, n)
/* 增加的个数记录到结构体中 */
gpio_list->num_in += n
至此,我们初始化了GIC中断相关的所有部分,把GIC的所有中断引脚都设置了handler函数,回顾GIC的中断布局,可以看到GIC->gpio_list->qemu_irq[]管理的中断布局如下,其中N就是我们真正可以管理的外部中断,它是所有cpu共享的,代码里面设置的是NUM_IRQS 256
个,同时在qemu_extend_irqs
中还初始化了qemu_irq结构体,里面的n不是中断号,而是qemu_irq在NamedGPIOList->in[]中的索引。如下图里面的apios->NamedGPIOList->qemu_irq[]所示,注意:为了描述,这个引用是随便写的。意思是根据左边的结构体可以找到右边的数组
- 设置GIC完成后,回到
create_gic
,它把gicdev中中断数组的的前NUM_IRQS
个都取出来,放到pic[]数组中去了,可以看到,这里就是取的外部中断,我们后面的pl061初始化,因为要连接到GIC上,就要从这个pic中拿handler函数,用于传递中断给上级设备,我们这里是kvm_arm_gicv2_set_irq
for (i = 0; i < NUM_IRQS; i++) {
pic[i] = qdev_get_gpio_in(gicdev, i);
}
create_v2m(vms, pic)
sysbus_connect_irq(SYS_BUS_DEVICE(dev), i, pic[irq + i])
GIPO控制器芯片PL061设备模拟
- GIC设备初始化完成后,machvirt_init就开始初始gpio设备
create_apio
/* vms->irqmap在virt_instance_init的时候被设置成了a15irqmap,这是个Cotex-A15的外部中断号分布
* 第1个是串口中断,第1个时钟中断,依次类推,第7个是GPIO中断,这里的中断号如果转换成中断向量
* 需要加上ARM内部固有的32个中断,因此VIRT_GPIO对应的中断向量是39,而NamedGPIOList->in[]的前面256都是外部中断
* 恰巧和这里对上,因此VIRT_GPIO对应的qemu_irq就是in[]数组里面的第7个,因此a15irqmap[VIRT_GPIO]存放的是GPIO控制器
* 在NamedGPIOList->in[]数组中的索引,后面我们就用这个索引号去获取qemu_irq
static const int a15irqmap[] = {
[VIRT_UART] = 1,
[VIRT_RTC] = 2,
[VIRT_PCIE] = 3, /* ... to 6 */
[VIRT_GPIO] = 7,
[VIRT_SECURE_UART] = 8,
[VIRT_MMIO] = 16, /* ...to 16 + NUM_VIRTIO_TRANSPORTS - 1 */
[VIRT_GIC_V2M] = 48, /* ...to 48 + NUM_GICV2M_SPIS - 1 */
[VIRT_SMMU] = 74, /* ...to 74 + NUM_SMMU_IRQS - 1 */
[VIRT_PLATFORM_BUS] = 112, /* ...to 112 + PLATFORM_BUS_NUM_IRQS -1 */
};
*/
int irq = vms->irqmap[VIRT_GPIO]
/* 创建模拟pl061 gpio-key控制芯片的设备,传入设备的类名,设备在系统中的物理地址,设备上报中断时要执行的qemu_irq */
pl061_dev = sysbus_create_simple("pl061", base, pic[irq])
- pl061作为中断控制芯片,和GIC一样,它也要初始化自己的输入引脚,设置handler,流程如下
pl061_dev = sysbus_create_simple("pl061", base, pic[irq])
sysbus_create_varargs
qdev_create
qdev_init_nofail
/* 创建设备对象后立即realize这个设备,最终触发pl061的初始化回调 pl061_init */
static const TypeInfo pl061_info = {
.name = TYPE_PL061,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(PL061State),
.instance_init = pl061_init,
.class_init = pl061_class_init,
};
pl061_init
/* pl061有8跟gpio引脚,分别设置其输入输出和中断handler */
qdev_init_gpio_in(dev, pl061_set_irq, 8);
qdev_init_gpio_out(dev, s->out, 8);
这里我们看到和GIC初始化类似的流程qdev_init_gpio_in(dev, pl061_set_irq, 8)
将增加8个handler到gpios
管理的in[]数组中,其hanlder
函数是pl061_set_irq
,pl061初始化完成,再回到create_gpio函数中,查看gpio-key设备的初始化。
GPIO-KEY设备模拟
-
gpio-key
紧随着pl061
的初始化,流程如下
pl061_dev = sysbus_create_simple("pl061", base, pic[irq])
/* gpio-key将pl061设备8根输入引脚中的第三根作为了它的输出引脚,由此应证了上图中的硬件连接图 */
gpio_key_dev = sysbus_create_simple("gpio-key", -1, qdev_get_gpio_in(pl061_dev, 3))
/* gpio-key设备的创建最终触发其初始化回调函数*/
static const TypeInfo gpio_key_info = {
.name = TYPE_GPIOKEY,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(GPIOKEYState),
.class_init = gpio_key_class_init,
};
gpio_key_realize
/* gpio-key设备就一个电源按键,管理的引脚就1个,因此传入的个数是1,handler是gpio_key_set_irq */
dev_init_gpio_in(dev, gpio_key_set_irq, 1)
/* 设置一个定时器,因为gpio引脚被配置成下降沿触发的中断模式
* 需要模拟两次电平输入,一次高电平一次低电平
* 所以在gpio_key_set_irq中发送高电平之后,开启定时器
* 100ms后通过执行gpio_key_timer_expired函数发送低电平
* 从而模拟下降沿中断信号
* */
s->timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, gpio_key_timer_expired, s);
设备树
- GIC
intc@8000000 {
phandle = <0x00008001>;
reg = <0x00000000 0x08000000 0x00000000 0x00010000 0x00000000 0x080a0000 0x00000000 0x00f60000>;
#redistributor-regions = <0x00000001>;
compatible = "arm,gic-v3";
ranges;
#size-cells = <0x00000002>;
#address-cells = <0x00000002>;
interrupt-controller;
#interrupt-cells = <0x00000003>;
its@8080000 {
phandle = <0x00008002>;
reg = <0x00000000 0x08080000 0x00000000 0x00020000>;
msi-controller;
compatible = "arm,gic-v3-its";
};
};
- pl061
pl061@9030000 {
phandle = <0x00008003>;
clock-names = "apb_pclk";
clocks = <0x00008000>;
interrupts = <0x00000000 0x00000007 0x00000004>;
gpio-controller;
#gpio-cells = <0x00000002>;
compatible = "arm,pl061", "arm,primecell";
reg = <0x00000000 0x09030000 0x00000000 0x00001000>;
};
- GPIO-keys
gpio-keys {
#address-cells = <0x00000001>;
#size-cells = <0x00000000>;
compatible = "gpio-keys";
poweroff {
gpios = <0x00008003 0x00000003 0x00000000>;
linux,code = <0x00000074>;
label = "GPIO Key Poweroff";
};
};
下发中断到KVM
- 从上面的分析可以看出,gpio-key的响应中断的handler是
gpio_key_set_irq
,它上一级是pl061,对应handler是pl061_set_irq
,再上一级是GIC,对应的handler是kvm_arm_gicv2_set_irq
。再回头看virt_powerdown_req
static void virt_powerdown_req(Notifier *n, void *opaque)
{
/* use gpio Pin 3 for power button event */
qemu_set_irq(qdev_get_gpio_in(gpio_key_dev, 0), 1);
}
它拿到的qemu_irq是gpio-key的,qemu_set_irq会依次触发:
- gpio_key_set_irq
- pl061_set_irq
- kvm_arm_gicv2_set_irq
- 依次分析各个函数
gpio_key_set_irq
qemu_set_irq(s->irq, 1)
/* gpio_key_set_irq的处理很简单,就是上报,因为它不是可编程的芯片,不会受虚拟机内部芯片驱动印象
* 但pl061_set_irq就比较复杂了,它的设备状态会被客户机驱动设置,客户机可以初始化,修改,甚至屏蔽pl061设备的中断
*/
pl061_set_irq
/* 如果客户端内部驱动正常,那么pl061也会正常工作,最终,它会走到kvm_arm_gicv2_set_irq*/
kvm_arm_gicv2_set_irq
kvm_arm_gic_set_irq(s->num_irq, irq, level)
- kvm_arm_gic_set_irq函数最终要通过ioctl命令字通知内核注入中断,这中间会将QEMU管理的中断号表的索引转化成kvm可以识别处理的中断信息,单独分析这个函数
void kvm_arm_gic_set_irq(uint32_t num_irq, int irq, int level)
{
/* Meaning of the 'irq' parameter:
* [0..N-1] : external interrupts
* [N..N+31] : PPI (internal) interrupts for CPU 0
* [N+32..N+63] : PPI (internal interrupts for CPU 1
* ...
* Convert this to the kernel's desired encoding, which
* has separate fields in the irq number for type,
* CPU number and interrupt number.
*/
int kvm_irq, irqtype, cpu;
/* irq < (num_irq - GIC_INTERNAL)索引对应的中断就是外部中断,因为QEMU把外部中断放在了开头,后面才是每个cpu对应的私有中断 */
if (irq < (num_irq - GIC_INTERNAL)) {
/* External interrupt. The kernel numbers these like the GIC
* hardware, with external interrupt IDs starting after the
* internal ones.
*/
irqtype = KVM_ARM_IRQ_TYPE_SPI; // 设置中断类型,SPI意味所有cpu共享的中断,就是外部中断
cpu = 0;
irq += GIC_INTERNAL; // 将索引号变成真正的中断号
} else { // 内部中断,需要根据索引号计算这个中断是投递给哪个cpu的,然后也转换中断号
/* Internal interrupt: decode into (cpu, interrupt id) */
irqtype = KVM_ARM_IRQ_TYPE_PPI;
irq -= (num_irq - GIC_INTERNAL);
cpu = irq / GIC_INTERNAL;
irq %= GIC_INTERNAL;
}
/* 这里将中断信息放在一个int变量里面,QEMU和KVM约定号这个int长度的意思,KVM收到后就会解析出来,如下图 */
kvm_irq = (irqtype << KVM_ARM_IRQ_TYPE_SHIFT)
| (cpu << KVM_ARM_IRQ_VCPU_SHIFT) | irq;
kvm_set_irq(kvm_state, kvm_irq, !!level);
}
- 下发到kvm的ioctl中断命令字有两个,一个是KVM_IRQ_LINE,一个是KVM_IRQ_LINE_STATUS,后者需要cpu支持,这里我们走的是KVM_IRQ_LINE
kvm_set_irq
kvm_vm_ioctl(s, s->irq_set_ioctl, &event)
return (s->irq_set_ioctl == KVM_IRQ_LINE) ? 1 : event.status
Q&A
Q:QEMU对GIC设备的模拟,只有设备信息的初始化,真正的中断投递和硬件状态都没有看到,这部分在哪儿实现的?
A: QEMU其实只维护GIC设备在用户态的输入信息数据结构,GIC中断提交和硬件状态的模拟在内核态实现的。
Q:既然GIC设备的硬件状态模拟在内核态,那用户态的GIC设备有什么用?
A:GIC只是一个中断控制器,处理其它外设的中断提交是它的主要工作方式,而大多数外设是在用户态模拟的,因此QEMU需要在用户态也模拟一个GIC设备,与其它硬件外设相连,以此收集中断信息。提交给内核的GIC设备处理。