5.14.2.4. 中断

在qemu中,中断本质是cpu_exec()过程中的一个定期判断(如果是KVM一类的真正执行就靠KVM本身的硬件机制了,那个原理可以自然想像)。

qemu通过cpu_reset_interrupt/cpu_interrupt()把一个标记种入到CPU中,CPU执行中就可以检查这个标记,发现有中断的要求,就调用一个回调让中断控制器backend修改CPU状态,之后CPU就在新的状态上执行了。(KVM等硬件配合的加速方式会有性能更高的手段,但可以很容易想象其原理。)

所以,模拟中断最原始的方法是你强行调用cpu_reset_interrupt()和cpu_interrupt()。

但很少硬件会这么简单,因为只靠中断你没法判断中断源。虽然cpu_interrup()可以带一个mask参数区分中断类型,但这通常用于区分不同的特权级的中断,没法靠一个mask就表示数十甚至数百数千的中断源的。所以,大部分CPU需要通过中断控制器来控制。中断控制器是个设备(比如qdev),具体怎么做就要靠硬件了。

可能是历史原因,qemu习惯上把硬件的中断传递看作是一个gqio行为。大部分平台的实现就是把产生中断看作是给对应的中断线(作为gpio)发信号。

比如RISCV就是这样的:

static void sifive_plic_irq_request(void *opaque, int irq, int level) {
     plic_dev = opaque;
     ...
     cpu_interrupt(); //给对应的CPU发中断,是哪个CPU看plic算法了
     ...
}
qdev_init_gpio_in(plic_dev, sifive_plic_irq_request, plic->num_sources);

这里给sifive的中断控制器(PLIC)CPU创建了plic->num_sources条输入的中断线,这些中断线收到信号后,调用回调函数,sifive_plic_irq_request,从中决定具体激活哪个CPU的中断。qdev_init_gpio_in()会把这些gpio_in记录在DeviceState中,你可以通过qdev本身找到它们。

之后,你要给这个中断线发信号,你可以在你的硬件对象上创建一个gpio_out的对象,然后用qdev_connect_gpio_out()连这个引线。这个逻辑是这样的::

qemu_irq *irqs = qemu_allocate_irqs(..., n);  // 创建n个irq
qdev_connect_gpio_out(plic_dev, x, irqs[y]);     // 把创建的第y个qemu_irqx连到plic_dev的第x条中断线上

之后,你可以用对应的qemu_irq来通知到CPU上了。

对qemu_irq发通知的函数主要有这样一些:

void qemu_irq_raise(qemu_irq irq);
void qemu_irq_lower(qemu_irq irq);
void qemu_irq_pulse(qemu_irq irq);
void pci_irq_assert(PCIDevice *pci_dev);
void pci_irq_deassert(PCIDevice *pci_dev);
void pci_irq_pulse(PCIDevice *pci_dev);

如果用的是PCI MSI/MSI-X,则中断触发通过msi_notify()来做。按MSI/MSI-X的原理,这个行为实际上就是根据MSX PCI配置,在对应的内存地址中写入要求的参数,这个内存地址写入的过程通过MR的翻译,最终会匹配到中断控制器的io写上,最后还是那组qemu_set_irq()调用。

备注

一点调试经验:qemu_irq_pulse()本质就是先raise在lower,但不要以为你拉起来过就一定会产生中断,如果你在OS这一侧认为是电平中断,那么qemu_irq_pulse()很可能不会触发OS的任何反应。

如果用的是平台设备,大部分平台的适配代码已经在qdev上已经把平台总线适配上了,所以你只要在平台设备中用sysbus_init_irq(state, irq_no)注册中断号就可以了。具体中断号是多少,则需要找到这个平台的Machine代码来看。