4. 中断处理测试

任务说明

本案例在QEMU中创建一个自定义PCI虚拟设备,该设备具备中断触发能力。客户机通过MMIO寄存器写入触发命令后,设备向中断控制器发送中断请求,客户机内核中的中断处理程序响应并记录中断次数。通过对比触发次数与客户机内记录的中断次数,验证虚拟设备中断处理流程的正确性。

虚拟设备实现

创建设备文件 interrupt-test-device.c,实现一个简单的PCI设备:


#include "qemu/osdep.h"
#include "hw/pci/pci_device.h"
#include "hw/pci/msi.h"
#include "qemu/log.h"

#define TYPE_INTERRUPT_TEST_DEVICE "interrupt-test-device"
#define INTERRUPT_TEST_DEVICE(obj) \
    OBJECT_CHECK(InterruptTestDevice, (obj), TYPE_INTERRUPT_TEST_DEVICE)

#define INT_TEST_MMIO_BAR 0
#define INT_TEST_MMIO_SIZE 0x1000
#define REG_TRIGGER 0x00
#define REG_STATUS  0x04
#define REG_IRQ_COUNT 0x08

typedef struct {
    PCIDevice parent_obj;
    MemoryRegion mmio;
    uint32_t irq_count;
    uint32_t status;
} InterruptTestDevice;

static void int_test_update_irq(InterruptTestDevice *dev)
{
    /* 当status的第0位为1时,触发中断 */
    int level = (dev->status & 0x1) ? 1 : 0;
    pci_set_irq(&dev->parent_obj, level);
}

static uint64_t int_test_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
    InterruptTestDevice *dev = opaque;
    uint32_t val = 0;

    switch (addr) {
    case REG_STATUS:
        val = dev->status;
        break;
    case REG_IRQ_COUNT:
        val = dev->irq_count;
        break;
    default:
        val = 0;
        break;
    }
    return val;
}

static void int_test_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
    InterruptTestDevice *dev = opaque;

    switch (addr) {
    case REG_TRIGGER:
        /* 写入任意值触发一次中断 */
        dev->irq_count++;
        dev->status |= 0x1;
        int_test_update_irq(dev);
        break;
    case REG_STATUS:
        /* 写入1清除中断状态 */
        if (val & 0x1) {
            dev->status &= ~0x1;
            int_test_update_irq(dev);
        }
        break;
    default:
        break;
    }
}

static const MemoryRegionOps int_test_mmio_ops = {
    .read = int_test_mmio_read,
    .write = int_test_mmio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .impl = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

static void int_test_realize(PCIDevice *pci_dev, Error **errp)
{
    InterruptTestDevice *dev = INTERRUPT_TEST_DEVICE(pci_dev);

    memory_region_init_io(&dev->mmio, OBJECT(dev), &int_test_mmio_ops,
                          dev, "int-test-mmio", INT_TEST_MMIO_SIZE);
    pci_register_bar(pci_dev, INT_TEST_MMIO_BAR,
                     PCI_BASE_ADDRESS_SPACE_MEMORY, &dev->mmio);
}

static void int_test_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);

    k->realize = int_test_realize;
    k->vendor_id = 0x1234;
    k->device_id = 0x5678;
    k->class_id = PCI_CLASS_OTHERS;
    dc->desc = "Interrupt Test Device";
}

static const TypeInfo int_test_info = {
    .name = TYPE_INTERRUPT_TEST_DEVICE,
    .parent = TYPE_PCI_DEVICE,
    .instance_size = sizeof(InterruptTestDevice),
    .class_init = int_test_class_init,
    .interfaces = (InterfaceInfo[]) {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { }
    },
};

static void int_test_register_types(void)
{
    type_register_static(&int_test_info);
}

type_init(int_test_register_types)

客户机驱动与测试程序

在客户机内编写驱动 int_test_drv.c,注册中断处理程序并暴露中断次数:


#include <linux/module.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/io.h>

#define INT_TEST_VENDOR 0x1234
#define INT_TEST_DEVICE 0x5678
#define REG_TRIGGER 0x00
#define REG_STATUS  0x04
#define REG_IRQ_COUNT 0x08

static void __iomem *mmio_base;
static int irq_line;
static atomic_t irq_received = ATOMIC_INIT(0);

static irqreturn_t int_test_isr(int irq, void *dev_id)
{
    atomic_inc(&irq_received);
    /* 清除设备中断状态 */
    iowrite32(0x1, mmio_base + REG_STATUS);
    return IRQ_HANDLED;
}

static int int_test_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;

    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    ret = pci_request_regions(pdev, "int_test");
    if (ret)
        goto err_disable;

    mmio_base = pci_iomap(pdev, 0, 0x1000);
    if (!mmio_base) {
        ret = -ENOMEM;
        goto err_regions;
    }

    ret = request_irq(pdev->irq, int_test_isr, IRQF_SHARED,
                      "int_test", &irq_line);
    if (ret)
        goto err_unmap;

    irq_line = pdev->irq;
    pci_set_drvdata(pdev, &irq_line);
    return 0;

err_unmap:
    pci_iounmap(pdev, mmio_base);
err_regions:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

static void int_test_remove(struct pci_dev *pdev)
{
    free_irq(pdev->irq, &irq_line);
    pci_iounmap(pdev, mmio_base);
    pci_release_regions(pdev);
    pci_disable_device(pdev);
}

static const struct pci_device_id int_test_ids[] = {
    { PCI_DEVICE(INT_TEST_VENDOR, INT_TEST_DEVICE) },
    { 0 }
};
MODULE_DEVICE_TABLE(pci, int_test_ids);

static struct pci_driver int_test_driver = {
    .name = "int_test",
    .id_table = int_test_ids,
    .probe = int_test_probe,
    .remove = int_test_remove,
};

module_pci_driver(int_test_driver);

/* 导出中断次数读取接口 */
static int __init int_test_init(void)
{
    return 0;
}

static void __exit int_test_exit(void)
{
}

module_init(int_test_init);
module_exit(int_test_exit);
MODULE_LICENSE("GPL");

客户机内测试程序 test_int.c:


#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define PCI_DEV_PATH "/sys/bus/pci/devices/0000:00:0b.0/resource0"
#define REG_TRIGGER 0x00
#define REG_STATUS  0x04
#define REG_IRQ_COUNT 0x08

int main(void)
{
    int fd = open(PCI_DEV_PATH, O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    void *mmio = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, 0);
    if (mmio == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    /* 读取设备侧触发计数 */
    uint32_t dev_count = *(volatile uint32_t *)((char *)mmio + REG_IRQ_COUNT);
    printf("Device IRQ count before: %u\n", dev_count);

    /* 触发3次中断 */
    for (int i = 0; i < 3; i++) {
        *(volatile uint32_t *)((char *)mmio + REG_TRIGGER) = 1;
        usleep(100000); /* 等待中断处理完成 */
    }

    /* 读取设备侧计数 */
    dev_count = *(volatile uint32_t *)((char *)mmio + REG_IRQ_COUNT);
    printf("Device IRQ count after: %u\n", dev_count);

    /* 读取驱动记录的中断次数 */
    FILE *fp = fopen("/proc/interrupts", "r");
    if (!fp) {
        perror("fopen");
        munmap(mmio, 0x1000);
        close(fd);
        return 1;
    }

    char line[256];
    int found = 0;
    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, "int_test")) {
            printf("Driver IRQ info: %s", line);
            found = 1;
            break;
        }
    }
    fclose(fp);

    if (!found)
        printf("int_test not found in /proc/interrupts\n");

    munmap(mmio, 0x1000);
    close(fd);
    return 0;
}

编译与运行

QEMU侧编译设备:


# 在QEMU源码目录下
./configure --target-list=x86_64-softmmu
make
# 将interrupt-test-device.c放入hw/misc/并添加到对应Makefile.objs

启动QEMU并挂载设备:


qemu-system-x86_64 \
    -kernel /path/to/bzImage \
    -initrd /path/to/initrd.img \
    -append "root=/dev/ram0 console=ttyS0" \
    -device interrupt-test-device \
    -nographic

客户机内编译驱动与测试:


# 客户机内
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
insmod int_test_drv.ko
gcc -o test_int test_int.c
./test_int

验证方法与判定标准

执行测试程序后,观察以下输出并判定:

  • 设备侧 REG_IRQ_COUNT 在触发后应为3(与触发次数一致);
  • /proc/interrupts 中 int_test 对应的中断计数应增加3次;
  • 若设备侧计数为3且驱动中断计数为3,则中断触发、传递、处理、状态清除全流程正确,测试通过。
客户机用户态 test_int (mmap + 写触发) 客户机内核 int_test_isr (记录+清状态) /proc/interrupts 计数 QEMU 虚拟设备 MMIO 读写处理 irq_count 寄存器 KVM + 中断控制器 中断注入 中断线状态跟踪 写REG_TRIGGER pci_set_irq(1) 中断向量 写REG_STATUS清中断 读REG_IRQ_COUNT 验证观测点 设备侧 irq_count 寄存器值 = 触发次数 客户机 /proc/interrupts 中 int_test 计数增加相同次数 ISR 正确清除状态,设备不会重复触发中断

预期输出示例


Device IRQ count before: 0
Device IRQ count after: 3
Driver IRQ info:  0:  3  IO-APIC  8-edge  int_test

以上输出中设备侧计数为3,驱动中断计数为3,两者一致即判定测试通过。