Virtio的传输形式

Virtio可以使用各种不同的总线, 因此标准分为Virtio通用部分和总线专用部分

基于PCI总线的Virtio

发现PCI设备

任何PCI供应商ID为0x1AF4且PCI设备ID为0x1000到0x107F(包括1000和107F)的PCI设备都是virtio设备, 这个范围内(0x1000-0x107F)的值标识是哪个virtio设备, PCI设备ID与Virtio设备计算方式为PCI设备ID = 0x1040+Virtio设备ID, 此外, 根据设备类型, 设备可能使用过渡的PCI设备ID范围0x1000-0x103F. PCI的子设备供应商ID和PCI子设备ID可以反应PCI供应商和设备ID(毕竟原来的被占用了)

暂时PCI设备ID Virtio设备类型
0x1000 网络设备
0x1001 块设备
0x1002 内存膨胀(memory ballooning)
0x1003 控制台
0x1004 SCSI host
0x1005 entropy source
0x1009 9P transport

一个例子: 网络设备的Virtio设备ID为1, 则他的PCI设备ID为0x1041(0x1040+1)或者过渡PCI设备ID 0x1000.

PCI设备布局

设备通过IO和/或者内存区域进行配置, 具体由Virtio Structure PCI Capabilities规定. 所有的64bit, 32bit, 16bit大小的空间为小端模式, 64bit被认为是两个32bit组成的, 有高32bit和低32bit之分, 驱动程序必须使用8bit读写命令去读写8bit大小的空间, 同样适用于16和32bit, 对于64bit, 驱动程序可以独立访问字段的高32位和低32位

Virtio Structure PCI Capabilities

一个virtio设备配置布局包含以下结构:

  • 通用配置 Common configuration
  • 通知 Notifications
  • ISR状态 ISR Status
  • 设备指定的配置(可选) Device-specific configuration (optional)
  • PCI配置访问 PCI configuration access

每一个结构可以通过function中的Base Address register(BAR)映射, 或者通过访问特殊的VIRTIO_PCI_CAP_PCI_CFG字段. 每个结构体的位置由一个设备PCI配置空间能力(Capability)列表中的一个供应商PCI能力指定. Virtio结构使用小端格式, 除非另有说明, 否则对于驱动程序来讲所有字段均为只读:

  • cap_vndr u8: capability类型

  • cap_next u8: 下一个capability再pci配置空间的位置

  • cap_len u8: 表示这个capability的长度

  • cfg_type u8: 含有以下取值(前面的标号即为大小), 将virtio-pci的capability细分为几类:

    1. VIRTIO_PCI_CAP_COMMON_CFG: 通用配置
    2. VIRTIO_PCI_CAP_NOTIFY_CFG: 通知
    3. VIRTIO_PCI_CAP_ISR_CFG: ISR Access
    4. VIRTIO_PCI_CAP_DEVICE_CFG: 设备特定的配置
    5. VIRTIO_PCI_CAP_PCI_CFG: PCI配置访问
  • bar u8: 表示是哪一个BAR被用来将结构体映射到内存或者IO空间

  • padding u8*3(数组): 貌似没啥用, 用来填充到64bit的

  • offset 小端32bit: 这个结构体在BAR中的位移

  • length 小端32bit: 这个结构体的大小

Virtio通用配置空间

cfg_type为1, 可以根据bar和offset找到他, 包含以下字段(介绍中就以顺序介绍了, 不把变量名打上去了):

code-snapshot-1663157105477

变量名 作用
device_feature_select 选择下一个变量内的包含哪些bit,0代表选择feature bit 031, 1代表选择feature bit 3263
device_feature 设备有哪些特征
driver_feature_select 选择下一个变量内的包含哪些bit,0代表选择feature bit 031, 1代表选择feature bit 3263
driver_feature 驱动程序接受设备的哪些特征
msix_config 驱动程序设置MSI-X的配置向量(在文档中也成为config_msix_vector),注意是MSIX的entry number
num_queues 设备支持的最大数量virtqueue
device_status 设备状态, 写入0以重启设备
config_generation 配置计数器? 每次有配置改变时,这个也会跟着改变
queue_select 驱动程序设置下面域说明的是哪个virtqueue
queue_size 队列大小, 设备重置时代表设备能支持的最大队列大小, 0则代表不能使用该队列
queue_msix_vector 驱动程序使用这个去指定MSI-X的队列向量,注意是MSIX的entry number
queue_enable 队列是否启用, 1-启用, 0-不启用
queue_notify_off 驱动程序可以读取这个来计算得到这个队列在通知结构体中的偏移量
queue_desc 驱动程序写入, 用于表示描述符区域的起始物理地址
queue_driver 驱动程序写入, 用于表示驱动区域的起始物理地址
queue_device 驱动程序写入, 用于表示设备区域的起始物理地址

有一些字段文档中会有详细说明

通知结构布局

cfg_type为2, 在原来的cap后面加一个notify_off_multiplier, 小端32bit, 可以通过计算: cap.offset + queue_notify_off * notify_off_multiplier找到一个virtqueue在BAR空间中的位置, 其中cap.offset是该capability的offset, queue_notify_off为通用配置空间中的queue_notify_off, notify_off_multiplier就是上面字段的值

ISR状态

cfg_type为3, 包含一个8bit的ISR状态用于INT#x中断处理(查看他的具体值得知这个貌似是存在了BAR空间), 以下是bit的具体说明:

  • bit0, Queue Interrupt
  • bit1, Device Configuration Interrupt
  • bit2+, Reserved

如果MSI-X capability启用, 检测到队列中断时, 驱动程序不能访问ISR状态

设备特定的配置

cfg_type为4, 依赖于每个具体的设备的配置

PCI配置访问

cfg_type为5, 在原来的cap后面加一个pci_cfg_data, 8bit*4的数组, 对于驱动来讲这个数组可读可写, 这个配置相当于提供了一个第二种访问以上区域的方法, 具体流程为:

  • 写入要访问的BAR到cap.bar
  • 写入1,2或者4代表长度到cap.length
  • 写入在BAR中的偏移量到cap.offset

这样pci_cfg_data就会提供位于cap.bar, 偏移量为cap.offset, cap.length大小的窗口.

初始化以及之后的操作

第一步自然是扫描PCI capabilities list, 获得配置空间信息, 不多赘述了

配置MSI-X Vector

如果有MSI-X Capability并且启用了, 则config_msix_vector和queue_msix_vector存储在MSI-X Table中有效的entry number(0~0x7FF), 用来代表配置改变中断和队列中断, 如果需要禁用某个终端, 则写入一个特殊的值:0xffff

配置Virtqueue

驱动需要对每个virtqueue做以下事情:

  1. 写入virtqueue的index(第一个virtqueue为0)到queue_select中
  2. 读取queue_size, 获得该queue的大小, 如果为0则代表该virtqueue不存在
  3. (可选)选择一个小于virtqueue大小的值然后写入queue_size中
  4. 分配并清空一个virtqueue的描述符表, 可用和已用环在连续物理内存的地方
  5. (可选)如果有MSI-X Capability并且启用了, 选择一个vector用于请求virtqueue事件触发的中断, 写入MSI-X的entry number到queue_msix_vector中并读取他, 如果成功了则为之前的值, 如果失败则返回NO_VECTOR(0xffff)

可用缓冲通知

如果VIRTIO_F_NOTIFICATION_DATA没启用, 则驱动需要通过写入16bit的virtqueue索引到Queue Notify Address(参考通知结构布局一节计算这个地址的位置)来发送一个可用缓冲通知. 如果启用, 则需要写入32bit小端的值到Queue Notify Address中:

  • 31bit: next_wrap: 如果有VIRTIO_F_RING_PACKED,则 代表指向下一个可用的描述符的wrap counter, 如果没有则代表这个是可用索引的最高位bit(第15bit, 以0开始)
  • 30~16bit: next_off: 在一个ring种下一个可用的ring entry的值, 如果没有VIRTIO_F_RING_PACKED则代表是可用索引的低15位(0~14), 如果有则代表下一个可用的描述符再描述符ring中的oofset
  • 15~0bit: vqn: 要通知的Virtqueue数字

已用缓冲通知

如果需要触发一个可用缓冲通知, 则设备将会做以下操作:

  • MSI-X 未启用:
    1. 设置ISR状态域的最低位
    2. 发送合适的PCI中断
  • MSI-X 启用:
    1. 如果 queue_msix_vector 不是NO_VECTOR, 则请求对应的MSI-X中断报文(queue_msix_vector的值为MSI-X Table entry number)

设备配置更改通知

有一些Virtio PCI设备会更改设备配置, 具体为设备指定的配置空间(cfg_type为4), 这种情况下:

  • MSI-X 未启用:
    1. 设置ISR状态域的第二个最低位
    2. 发送合适的PCI中断
  • MSI-X 启用:
    1. 如果 config_msix_vector不是NO_VECTOR, 则请求对应的MSI-X中断报文(config_msix_vector的值为MSI-X Table entry number)

一次中断可能代表一个以上已经被使用的virtqueue或者/和配置空间被修改.

驱动如何处理中断

  • MSI-X 未启用:
    • 读取ISR 状态域, 设为0
    • 如果最低位被设置, 则查找所有的virtqueues, 查看设备操作的结果并进行反应
    • 如果第二个最低位被设置, 则重新分析配置空间
  • MSI-X 启用:
    • 查找所有被映射到该MSI-X vector的virtqueues, 查看设备操作的结果并进行反应
    • 如果MSI-X vector等于config_msix_vector, 重新分析配置空间

参考文档及链接

文档链接:

  1. oasis