VirtIO实现原理——前端通知机制

流程介绍

虚机向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

VM-exit

前端往notify地址写入数据后,由于这是外设的空间,写操作被认为是敏感指令,触发VM-exit,首先查看前端notify virtio磁盘时要写的地址区间。

  • 后端查看virtio磁盘的pci配置空间物理地址,notify的cap位于BAR4,BAR4的物理区间是0xfe008000~0xfe00bfff
    virsh qemu-monitor-command vm --hmp info pci
    在这里插入图片描述
  • 后端查看notify配置空间在BAR4占用的物理区间0xfe00b000~0xfe00bfff
    virsh qemu-monitor-command vm --hmp info mtree
    在这里插入图片描述
  • 从上面可以知道,前端如果往virtio磁盘上添加buffer之后,notify要写入的地址区间是0xfe00b000~0xfe00bfff,使用gdb跟踪vp_notify的流程,可以看到最终notify会往地址为0xffffc900003b8000的内存空间写0,该地址就是pci总线域notify地址通过ioremap映射到存储器域的地址,理论上,访问这个地址就是访问pci的notify地址
    在这里插入图片描述
  • 继续单指令调试,下面这条mov指令是真正的写io空间的操作,执行的时候触发VM-Exit
    在这里插入图片描述
  • 在执行mov指令之前,打开主机上的kvm的两个trace点,获取VM-Exit中KVM的日志打印
    echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_exit/enable
    echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_fast_mmio/enable
    在这里插入图片描述
  • 两个trace点输出信息在内核的定义如下:
/*
 * 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
字段,这里就是打印这个字段里面的值
  • 追踪VM-Exit流程,可以知道notify引发的退出,被归类为EPT misconfiguration,是由于访问内存异常产生的退出。如果虚机EPT页不存在,触发的是EPT violation异常,KVM会进行缺页处理。如果页存在但访问权限不对,才触发EPT misconfiguration,这里是虚机没有这个地址的访问权限。从而触发VM-Exit。

KVM缺页处理

  • 当客户机因为缺页异常而退出后,KVM有两种处理方式,第一种是常规的方式,针对引起缺页的GPA,填充其对应的EPT页表并将信息同步给qemu进程的页表,这时KVM处理客户机缺页的最常见流程。另外一种,就是本文讨论的情况,虚机访问的物理地址不是普通的内存,是一个PCI的配置空间,这种类型的内存,在qemu的分类中是MMIO(Memory-map ),当虚机读写这类内存的时候,KVM检查到之后通过ioeventfd通知qemu,具体原理参考qemu中的eventfd机制
  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Linux上,virtio的具体实现涉及到几个关键的结构和函数。首先,在virtio驱动中,使用了virtio_device_id结构来匹配设备,并在virtio_driver结构中包含了支持的virtio_device_id列表。\[1\]其次,对于特定的虚拟设备,比如virtio-net网卡驱动,它的实现包括了网卡驱动和virtio操作。\[2\]在创建virtqueue时,核心流程包括了一系列的操作,其中重点是核心流程的梳理。\[2\]最后,对于挂接在virtio总线上的设备,对应了struct virtio_device结构。对于virtio-net设备,它提供了自己的驱动即struct virtio_driver virtio_net_driver。当virtio-net设备挂接到virtio总线上或者virtio_net_driver注册到virtio总线上时,会调用virtio bus的探测函数virtio_dev_probe来找到驱动探测函数virtnet_probe。最终,通过register_netdev()函数将网络设备注册到Linux网络协议栈中。\[3\]这些是virtio在Linux上的具体实现的一些关键点。 #### 引用[.reference_title] - *1* *2* [virtio-net 实现机制【一】(图文并茂)](https://blog.csdn.net/m0_74282605/article/details/128114444)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Linux virtio-net driver](https://blog.csdn.net/lingshengxiyou/article/details/127771119)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值