虚机向virtio磁盘写入数据后,走到块设备层提交bio,最终会往virtio-blk队列的环上添加写入数据的物理地址,整个流程如下:
submit_bio
generic_make_request /* 将bio提交到块设备的工作队列上去 */
blk_mq_dispatch_rq_list /* 工作队列处理函数 */
q->mq_ops->queue_rq() /* 调用多队列入队请求的具体实现 */
virtio_queue_rq /* virtio磁盘的入队请求实现 */
__virtblk_add_req /* 将IO数据地址添加到virtio的队列上 */
virtqueue_kick_prepare /* 判断是否要通知后端 */
virtqueue_notify /* 通知 */
vq->notify() /* virtio队列的通知实现 */
vp_notify() /* 对于基于pci的virtio设备,最终调用该函数实现通知 */
vp_notify的具体实现如下:
/* the notify function used when creating a virt queue */
bool vp_notify(struct virtqueue *vq)
{
/* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, (void __iomem *)vq->priv);
return true;
}
从这里看,notify的动作就是往队列的一个priv成员中写入队列的idx。这个priv成员在哪儿初始化的?看下面:
static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
struct virtio_pci_vq_info *info,
unsigned index,
void (*callback)(struct virtqueue *vq),
const char *name,
u16 msix_vec)
{
......
/* create the vring */
vq = vring_create_virtqueue(index, num, /* 1 */
SMP_CACHE_BYTES, &vp_dev->vdev,
true, true, vp_notify, callback, name);
vq->priv = (void __force *)vp_dev->notify_base + off * vp_dev->notify_offset_multiplier; /* 2 */
......
}
1. 创建virtio磁盘列队,为vring分配空间,并将其挂载到队列上。函数传入了一个vp_notify回调函数,这个函数就是在Guest添加buffer后要调用的
通知后端的notify函数
2. 设置notify函数中要写入的pci地址,这个地址的计算依据是virtio规范
virtio设备的PCI空间中,virtio_pci_cap_notify_cfg是专门用作前端通知的cap,通过读取这个配置空间的信息,可以计算出通知后端时前端写入的地址,整个virtio-pci的配置空间如下:
virtio中关于notify写入地址的计算方法介绍如下:
从规范的介绍来看,notify地址是notify cap在bar空间内的偏移,加上common cap的queue_notify_off字段与notify cap的notify_off_multiplier的乘积。再看一次之前的地址计算公式,就是规范里面介绍的计算方法
vq->priv = (void __force *)vp_dev->notify_base + off * vp_dev->notify_offset_multiplier
前端往notify地址写入数据后,由于这是外设的空间,写操作被认为是敏感指令,触发VM-exit,首先查看前端notify virtio磁盘时要写的地址区间。
virsh qemu-monitor-command vm --hmp info pci
virsh qemu-monitor-command vm --hmp info mtree
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_exit/enable
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_fast_mmio/enable
/*
* Tracepoint for kvm guest exit:
*/
TRACE_EVENT(kvm_exit,
TP_PROTO(unsigned int exit_reason, struct kvm_vcpu *vcpu, u32 isa),
TP_ARGS(exit_reason, vcpu, isa),
......
TP_printk("reason %s rip 0x%lx info %llx %llx", /* 1 */
(__entry->isa == KVM_ISA_VMX) ?
__print_symbolic(__entry->exit_reason, VMX_EXIT_REASONS) :
__print_symbolic(__entry->exit_reason, SVM_EXIT_REASONS),
__entry->guest_rip, __entry->info1, __entry->info2)
);
/*
* Tracepoint for fast mmio.
*/
TRACE_EVENT(kvm_fast_mmio,
TP_PROTO(u64 gpa),
TP_ARGS(gpa),
......
TP_printk("fast mmio at gpa 0x%llx", __entry->gpa) /* 2 */
);
1. kvm_exit的输出信息分别是:退出原因,引发退出的指令地址,VM-Exit退出时VMCS VM-Exit相关信息,如下:
info1:EXIT_QUALIFICATION,记录触发VM-Exit的指令或者异常
info2:VM_EXIT_INTR_INFO,记录触发VM-Exit的中断信息
2. kvm_exit如果是EPT violation或者EPT misconfiguration引起的,会将引起退出的物理地址放到VMCS VM-Exit的GUEST_PHYSICAL_ADDRESS
字段,这里就是打印这个字段里面的值