背景
在鲲鹏云主机适配lava项目中,测试发现带lava盘(qemu与spdk存储后端通过vfio-user连接)的虚拟机,在热迁移后,guest里面nvme盘不能响应中断。
关键词:
鲲鹏:ARMv8架构
vfio-user: qemu与spdk通过vfio-user协议连接
测试与现象
- 热迁移源端,目的端都没有明显错误打印。
- 目的端guest内nvme驱动正常加载,lsblk正常显示nvme设备。
- 命令file -s /dev/nvme0n1会卡住,过一会内核打印
之后正常显示文件信息。这个时间比较久,严重影响用户体验。
- 虚拟机内reboot操后,nvme盘恢复正常。
- x86架构下相同条件vm热迁移没有发生这个问题。
分析与排查
从上面第三条信息看,虽然获取文件信息的时间比较长,但是最后还是获取成功了,说明guest nvme驱动,qemu, spdk之间的数据通路应该是正常的。guest nvme驱动通知后端的路径是正常的,但是spdk后端通知guest的路径可能出了问题。为啥这里出了问题file命令还能得到正确输出呢?存储同学说nvmeq驱动在request发生timeout之后还是会尝试捞一把数据,正确输出说明数据的确是送过来了,是中断没有正常送上去。
中断是如何投递给guest的
spdk和qemu在不同的地址空间,所以spdk在完成request后,想要快速得投递中断给guest,必须使用irqfd。irqfd是qemu这边创建的,创建之后会通过ioctl注册到kvm, 再通过vfio-user msg注册到spdk。我们可以追踪这个过程是否正常执行了。因为irqfd作为一个文件描述符,通过sendmsg接口传输给另一个进程,fd号是会发生变化的,所以无法根据fd号判断是否正常,但只要recvmsg没有报错,大概率就是正常的。接着需要确认spdk侧再处理完request之后是否正常写了对应queue的irqfd。kvm是否正常执行了kvm_set_irq函数。如果正常执行了kvm_set_irq函数,guest里面还没有收到中断,那可能就是arm中断控制器(vgic)流程有问题。
kvm_set_irq函数执行了吗
比较关键的是在guest触发读写nvme请求后,负责给guest注入中断的host kvm的kvm_set_irq函数有没有正常执行到。
对于kvm_set_irq,内核是有一个tracepoint的,我们可以方便得看到这个函数有没有被执行到。当内核执行到这个函数后,会打印gsi号,level电平值,以及irq_source_id。我们可以使能这个tracepoint, 然后在guest里触发对nvme盘的读写操作,然后查看kvm_set_irq函数有没有执行到。
#if defined(CONFIG_HAVE_KVM_IRQFD)
TRACE_EVENT(kvm_set_irq,
TP_PROTO(unsigned int gsi, int level, int irq_source_id),
TP_ARGS(gsi, level, irq_source_id),
TP_STRUCT__entry(
__field( unsigned int, gsi )
__field( int, level )
__field( int, irq_source_id )
),
TP_fast_assign(
__entry->gsi = gsi;
__entry->level = level;
__entry->irq_source_id = irq_source_id;
),
TP_printk("gsi %u level %d source %d",
__entry->gsi, __entry->level, __entry->irq_source_id)
);
#endif /* defined(CONFIG_HAVE_KVM_IRQFD) */
因为这个gsi号是vm内唯一的,最好把其他虚拟机停掉,避免干扰。为了避免虚拟机内其他中断的干扰,我们把qemu里vfio设备注册中断的gsi都打印出来。
diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
index 38023f79e5..f452d6391b 100644
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -2043,6 +2043,7 @@ static int kvm_irqchip_assign_irqfd(KVMState *s, int fd, int rfd, int virq,
return -ENOSYS;
}
+ printf("assign irqfd: fd %d, gsi %d, flags %d\n", irqfd.fd, irqfd.gsi, irqfd.flags);
return kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index 9c08252948..597ae028bb 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -418,8 +418,10 @@ static int vfio_enable_vectors(VFIOPCIDevice *vdev, bool msix)
if (vdev->msi_vectors[i].virq < 0 ||
(msix && msix_is_masked(&vdev->pdev, i))) {
fd = event_notifier_get_fd(&vdev->msi_vectors[i].interrupt);
- } else {
+ printf("debug1: interrupt fd %d \n", fd);
+ } else {
fd = event_notifier_get_fd(&vdev->msi_vectors[i].kvm_interrupt);
+ printf("debug2: kvm_interrupt fd %d \n", fd);
}
}
@@ -427,6 +429,12 @@ static int vfio_enable_vectors(VFIOPCIDevice *vdev, bool msix)
}
ret = VDEV_SET_IRQS(&vdev->vbasedev, irq_set);
+ printf("!!!!!VDEV_SET_IRQS: index = %d, count = %d, flags = %d\n", irq_set->index, irq_set->count, irq_set->flags);
+ for (i = 0; i < vdev->nr_vectors; i++) {
+ printf("%d ", fds[i]);
+ }
+ printf("\n");
+
g_free(irq_set);
我们期望看到qemu正常使用了irqfd(打印kvm_interrupt fd
),并且得到了vfio设备(当前只有这个lava盘)的irqfd对应的gsi号。
然后我们在guest里面执行file -s /dev/nvme0n1, 观察内核的trace,看到多了以下两条打印信息,这个gsi号和之前qemu里打印的vfio设备注册的gsi号是可以对应起来的,证明正常得触发了KVM的中断注入函数kvm_set_irq,即spdk在完成request之后正常写了irqfd。
追踪kvm_set_irq流程
kvm_set_irq都执行了,guest还没有收到中断,只能追踪内核流程看有无线索。函数调用链
kvm_set_irq
kvm_set_msi
kvm_populate_msi //这里获取msi信息
vgic_its_inject_msi
vgic_its_trigger_msi(kvm, its, msi->devid, msi->data); //这里传入msi->devid是0
vgic_its_resolve_lpi
find_ite
find_its_device //return NULL
通过添加内核打印发现:
kvm_set_irq未能成功执行直接原因:find_its_device返回NULL
/*
* Find and returns a device in the device table for an ITS.
* Must be called with the its_lock mutex held.
*/
static struct its_device *find_its_device(struct vgic_its *its, u32 device_id)
{
struct its_device *device;
list_for_each_entry(device, &its->device_list, dev_list)
if (device_id == device->device_id)
return device;
return NULL;
}
为啥找不到这个设备呢?这个设备是在哪里添加到链表中的?是不是热迁移后表象没有正常恢复?另外打印发现传入的device_id是0,这里也值得怀疑。
/* Must be called with its_lock mutex held */
static struct its_device *vgic_its_alloc_device(struct vgic_its *its,
u32 device_id, gpa_t itt_addr,
u8 num_eventid_bits)
{
struct its_device *device;
device = kzalloc(sizeof(*device), GFP_KERNEL);
if (!device)
return ERR_PTR(-ENOMEM);
device->device_id = device_id;
device->itt_addr = itt_addr;
device->num_eventid_bits = num_eventid_bits;
INIT_LIST_HEAD(&device->itt_head);
list_add_tail(&device->dev_list, &its->device_list);
return device;
}
我们在这个函数里面添加打印,打印出注册设备的device_id,发现都不是0, 所以问题出在传入的device_id是0上。
这个device_id是从哪里拿到的呢?从之前的函数调用链找,是从irq_routing_entry拿到的。
static void kvm_populate_msi(struct kvm_kernel_irq_routing_entry *e,
struct kvm_msi *msi)
{
msi->address_lo = e->msi.address_lo;
msi->address_hi = e->msi.address_hi;
msi->data = e->msi.data;
msi->flags = e->msi.flags;
msi->devid = e->msi.devid;
}
所以应该是qemu注册irq_routing表项的时候传入了错误的devid;
int kvm_irqchip_update_msi_route(KVMState *s, int virq, MSIMessage msg,
PCIDevice *dev)
{
struct kvm_irq_routing_entry kroute = {};
if (kvm_gsi_direct_mapping()) {
return 0;
}
if (!kvm_irqchip_in_kernel()) {
return -ENOSYS;
}
kroute.gsi = virq;
kroute.type = KVM_IRQ_ROUTING_MSI;
kroute.flags = 0;
kroute.u.msi.address_lo = (uint32_t)msg.address;
kroute.u.msi.address_hi = msg.address >> 32;
kroute.u.msi.data = le32_to_cpu(msg.data);
if (pci_available && kvm_msi_devid_required()) {
kroute.flags = KVM_MSI_VALID_DEVID;
kroute.u.msi.devid = pci_requester_id(dev);
}
if (kvm_arch_fixup_msi_route(&kroute, msg.address, msg.data, dev)) {
return -EINVAL;
}
trace_kvm_irqchip_update_msi_route(virq);
return kvm_update_routing_entry(s, &kroute);
}
看到kroute.u.msi.devid = pci_requester_id(dev);
为啥迁移到目的端,这里这个pci_requester_id(dev)
就返回0了呢?看一下这个函数的实现。
pci_requester_id ->
pci_req_id_cache_extract ->
pci_get_bdf
pci_requester_id是根据pci device的bdf得到的。bdf应该是pci conf信息的一部分,应该在热迁移前后被保存与恢复,为啥目的端变成0了呢?
static inline uint16_t pci_get_bdf(PCIDevice *dev)
{
return PCI_BUILD_BDF(pci_bus_num(pci_get_bus(dev)), dev->devfn);
}
static inline PCIBus *pci_get_bus(const PCIDevice *dev)
{
return PCI_BUS(qdev_get_parent_bus(DEVICE(dev)));
}
BusState *qdev_get_parent_bus(DeviceState *dev)
{
return dev->parent_bus;
}
添加打印看,目的端bus号变成了0;从上面代码猜测,bus号是根据parent pcidevice的信息得到的。是否是需要保存parent pci设备的config信息?另外热迁移后的virtio设备是正常的,我们看一下virtio设备热迁移有没有save, load parent dev信息的操作
virtio_save
k->save_config(qbus->parent, f) //virtio_pci_save_config
pci_device_save(&proxy->pci_dev, f); //save parent pci dev
所以我们对vfio设备也做类似的操作
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -2472,6 +2472,10 @@ const VMStateDescription vmstate_vfio_pci_config = {
static void vfio_pci_save_config(VFIODevice *vbasedev, QEMUFile *f)
{
VFIOPCIDevice *vdev = container_of(vbasedev, VFIOPCIDevice, vbasedev);
+ PCIDevice *pdev = &vdev->pdev;
+ BusState *qbus = qdev_get_parent_bus(DEVICE(pdev));
+
+ pci_device_save(PCI_DEVICE(qbus->parent), f);
vmstate_save_state(f, &vmstate_vfio_pci_config, vdev, NULL);
}
@@ -2480,8 +2484,14 @@ static int vfio_pci_load_config(VFIODevice *vbasedev, QEMUFile *f)
{
VFIOPCIDevice *vdev = container_of(vbasedev, VFIOPCIDevice, vbasedev);
PCIDevice *pdev = &vdev->pdev;
+ BusState *qbus = qdev_get_parent_bus(DEVICE(pdev));
int ret;
+ ret = pci_device_load(PCI_DEVICE(qbus->parent), f);
+ if (ret) {
+ return ret;
+ }
+
ret = vmstate_load_state(f, &vmstate_vfio_pci_config, vdev, 1);
if (ret) {
return ret;
修改后目的端guest nvme设备能正常收到中断。问题得到解决。
另外,为啥x86上没有这个问题?
/**
* kvm_msi_devid_required:
* Returns: true if KVM requires a device id to be provided while
* defining an MSI routing entry.
*/
#define kvm_msi_devid_required() (kvm_msi_use_devid)
这个参数默认是false, 目前只有arm使能了。