文章目录
1.概述
guest中的virtio驱动框架
核心模块为virtio和virtqueue,其他高层的驱动都是基于核心模块之上构建的;virtio-net,是一个virtio设备,又是一个PCI设备
每一个virtio设备都有一个对应的virtio PCI代理设备
以virtio-net驱动为例,由于virtioPCI设备的存在,PCI进行扫描的时候会扫描到该设备,并且会调用想要的驱动probe函数
2.数据结构
内核创建总线用于挂载设备,总线负责设备与驱动的匹配。Linux内核创建了一个virtio bus
virtio设备和virtio驱动,通过virtio_device_id来匹配,而这个都是在virtio规范中定义好的;
virtio_device结构中有一个struct virtio_config_ops,函数集由驱动来进行指定,用于操作具体的设备;
这里描述的virtio-net驱动,既是一个virtio设备,也是一个pci设备,在内核中通过结构体struct virtio_pci_device来组织:
该结构体中维护了几个IO区域:
Common, ISR, Device, Notify,用于获取virtio设备的各种信息,这个也是由virtio规范决定的;
通常来说一个virtio设备,由以下几个部分组成:
- Device status field
- Feature bits
- Notifications
- Device Configuration space
- One or more virtqueues
从结构体看,它用于充当pci设备和virtio设备的纽带,后续也会在probe函数中针对不同的部分进行对应的初始化;
以总线的匹配视角来看就是这样子的:
3. 流程分析
3.1 virtio总线创建
bus_register注册virtio总线,总线负责匹配,在匹配成功后调用通用的virtio_dev_probe函数;
virtio-net设备通过挂在pci总线上,系统在PCI子系统初始化时会去枚举所有的设备,并将枚举的设备注册进系统;
系统在匹配上之后,调用设备的驱动;
PCI设备根据Vendor ID来匹配驱动;
virtio规范中规定基于PCI的virtio设备,Vendor ID号为:0x1AF4,因此最终调用的驱动入口为virtio_pci_probe;
在probe函数中分配struct virtio_pci_device结构,前文中也提到过它负责将virtio设备和pci设备绑定到一起,最终会在两个设备驱动的probe函数中完成整体结构的初始化,也就是virtio_pci_probe完成一部分,实际的virtio设备驱动中完成一部分;
virtio_pci_modern_probe:该函数的内容就与virtio规范紧密相关了,简单来说,virtio设备都会按照规范填充common、device、isr、notification等功能部分,而virtio_pci_modern_probe函数通过virtio_pci_find_capability去获取对应的能力,并且通过map_capability完成IO空间的映射;
virtio_pci_probe中还设置了virtio_pci_config_ops操作函数集,并传递给virtio驱动,在驱动中调用这些回调函数来操作virtio设备;
register_virtio_device:向系统注册virtio设备,从而也就触发了virtio总线的匹配操作,最终调用virtio_dev_probe函数;
virtio_dev_probe函数中按照virtio规范分阶段设置不同的状态、获取virtio设备的feature等,并最终调用实际设备的驱动程序了;
总结:
virtio_pci_config_ops是vritio配置函数集合,它的成员函数通常是代理virtioPCI代理设备的IO操作,如读写vritioPCI代理设备的PIO和MMIO,vritio设备可以通过该结构体中的各个回调函数来驱动设备
3.3virtio-net
virtio-net数据发送流程
3.3.1virtio-net初始化
Virtio-Net是PCI网卡设备驱动,分别会在virtnet-probe和virtio_pci_probe中完成所有的初始化;
virtnet_probe函数入口中,通过init_vqs完成Virtqueue的初始化,这个逐级调用关系如图所示,最终会调用到vring_create_virtqueue来创建Virtqueue;
这个创建的过程中,有些细节是忽略的,比如通过PCI去读取设备的配置空间,获取创建Virtqueue所需要的信息等;
最终就是围绕vring_virtqueue数据结构的初始化展开,其中vring数据结构的内存分配也都是在驱动中完成,整个结构体都由驱动来管理与维护;
3.3.2 virtio-net驱动发送
网络数据的传输在驱动中通过start_xmit函数来实现;
xmit_skb函数中,sg_init_table初始化sg列表,sg_set_buf将sg指向特定的buffer,skb_to_sgvec将socket buffer中的数据填充sg;
通过virtqueue_add_outbuf将sg添加到Virtqueue中,并更新Avail队列中描述符的索引值;
virtqueue_notify通知Device,可以过来取数据了;
3.3.3 Qemu virtio-net设备接收
Guest驱动写寄存器操作时,陷入到KVM中,最终Qemu会捕获到进行处理,入口函数为kvm_handle_io;
Qemu中会针对IO内存区域设置读写的操作函数,当Guest进行IO操作时,最终触发操作函数的调用,针对Virtio-Net,由于它是PCI设备,操作函数为virtio_pci_config_write;
virtio_pci_config_write函数中,对Guest的写操作进行判断并处理,比如在VIRTIO_PCI_QUEUE_NOTIFY时,调用virtio_queue_notify,用于处理Guest驱动的通知,并最终回调handle_output函数;
针对Virtio-Net设备,发送的回调函数为virtio_net_handle_tx_bh,并在virtio_net_flush_tx中完成操作;
通用的操作模型:通过virtqueue_pop从Avail队列中获取地址,将数据进行处理,通过virtqueue_push将处理完后的描述符索引更新到Used队列中,通过virtio_notify通知Guest驱动;
4.virtqueue
Virtqueue 是整个virtio方案的灵魂所在,是virtio驱动和virtio设备的通信方式,每个virtio设备有一个或多个virtioqueue。每个virtqueu包含三个部分,descriptor table、available ring和used ring。descriptor table中每一项用来描述一段缓冲区,包含缓冲区的物理地址GPA和长度,descriptor table的项数标识vritqueue的大学。available ring中每一项的值表示当前可用descriptor table中的indx,有虚拟机内部virtio驱动设置,有QEMU测定virtio设备读取。used ring中每一项的值表示也就使用过的descriptor table中的indx,由vritio设备设置,virtio驱动读取。
4.1数据结构
通常Virtio设备操作Virtqueue时,都是通过struct virtqueue结构体,这个可以理解成对外的一个接口,而Virtqueue机制的实现依赖于struct vring_virtqueue结构体;
Virtqueue有三个核心的数据结构,由struct vring负责组织:
struct vring_desc:描述符表,每一项描述符指向一片内存,内存类型可以分为out类型和in类型,分别代表输出和输入,而内存的管理都由驱动来负责。该结构体中的next字段,可用于将多个描述符构成一个描述符链,而flag字段用于描述属性,比如只读只写等;
struct vring_avail:可用描述符区域,用于记录设备可用的描述符ID,它的主体是数组ring,实际就是一个环形缓冲区;
struct vring_used:已用描述符区域,用于记录设备已经处理完的描述符ID,同样,它的ring数组也是环形缓冲区,与struct vring_avail不同的是,它还记录了设备写回的数据长度;
驱动会分配好内存(scatterlist),并通过virtqueue_add添加到描述表中,这样描述符表中的条目就都能对应到具体的物理地址了,其实可以把它理解成一个资源池子;
驱动可以将可用的资源更新到struct vring_avail中,也就是将可用的描述符ID添加到ring数组中,类似于环形缓冲区机制,通过维护头尾两个指针来进行管理,Driver负责更新头指针(idx),Device负责更新尾指针(Qemu中的Device负责维护一个last_avail_idx),头尾指针,你追我赶,生生不息;
当设备使用完了后,将已用的描述符ID更新到struct vring_used中,vring_virtqueue自身维护了last_used_idx,机制与struct vring_avail一致;
4.2发送
当驱动需要把数据发送给设备时,流程如上图所示:
①A表示分配一个Buffer并添加到Virtqueue中,①B表示从Used队列中获取一个Buffer,这两种中选择一种方式;
②表示将Data拷贝到Buffer中,用于传送;
③表示更新Avail队列中的描述符索引值,注意,驱动中需要执行memory barrier操作,确保Device能看到正确的值;
④与⑤表示Driver通知Device来取数据;
⑥表示Device从Avail队列中获取到描述符索引值;
⑦表示将描述符索引对应的地址中的数据取出来;
⑧表示Device更新Used队列中的描述符索引;
⑨与⑩表示Device通知Driver数据已经取完了;
4.3接收
当驱动从设备接收数据时,流程如上图所示:
①表示Device从Avail队列中获取可用描述符索引值;
②表示将数据拷贝至描述符索引对应的地址上;
③表示更新Used队列中的描述符索引值;
④与⑤表示Device通知Driver来取数据;
⑥表示Driver从Used队列中获取已用描述符索引值;
⑦表示将描述符索引对应地址中的数据取出来;
⑧表示将Avail队列中的描述符索引值进行更新;
⑨与⑩表示Driver通知Device有新的可用描述符;
参考:https://www.cnblogs.com/LoyenWang/category/1828942.html