1 virtio原理

拿网络驱动部分做案例,网络驱动virtio-net有两个队列:接收队列和发送队列;每个队列都对应一个virtqueue,两个队列之间是互不影响的。

前后端利用virtqueue的方式如下图所示:

当收包时,ReceiveQueue需要客户机 driver提前填充分配好的空buffer,然后记录到availRing,并在恰当的时机通知后端,当外部网络有数据包到达时,qemu后端就从availRing 中获取一个buffer,然后填充数据,完事后记录buffer head index到usedRing.最后在恰当的时机通知客户机(向客户机注入中断),客户机接收到信号便知道有数据包到达,这里只需要从usedRing 中获取到index,然后取data数组的第i个元素即可。因为在客户机填充buffer的时候把逻辑buffer的指针保存在data数组中。

而SendQueue同样需要客户机去填充,只不过这里是当客户机需要发送数据包时,把数据包构造成逻辑buffer,然后填充到send Queue,并在恰当的时机通知后端,qemu后端收到通知就知道那个队列有请求到达,如果当前没有处理其他数据包就着手处理这个数据包。

具体就同样是从AvailRing中取出buffer head index,然后从描述符表中get到buffer,这时就需要从buffer中copy数据了,因为要把数据包从host发送出去,然后更新usedRing。最后同样要在恰当的时机通知客户机。注意这里客户机同样需要从usedRing 中get index,但是这里主要是用于delay notify,因为数据包由客户机构造,其占用的buffer并不能重复使用,只是每次有数据包就把其构造成buffer而已。

2 Virtqueue

struct VirtQueue
{VRing vring;  /* vring的元数据 */hwaddr pa;  /* vring实际的内存地址 */uint16_t last_avail_idx;/* Last used index value we have signalled on */uint16_t signalled_used;/* Last used index value we have signalled on */bool signalled_used_valid;/* Notification enabled? */bool notification;uint16_t queue_index;int inuse;uint16_t vector;void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);VirtIODevice *vdev;EventNotifier guest_notifier;EventNotifier host_notifier;
};

每个设备拥有多个 virtqueue 用于大块数据的传输。virtqueue 是一个简单的队列,guest 把 buffers 插入其中,每个 buffer 都是一个分散-聚集数组。驱动调用 find_vqs()来创建一个与 queue 关联的结构体。virtqueue 的数目根据设备的不同而不同,比如 block 设备有一个 virtqueue,network 设备有 2 个 virtqueue,一个用于发送数据包,一个用于接收数据包,Balloon 设备有 3 个 virtqueue.

针对 virtqueue 的操作包括:

  1. int virtqueue_add_buf(struct virtqueue *_vq, struct scatterlist sg[], unsigned int out, unsigned int in, void *data, gfp_t gfp)
    用于向 queue 中添加一个新的 buffer,参数 data 是一个非空的令牌,用于识别 buffer,当 buffer 内容被消耗后,data 会返回。

  2. virtqueue_kick()
    Guest 通知 host 单个或者多个 buffer 已经添加到 queue 中,调用 virtqueue_notify(),notify 函数会向 queue notify(VIRTIO_PCI_QUEUE_NOTIFY)寄存器写入 queue index 来通知 host。

  3. void *virtqueue_get_buf(struct virtqueue *_vq, unsigned int *len)
    返回使用过的 buffer,len 为写入到 buffer 中数据的长度。获取数据,释放 buffer,更新 vring 描述符表格中的 index。

  4. virtqueue_disable_cb()
    示意 guest 不再需要再知道一个 buffer 已经使用了,也就是关闭 device 的中断。驱动会在初始化时注册一个回调函数,disable_cb()通常在这个 virtqueue 回调函数中使用,用于关闭再次的回调发生。

  5. virtqueue_enable_cb()
    与 disable_cb()刚好相反,用于重新开启设备中断的上报。

3 VRing

typedef struct VRing
{unsigned int num;unsigned int align;hwaddr desc;hwaddr avail;hwaddr used;
} VRing;

virtio_ring 是 virtio 传输机制的实现,vring 引入 ring buffers 来作为我们数据传输的载体。virtio_ring 包含 3 部分:

  1. 描述符数组(descriptor table)用于存储一些关联的描述符,每个描述符都是一个对 buffer 的描述,包含一个 address/length 的配对。
  2. 可用的 ring(available ring)用于 guest 端表示那些描述符链当前是可用的。
  3. 使用过的 ring(used ring)用于表示 Host 端表示那些描述符已经使用。

Ring 的数目必须是 2 的次幂。

avail ring有两个用途,一是发送侧(send queue)前端驱动发送报文的时,将待发送报文加入avail ring等待后端的处理,后端处理完后,会将其放入used ring,并由前端将其释放desc中(free_old_xmit_skbs, detach_buf),最后通过try_fill_recv重新装入avail ring中; 二是接收侧(receive qeueu),前端将空白物理块加入avail ring中,提供给后端用来接收报文,后端接收完报文会放入used ring。

可以看出,都是后端用完前端的avail ring的东西放入used ring,即avail ring是前端维护,后端访问;used ring 是后端修改,前端访问。

4 VRingDesc

描述符和描述符表格

vring descriptor 用于指向 guest 使用的 buffer。

typedef struct VRingDesc
{uint64_t addr;uint32_t len;uint16_t flags;uint16_t next;
} VRingDesc;

addr:guest 物理地址
len:buffer 的长度
flags:flags 的值含义包括:

  • VRING_DESC_F_NEXT:用于表明当前 buffer 的下一个域是否有效,也间接表明当前 buffer 是否是 buffers list 的最后一个。
  • VRING_DESC_F_WRITE:当前 buffer 是 read-only 还是 write-only。
  • VRING_DESC_F_INDIRECT:表明这个 buffer 中包含一个 buffer 描述符的 list

next:所有的 buffers 通过 next 串联起来组成 descriptor table

多个 buffer 组成一个 list 由 descriptor table 指向这些 list。

约定俗成,每个 list 中,read-only buffers 放置在 write-only buffers 前面。

图 2.descriptor table

有些设备可能需要同时完成大量数据传输的大量请求,设备 VIRTIO_RING_F_INDIRECT_DESC 特性能够满足这种需求。为了增加 ring 的容量,vring 可以指向一个可以处于内存中任何位置 indirect descriptors table,而这个 table 指向一组 vring descriptors,而这些 vring descriptor 分别指向一组 buffer list(如图所示)。当然 indirect descriptors table 中的 descriptor 不能再次指向 indirect descriptors table。单个 indirect descriptor table 可以包含 read-only 和 write-only 的 descriptors,带有 write-only flag 的 descriptor 会被忽略。

图 3.indirect decriptors

5 VRingAvail

typedef struct VRingAvail
{uint16_t flags;uint16_t idx;uint16_t ring[0];
} VRingAvail;

Available ring 指向 guest 提供给设备的描述符,它指向一个 descriptor 链表的头。Available ring 结构如下图所示。其中标识 flags 值为 0 或者 1,1 表明 Guest 不需要 device 使用完这些 descriptor 时上报中断。idx 指向我们下一个 descriptor 入口处,idx 从 0 开始,一直增加,使用时需要取模:idx=idx&(vring.num-1)

图 4.available ring

6 VRingUsed

typedef struct VRingUsedElem
{uint32_t id;uint32_t len;
} VRingUsedElem;typedef struct VRingUsed
{uint16_t flags;uint16_t idx;VRingUsedElem ring[0];
} VRingUsed;

Used ring 指向 device(host)使用过的 buffers。Used ring 和 Available ring 之间在内存中的分布会有一定间隙,从而避免了 host 和 guest 两端由于 cache 的影响而会写入到 virtqueue 结构体的同一部分的情况。

flags 用于 device 告诉 guest 再次添加 buffer 到 available ring 时不再提醒,也就是说 guest 添加 buffers 到 available ring 时不必进行 kick 操作。

Used vring element 包含 id 和 len,id 指向 descriptor chain 的入口,与之前 guest 写入到 available ring 的入口项一致。

len 为写入到 buffer 中的字节数。

Guest 向设备提供 buffer

  1. 把 buffer 添加到 description table 中,填充 addr,len,flags
  2. 更新 available ring head
  3. 更新 available ring 中的 index
  4. 通知 device,通过写入 virtqueue index 到 Queue Notify 寄存器

Device 使用 buffer 并填充 used ring

device 端使用 buffer 后填充 used ring 的过程如下:

  1. virtqueue_pop()——从描述符表格(descriptor table)中找到 available ring 中添加的 buffers,映射内存
  2. 从分散-聚集的 buffer 读取数据
  3. virtqueue_fill()——取消内存映射,更新 ring[idx]中的 id 和 len 字段
  4. virtqueue_flush()——更新 vring_used 中的 idx
  5. virtio_notify()——如果需要的话,在 ISR 状态位写入 1,通知 guest 描述符已经使用

VIRTIO的vring收发队列创建流程:

在初始化阶段,前端分配好内存区,并初始化好前端的vring后,就把内存区的信息传递到后端,后端也利用这个内存区的信息初始化队列相关的vring。这样vring就在前后端保持了一致。原理就是如此,下面看具体初始化代码:

// GUEST前端驱动,以网络设备为例: Virtio-net: PCI 发现后,通过PCI总线分配收发队列,函数调用依次如下:前端:virtnet_probe->init_vqs->virtnet_find_vqs->vi->vdev->config->find_vqs(vp_find_vqs)->vp_try_to_find_vqs->setup_vq,在setup_vp中通过IO端口和后端交互static int virtnet_probe(structvirtio_device *vdev)
{/** 初始化virtqueue* 创建和初始化发送/接收队列*/err = init_vqs(vi);
}/*创建和初始化发送/接收队列*/
static int init_vqs(struct virtnet_info*vi)
{/*分配*/ret = virtnet_alloc_queues(vi);if (ret)goto err;/*通过find vqs来创建vring*/ret = virtnet_find_vqs(vi);if (ret)goto err_free;
}/*通过find vqs来创建vring*/
static int virtnet_find_vqs(structvirtnet_info *vi)
{/*最后调用的是vp_find_vqs,真正的创建virtqueue内部结构和分配地址,并将地址告诉后端QEMU驱动*/ret = vi->vdev->config->find_vqs(vi->vdev,total_vqs, vqs, callbacks, names);
}//VIRTIO PCI总线
static int vp_find_vqs(struct virtio_device*vdev, unsigned nvqs,struct virtqueue *vqs[],vq_callback_t *callbacks[],const char *names[])
{int err;err = vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, true, true);err = vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, true, false);return vp_try_to_find_vqs(vdev,nvqs, vqs, callbacks, names, false, false);}static int vp_try_to_find_vqs()
{/*最核心的是setup_vq()*/vqs[i] = setup_vq(vdev,i, callbacks[i], names[i], msix_vec);}static struct virtqueue *setup_vq(structvirtio_device *vdev, unsigned index,void (*callback)(struct virtqueue *vq),const char *name,u16 msix_vec)
{/*这里实际上把info->queue的GPA(页框号写入到了设备的VIRTIO_PCI_QUEUE_PFN),这样后端就会得到这块内存区的信息。然后我们先看下前端利用这块内存区做了什么?看下面的vring_new_virtqueue函数,该函数中调用vring_init来初始化vring*/iowrite32(virt_to_phys(info->queue)>> VIRTIO_PCI_QUEUE_ADDR_SHIFT, vp_dev->ioaddr +VIRTIO_PCI_QUEUE_PFN);/* create the vring */vq = vring_new_virtqueue(index, info->num, VIRTIO_PCI_VRING_ALIGN, vdev,true, info->queue, vp_notify, callback, name);
}static inline void vring_init(struct vring *vr, unsigned int num, void *p,unsigned long align)
{vr->num = num;vr->desc = p;vr->avail = p + num*sizeof(struct vring_desc);vr->used = (void *)(((unsigned long)&vr->avail->ring[num] + sizeof(__u16)+ align-1) & ~(align - 1));
}// 这个函数正好体现了我们前面那个结构图。这样前端vring就初始化好了。对队列填充数据时就是根据这个vring填充信息。// QEMU后端驱动:
// 下面在通过VIRTIO_PCI_QUEUE_PFN传递地址的时候,调用virtio_queue_set_addr设置后端相关队列的vring,该函数实现较简单static void virtio_ioport_write(void*opaque, uint32_t addr, uint32_t val)
{switch (addr) {case VIRTIO_PCI_QUEUE_PFN:/*pa就是desc的GPA*/pa = (target_phys_addr_t)val << VIRTIO_PCI_QUEUE_ADDR_SHIFT;if (pa == 0) {virtio_pci_stop_ioeventfd(proxy);virtio_reset(proxy->vdev);msix_unuse_all_vectors(&proxy->pci_dev);}else//下面在通过VIRTIO_PCI_QUEUE_PFN传递地址的时候,调用virtio_queue_set_addr设置后端相关队列的vringvirtio_queue_set_addr(vdev, vdev->queue_sel, pa);break;//仅仅是标记了下设备中的queue_sel表示当前操作的队列索引case VIRTIO_PCI_QUEUE_SEL:if (val < VIRTIO_PCI_QUEUE_MAX)vdev->queue_sel = val;break;case VIRTIO_PCI_QUEUE_NOTIFY:if (val < VIRTIO_PCI_QUEUE_MAX) {virtio_queue_notify(vdev, val);}break;
}void virtio_queue_set_addr(VirtIODevice *vdev, int n, target_phys_addr_t addr)
{vdev->vq[n].pa = addr;virtqueue_init(&vdev->vq[n]);
}static void virtqueue_init(VirtQueue *vq)
{target_phys_addr_t pa = vq->pa;vq->vring.desc = pa;vq->vring.avail = pa + vq->vring.num * sizeof(VRingDesc);vq->vring.used = vring_align(vq->vring.avail +offsetof(VRingAvail, ring[vq->vring.num]),VIRTIO_PCI_VRING_ALIGN);
}

看到这里有么有很面熟,没错,这个函数和前端初始化vring的函数很是类似,这样前后端的vring就同步起来了……

而在guest通知后端的时候,通过VIRTIO_PCI_QUEUE_NOTIFY接口,该函数调用了virtio_queue_notify_vq继而调用 vq->handle_output……就这样,后端就得到通知着手处理了!

还有个细节问题,前端驱动写入的应该是QEUEU的GPA

iowrite32(virt_to_phys(info->queue)>> VIRTIO_PCI_QUEUE_ADDR_SHIFT,vp_dev->ioaddr+ VIRTIO_PCI_QUEUE_PFN);

这个被后端QEMU截获后,QEMU怎么直接使用了GPA呢?哪里完成的GPA->HVA的转换呢?

这个是在QEMU从virtqueue中取消息的时候,进行转换的

QEMU代码,在收到VIRTIO通知后,会通过virtqueue_pop从共享队列中取出消息

intvirtqueue_pop(VirtQueue *vq, VirtQueueElement *elem)
{/*Now map what we have collected */virtqueue_map_sg(elem->in_sg,elem->in_addr, elem->in_num, 1);virtqueue_map_sg(elem->out_sg,elem->out_addr, elem->out_num, 0);
}voidvirtqueue_map_sg()
{for (i = 0; i < num_sg; i++) {len = sg[i].iov_len;sg[i].iov_base =cpu_physical_memory_map(addr[i], &len, is_write);if (sg[i].iov_base == NULL || len != sg[i].iov_len) {error_report("virtio: trying to map MMIO memory");exit(1);}}
}/*完成一个GUEST的物理地址GPA到HVA的转换*/
void*cpu_physical_memory_map(hwaddr addr,hwaddr *plen,int is_write)
{return address_space_map(&address_space_memory,addr, plen, is_write);
}

收包过程如下所示:

  1. 前端填充好desc(addr/len),并更新vring->avail(ring[0])
  2. 后端读取avail ring索引,找到desc(if ring[0]=2,then desctable[2] 记录的就是一个逻辑buffer的首个物理块的信息),填充buffer数据;将buffer索引存在desc,将desc索引存放在used ring中
  3. 前端读取used ring索引,找到desc,获取buffer数据

原文链接:https://blog.csdn.net/qq_15437629/article/details/82084470

KVM中virtio之vring(八)相关推荐

  1. KVM中virtio之vring(十)

    1.数据结构图的左半部分描述了virtio-blk设备与virtio设备的关系,virtqueue与vring_virtqueue的关系,如下: virtio-blk是一个virtio设备,它看到的队 ...

  2. KVM中Virtio网络的演化之路

    作为一个开放的标准接口,virtio一直在云计算与虚拟化中扮演着重要的角色.而virtio网络接口,作为virtio标准支持下最复杂的接口之一,在虚拟机/容器网络加速.混合云加速中一直扮演着重要角色. ...

  3. KVM中virtio、vhost 和vhost-user比较(十一)

    virtio 在虚拟机中,可以通过qemu模拟e1000网卡,这样的经典网卡一般各种客户操作系统都会提供inbox驱动,所以从兼容性上来看,使用类似e1000的模拟网卡是非常一个不错的选择. 但是,e ...

  4. KVM中virtio实现(九)

    一.什么是 vhost vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机端和主机端都提供驱动才能完成通信,通 ...

  5. 计算机io工作方式,QEMU/KVM和VirtIO工作模式

    https://blog.csdn.net/shengxia1999/article/details/52244119 KVM:Kernel-Based Virtual Machine 基于内核的虚拟 ...

  6. KVM中ioeventfd创建与触发的大致流程(十四)

    在使用virtio-blk的情况时,virtio notify使用的ioeventfd机制,原因是为了提高性能,能够较快速的回到guest中运行.具体是如何建立这个ioeventfd的呢?流程理出来了 ...

  7. rhel6.1 kvm安装virtio驱动

    KVM: 安装Windows virtio半虚拟化驱动 Install KVM Windows virtio para-virtualized dirver 测试环境: 物理机: RHEL 6.1. ...

  8. 通过virt-v2v将VMware ESXi 5中的vm迁移至kvm中

    环境说明 Vmware Esxi 5.5 virt-v2v 1.28 kvm host:Centos 7.2 vm:Windows Server 2003 在很多文章中看见过通过virt-v2v连接e ...

  9. kvm中支持SCSI硬盘接口

    在KVM中安装ubuntu14(硬盘接口采用SCSI),可以正常安装,但是安装centos7却不行,原因是Centos7中没有SCSI驱动. 在ubuntu中用lsmod命令查看,可以发现ubuntu ...

最新文章

  1. java开发面试复试_面试java开发,面试官会问哪些问题?
  2. 使用Canvas进行验证码识别
  3. python是c语言写的吗-C语言是学python的基础吗?
  4. HDU2094 产生冠军
  5. 通向架构师的道路(第五天)之tomcat集群-群猫乱舞
  6. SpringBoot项目 整合 JacksonXml
  7. 蓝桥杯 ALGO-74 算法训练 连接字符串
  8. Python将矩阵格式数据在Console全部显示
  9. IM开发通信协议基础知识(一)---TCP、UDP、HTTP、SOCKET
  10. 腾讯、阿里、京东设计的股权激励
  11. 分布式之CAP原则详解
  12. ffmpeg实时传输视频_使用ffmpeg和DirectX 11流式传输视频
  13. php smart模板技术入门教程
  14. 联想小新13pro锐龙版网卡_联想小新Pro 13标压锐龙版评测:比香还香 买就完了
  15. shio世硕AI智慧指读学习机加入智能教育硬件新战场【无标题】
  16. 电信路由器用linux开发板,WRTnode: WRTnode是基于家用无线路由器芯片的开源Linux开发板,想法源于我之前的项目和产业经验...
  17. [ 网络协议篇 ] IGP 详解之 OSPF 详解(二)--- 四种网络类型 虚链路 详解
  18. 2022-2027年中国知识产权代理行业发展前景及投资战略咨询报告
  19. SST-Calib:结合语义和VO进行时空同步校准的lidar-visual外参标定方法(ITSC 2022)
  20. C语言简单五子棋两人/五子棋人机

热门文章

  1. 想学python从哪里入手-想要学习python,如何入手学习?
  2. python开发好学吗-python软件开发好学吗
  3. 用python怎么赚钱-用python赚钱(python新手怎么兼职)
  4. 大学python和vb哪个简单-python和vb哪个简单
  5. python处理数据的优势-Python的优势及应用领域
  6. python怎么学最快-怎么快速自学python
  7. linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
  8. 2017 年最流行的 15 个数据科学 Python 库
  9. Button的使用(六):ToggleButton
  10. 管理员端API——任仲行