在嵌入式系统和服务器领域,看门狗(Watchdog)是个不可或缺的安全机制。它就像个尽职尽责的保安,当系统长时间没有响应时,会自动触发重启防止系统死锁。但在虚拟化环境中,我们常常需要模拟这种硬件行为来测试系统可靠性。
我最近在做一个云原生项目的测试时,就遇到了这样的需求:需要在QEMU虚拟机里模拟硬件看门狗,验证我们的服务能否正确处理超时和重启。市面上的物理看门狗设备动辄上千元,而用QEMU虚拟一个不仅零成本,还能灵活调整参数。更重要的是,这个过程中能深入理解PCIe设备的虚拟化原理。
建议使用清华镜像源获取代码,速度更快:
bash复制git clone https://mirrors.tuna.tsinghua.edu.cn/git/qemu.git
cd qemu
git checkout v7.2.8 # 选择稳定版本
这个版本在Ubuntu 18.04上验证过,兼容性较好。我在CentOS 7上也测试过,需要额外安装一些依赖:
bash复制yum install ninja-build glib2-devel pixman-devel -y
编译配置直接影响后续开发效率,这是我的推荐配置:
bash复制mkdir build && cd build
../configure --target-list=x86_64-softmmu \
--enable-debug \
--enable-kvm \
--prefix=$HOME/qemu-install
关键参数说明:
--enable-debug:保留调试符号--enable-kvm:启用硬件加速prefix:指定安装目录避免污染系统遇到依赖问题可以尝试:
bash复制apt-get build-dep qemu # Ubuntu/Debian
QEMU源码中的hw/misc/edu.c是个绝佳的学习样本。这个教学设备虽然简单,但包含了PCIe设备的所有关键要素:
我们的看门狗设备可以复用这个框架,主要修改点在:
在hw/watchdog/目录下新建wdt_demo.c,初始结构如下:
c复制#include "qemu/osdep.h"
#include "hw/pci/pci.h"
#define TYPE_WDT_DEMO "wdt-demo"
#define WDT_DEMO(obj) OBJECT_CHECK(WDTDemoState, (obj), TYPE_WDT_DEMO)
typedef struct {
PCIDevice pdev;
MemoryRegion io;
QEMUTimer *timer;
uint32_t timeout;
} WDTDemoState;
static void wdt_timer_cb(void *opaque) {
WDTDemoState *s = opaque;
printf("Watchdog timeout!\n");
// 这里添加系统复位逻辑
}
记得修改meson.build添加编译配置:
meson复制softmmu_ss.add(when: 'CONFIG_WDT_DEMO', if_true: files('wdt_demo.c'))
完整的设备初始化需要三个关键步骤:
c复制static void wdt_realize(PCIDevice *pdev, Error **errp) {
WDTDemoState *s = WDT_DEMO(pdev);
// 1. 配置PCI头
pci_config_set_vendor_id(pdev->config, 0x1234); // 自定义厂商ID
pci_config_set_device_id(pdev->config, 0x5678);
// 2. 分配BAR空间
memory_region_init_io(&s->io, OBJECT(s), &wdt_ops, s, "wdt-io", 0x100);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_IO, &s->io);
// 3. 初始化定时器
s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, wdt_timer_cb, s);
}
定义MemoryRegionOps处理寄存器读写:
c复制static const MemoryRegionOps wdt_ops = {
.read = wdt_io_read,
.write = wdt_io_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
static uint64_t wdt_io_read(void *opaque, hwaddr addr, unsigned size) {
WDTDemoState *s = opaque;
switch (addr) {
case 0x00: return s->timeout;
default: return 0;
}
}
static void wdt_io_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
WDTDemoState *s = opaque;
switch (addr) {
case 0x00:
s->timeout = val;
timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
val * 1000000); // 毫秒转纳秒
break;
}
}
QEMU使用TypeInfo系统管理设备:
c复制static void wdt_class_init(ObjectClass *klass, void *data) {
DeviceClass *dc = DEVICE_CLASS(klass);
PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
k->realize = wdt_realize;
k->vendor_id = 0x1234;
k->device_id = 0x5678;
k->revision = 0x01;
k->class_id = PCI_CLASS_SYSTEM_OTHER;
}
static const TypeInfo wdt_info = {
.name = TYPE_WDT_DEMO,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(WDTDemoState),
.class_init = wdt_class_init,
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
},
};
static void wdt_register_types(void) {
type_register_static(&wdt_info);
}
type_init(wdt_register_types)
编译后启动虚拟机:
bash复制./qemu-system-x86_64 -device wdt-demo -monitor stdio
在QEMU monitor中验证设备:
code复制(qemu) info qtree
...
dev: wdt-demo, id ""
gpio-out "" 1
mmio 00000000fed1c000/0000000000000400
在Guest系统中用lspci查看:
bash复制lspci -vnn | grep "1234:5678"
看门狗超时需要触发中断,添加MSI支持:
c复制static void wdt_timer_cb(void *opaque) {
WDTDemoState *s = opaque;
pci_irq_assert(&s->pdev); // 触发中断
}
// 在realize中添加
if (msi_init(pdev, 0, 1, true, false, errp)) {
return;
}
模拟硬件复位需要QEMU特殊接口:
c复制#include "sysemu/reset.h"
static void wdt_trigger_reset(void *opaque) {
qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
}
// 在定时器回调中调用
qemu_register_reset(wdt_trigger_reset, NULL);
编译时保留调试符号后:
bash复制gdb --args ./qemu-system-x86_64 -device wdt-demo
(gdb) b wdt_io_write
在设备代码中添加trace点:
c复制#include "trace.h"
// 在wdt_io_write中添加
trace_wdt_set_timeout(val);
需要创建trace-events文件:
code复制wdt_demo wdt_set_timeout "Watchdog timeout set to %" PRIu64
在真实项目中,我建议:
一个完整的看门狗设备还应该:
遇到设备不识别的问题时,先检查: