virtio 是一种通用的半虚拟化的 i/o 通信协议,提供了一套前后端 i/o 通信的的框架协议和编程接口。根据该协议实现的设备通过前后端的配合,相比全模拟设备可以大幅减少陷入陷出以及内存拷贝的次数,使 guest 获得高效的 i/o 性能。作为目前虚拟化标准的通用协议规范,经历了 0.95、1.0、1.1 三个版本的演进。根据 0.95 版本实现的称为传统 virtio 设备,1.0 版本修改了一些 pci 配置空间的访问方式和 virtioqueue 的优化和特定设备的约定,1.1 版本则增加了 packed virtqueue 的支持,详细可以参考官方发布的 virtio 协议规范。
之所以称 virtio 是一种半虚拟化的解决方案,是因为其首先需要在主机侧通过软件创建 virito 的后端设备,其次在 guest 要有对应的设备前端驱动,前后端通过共享内存进行通信。virtio 规范定义了设备的控制和数据面接口,控制面接口包括设备状态、feature 的协商等,数据面则包括共享内存的数据布局定义以及前后端的通知方式。基于 virtio 协议,目前已衍生出了 virtio-blk、virtio-net、virtio-scsi、virtio-mem 等各种各样的半虚拟化设备。virtio 设备可以支持多种传输层协议,既可以挂载到 mmio 总线,也可以作为 pci 设备,另外还可以支持基于标准 i/o 通道机制的 s/390 设备。
鉴于 virtio 设备具备较好的性能及通用性,stratovirt 自然也支持,stratovirt 中 virtio 设备的实现架构以及 i/o 通信流程如上图所示。下面就基于目前最新的代码,探究一下 stratovirt 中 virtio 设备的代码实现框架。
virtiodevice trait stratovirt 的 virtio crate 提供了 virtio 设备的通用接口以及所有 virtio 设备的相关实现。其中,lib.rs 中定义了为所有 virtio 设备定义的 virtiodevice trait。每种 virtio 设备都需要实现自定义的 virtiodevice 接口。
/// the trait for virtio device operations.pub trait virtiodevice: send { /// realize low level device. fn realize(&mut self) -> result; /// unrealize low level device fn unrealize(&mut self) -> result { bail!(unrealize of the virtio device is not implemented); } /// get the virtio device type, refer to virtio spec. fn device_type(&self) -> u32; /// get the count of virtio device queues. fn queue_num(&self) -> usize; /// get the queue size of virtio device. fn queue_size(&self) -> u16; /// get device features from host. fn get_device_features(&self, features_select: u32) -> u32; /// set driver features by guest. fn set_driver_features(&mut self, page: u32, value: u32); /// read data of config from guest. fn read_config(&self, offset: u64, data: &mut [u8]) -> result; /// write data to config from guest. fn write_config(&mut self, offset: u64, data: &[u8]) -> result; /// activate the virtio device, this function is called by vcpu thread when frontend /// virtio driver is ready and write `driver_ok` to backend. /// /// # arguments /// /// * `mem_space` - system mem. /// * `interrupt_evt` - the eventfd used to send interrupt to guest. /// * `interrupt_status` - the interrupt status present to guest. /// * `queues` - the virtio queues. /// * `queue_evts` - the notifier events from guest. fn activate( &mut self, mem_space: arc, interrupt_cb: arc, queues: &[arc], queue_evts: vec, ) -> result; /// deactivate virtio device, this function remove event fd /// of device out of the event loop. fn deactivate(&mut self) -> result { bail!( reset this device is not supported, virtio dev type is {}, self.device_type() ); } /// reset virtio device. fn reset(&mut self) -> result { ok(()) } /// update the low level config of mmio device, /// for example: update the images file fd of virtio block device. /// /// # arguments /// /// * `_file_path` - the related backend file path. fn update_config(&mut self, _dev_config: option
) -> result { bail!(unsupported to update configuration) }} realize()/unrealize(): 这一组接口用于具现化/去具现化具体的 virtio 设备。具现化做的一些具体操作包括设置支持的 features、设备特有的属性(如网卡的 mac)、初始化连接 host 后端设备等。 set_driver_features():将前端驱动支持的 features 与后端模拟设备支持的 features 进行协商后,设置最终实现的 features。 read_config()/write_config():virtio 协议规范为每种 virtio 设备定义了自定义的配置空间,这组接口就是用来读写这部分配置数据。 activate()/deactivate(): 激活/去激活设备,负责绑定/解绑后端、加入/移除 i/o 循环。 reset():虚拟机重启时某些设备需要重置。 update_config():支持轻量机型下的 virtio-mmio 设备动态绑定/解绑后端,实现 virtio-mmio 设备的模拟热插拔。 virtqueue
virtio 设备可以有一个或多个队列,每个队列有描述符表、available ring、used ring 三个部分。当前 stratovirt 的 virtio 设备均遵循 1.0 规范,队列的内存布局仅支持 split vring 的方式。queue.rs 中定义了一系列针对队列操作及查询的接口。所有的 i/o 请求数据信息以描述符的形式存放在描述符表中,前端准备好数据后更新 available ring 告诉后端还有哪些 i/o 待发送,后端执行完 i/o 更新 used ring 通知前端。不同设备的 i/o 处理不尽相同,但是核心的 virtqueue 操作是一样的。
pub struct splitvring { /// region cache information. pub cache: option, /// guest physical address of the descriptor table. /// the table is composed of descriptors(splitvringdesc). pub desc_table: guestaddress, /// guest physical address of the available ring. /// the ring is composed of flags(u16), idx(u16), ring[size](u16) and used_event(u16). pub avail_ring: guestaddress, /// guest physical address of the used ring. /// the ring is composed of flags(u16), idx(u16), used_ring[size](usedelem) and avail_event(u16). pub used_ring: guestaddress, /// host address cache. pub addr_cache: virtioaddrcache, /// indicate whether the queue configuration is finished. pub ready: bool, /// the maximal size in elements offered by the device. pub max_size: u16, /// the queue size set by frontend. pub size: u16, /// interrupt vector index of the queue for msix pub vector: u16, /// the next index which can be popped in the available vring. next_avail: wrapping, /// the next index which can be pushed in the used vring. next_used: wrapping, /// the index of last descriptor used which has triggered interrupt. last_signal_used: wrapping,} virtio-mmio 设备 stratovirt 目前提供两种机型:轻量机型和标准机型。轻量机型由于需要追求极致的启动速度以及内存底噪开销,因此只支持挂载数量有限的 virtio-mmio 设备。而标准机型面向传统的标准云化场景,对于 i/o 设备的性能要求较高,且需要支持热插拔满足资源弹性,因此标准机型支持将 virtio 设备以 pci 设备挂载在模拟的 pci 总线上。目前标准机型只支持配置 virtio-pci 设备,不支持 virtio-mmio 设备。
结构体 virtiommiodevice 定义了一个通用的 virtio-mmio 设备,其中的 device 即为实现了 virtiodevice 这个 trait 的具体的 virtio 设备结构,可以是网卡、磁盘等。virtiommiostate 结构体中存放了 virtio-mmio 设备的控制寄存器,并且为其实现了对应的读写接口 read_common_config()/write_common_config()。virtio-mmio 设备的配置空间布局如下图所示:
interrupt_evt 通过 irqfd 向虚拟机注入中断,host_notify_info 则为每个队列创建了一个 eventfd,虚拟机利用 ioeventfd 机制陷出到 stratovirt 执行后端的 i/o 处理。
pub struct virtiommiodevice { // the entity of low level device. pub device: arc, // eventfd used to send interrupt to vm interrupt_evt: eventfd, // interrupt status. interrupt_status: arc, // hostnotifyinfo used for guest notifier host_notify_info: hostnotifyinfo, // the state of virtio mmio device. state: virtiommiostate, // system address space. mem_space: arc, // virtio queues. queues: vec, // system resource of device. res: sysres,} virtiommiodevice 实现了 realize 接口完成设备的具现化:
调用各设备实现的 virtiodevice trait 的具现化接口。 virtio-mmio 设备挂载在了系统总线上,stratovirt 为每个设备分配 512 字节的配置空间。除此之外,需要为其注册 irqfd 以便后续 i/o 完成后向虚拟机注入中断。这些信息都保存在 sysres 数据结构中。 添加内核启动参数,通过内核启动参数将设备的内存区间及中断号信息直接告诉 guest。 pub fn realize( mut self, sysbus: &mut sysbus, region_base: u64, region_size: u64, #[cfg(target_arch = x86_64)] bs: &arc,) -> result> { self.device .lock() .unwrap() .realize() .chain_err(|| failed to realize virtio.)?; if region_base >= sysbus.mmio_region.1 { bail!(mmio region space exhausted.); } self.set_sys_resource(sysbus, region_base, region_size)?; let dev = arc::new(self)); sysbus.attach_device(&dev, region_base, region_size)?; #[cfg(target_arch = x86_64)] bs.lock().unwrap().kernel_cmdline.push(param { param_type: virtio_mmio.device.to_string(), value: format!( {}@0x{:08x}:{}, region_size, region_base, dev.lock().unwrap().res.irq ), }); ok(dev)} 前端驱动加载过程中会读写设备的配置空间,前后端完成 feature 的协商,一切 ok 后前端驱动将向配置空间写状态,后端设备将会调用 activate 方法激活设备。当触发激活时,前端已为这三个部分分配了内存空间,guest 物理地址(gpa)已写入设备的配置空间,后端需要将 gpa 地址转化为 host 虚拟地址(hva)。随后,就可以根据队列配置创建队列,并将 i/o 的 eventfd 加入事件循环激活设备开始 i/o 通信。
virtio-pci 设备 如上所述,virtio 设备也可以作为一个 pci 类设备挂载到 pci 总线上。类似的,在 stratovirt 中用结构体 virtiopcidevice 来表示一个 virtio-pci 设备。既然是作为一个 pci 设备,virtio-pci 就需要拥有符合 pci 规范拥有 pci 设备的配置空间,guest 启动后通过 pci 设备树枚举来发现设备,而不是像 virtio-mmio 设备一样直接通过内核启动参数告诉 guest。
pub struct virtiopcidevice { /// name of this device name: string, /// the entity of virtio device device: arc, /// device id dev_id: arc, /// devfn devfn: u8, /// if this device is activated or not. device_activated: arc, /// memory addressspace sys_mem: arc, /// pci config space. config: pciconfig, /// virtio common config refer to virtio spec. common_config: arc, /// primary bus parent_bus: weak, /// eventfds used for notifying the guest. notify_eventfds: notifyeventfds, /// the function for interrupt triggering interrupt_cb: option, /// virtio queues. the vector and queue will be shared acrossing thread, so all with arc wrapper. queues: arc>>, /// multi-function flag. multi_func: bool,} virtiopcidevice 通过实现 pcidevops trait 的 realize()方法完成设备的具现化:
初始化 pci 配置寄存器。 将 virtio 协议规定的 common configuration、notifications、isr status、device-specific configuration 作为四个 pci 设备的 capability, 对应数据的内存空间则映射到第 3 个 bar 空间的不同部分。配置空间布局如下图所示:
前端驱动对于各空间的访问的回调函数由 modern_mem_region_init()注册,当前端读写这部分内存区间时会陷出到 stratovirt 执行注册的回调接口。每个队列在 notification cap 指向的空间中占据 4 个字节,stratovirt 为每个队列的 4 个字节空间注册 ioeventfd。前端驱动准备好某个队列后,就会写对应队列的这 4 个字节的地址空间,后端借助 ioeventfd 机制收到通知后陷出进行 host 侧的 i/o 下发。 中断机制采用 msi-x,向量表和 pending 位图则位于第 2 个 bar 空间。
阻燃系列认证基础知识
重磅!华为智能汽车纳入消费者业务 余承东出任总负责人
美的收购库卡终获批 交易或于1月上旬完成
生物识别模式需重点考虑的要素浅析
AR增强现实技术在工业制造中的五大应用模式
virtio I/O通信流程及设备框架的实现
AMD剑走偏锋激怒英特尔 Coffee Lake蓄势待发:Core i5达到6核!
iphone8什么时候上市?iphone8最新消息:iPhone 8搭载iOS11有亮点,但这些功能已在锤子坚果Pro上呈现
回顾OPPO Find系列邀请函,Find X更可期
环境光传感器在自动灯调光中的应用
导线压降补偿电路原理图
基于混合式控制系统实现多轴超声检测系统的设计
关于直流电子负载的定义及应用分析
研扬强固型平板电脑RTC-710RK,满足各行业客户的高规格要求
人工智能领域哪些技术是比较关键的
工业控制计算机在交互机器人系统中的应用
iPhone XI Max曝光!所有想象中的样子,它都有!
系统集成及集成测试
白噪声发生器没有1/f组件
华为先于产业界启动5G研究,力推全球统一标准