将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2023-05-07 15:23:53
我们经常在virtio代码中或者相关文档中看到legacy或者modern这种描述,但网上又很少文章将这两个模式解释清楚。这里我们主要对这两个模式进行下介绍。
首先,看一下spec的官方术语解释:
Legacy Interface is an interface specified by an earlier draft of this specification (before 1.0)
早期的开发者Rusty Russell设计并实现了virtio,之后成为了virtio规范, 经历了0.95, 1.0, 1.1,到现在的1.2版本的演进。0.95之前称为传统virtio设备,1.0修改了一些PCI配置空间访问方式和virtqueue的优化和特定设备的约定。简单来说就是在virtio1.0规范出来前,virtio已经广泛应用了(主要是virtio0.95),虽然这些早期版本设计上不够合理,但是也已经广泛部署,所以后续virtio都需要兼容这些早期版本。而Legacy指的就是virtio1.0之前的版本(驱动,设备及接口),而morden就是只virtio1.0及之后的版本。
如果后端设备即支持morden的接口,又兼容lagecy的接口,那前端驱动如何判断应该用哪个模式呢?这就不得不提VIRTIO_F_VERSION_1这个feature,如果后端不支持这个feature,前端就只能按照lagecy接口进行交互。
那么早期的lagecy版本和后来的morden有什么不同呢?其中{BANNED}最佳主要的方面就是PCIe设备空间的layout方面的不同。下面我们就从PCIe设备空间布局讲起,对比lagecy和morden的区别的同时也更深入的了解PCIe设备的空间布局。当前morden相对lagecy除了PCIe不同,还有其他特性的不同,如packed queue的支持等,不过这些不是本文的重点,这里只关注PCIe相关的。
PCI或PCIe设备有自己独立的地址空间。地址空间又可以分为两类:一类是配置空间,这是每个PCI设备必须具备的,用来描述PCI设备的一些关键属性;另一类是PCI设备内部的一些存储空间,这类空间根据不同PCI设备的实现不同而不同,由于这类空间是通过配置空间的BAR寄存器进行地址映射,所以也称作BAR空间。
PCI spec规定了PCI设备必须提供的单独地址空间:配置空间(configuration space)。而配置空间具体又可以分为三个部分:
1. 前64个字节(其地址范围为0x00~0x3F)是所有PCI设备必须支持的,而其中前16字节对所有类型的pci设备格式都相同,之后的空间格式因类型而不同,对前16字节空间我称它为通用配置空间;
2. 此外PCI/PCI-X还扩展了0x40~0xFF(64-266)这段配置空间,在这段空间主要存放一些与MSI或者MSI-X中断机制和电源管理相关的Capability结构;
3. PCIe规范在PCI规范的基础上,将配置空间扩展到4KB,也就是256-4k这段配置空间是PCIe设备所特有的;
基本配置空间是只PCI设备必须支持的前64字节配置空间,其中通用配置空间是指PCI配置空间的前16字节,以virtio设备为例,其通用配置空间如下:
具体virtio-blk配置空间的内容可以通过lspci命令查看到,如下
前16字节中有4个地方用来识别virtio设备:
l vendor id:厂商ID,用来标识pci设备出自哪个厂商,这里是0x1af4,来自Red Hat。
l device id:厂商下的产品ID,传统virtio-blk设备,这里是0x1001
l revision id:厂商决定是否使用,设备版本ID,这里未使用
l header type:pci设备类型,0x00(普通设备),0x01(pci bridge),0x02(CardBus bridge)。virtio是普通设备,这里是0x00
command字段用来控制pci设备,打开某些功能的开关,virtio-blk设备是(0x0507 = 0b1010111),command的各字段含义如下图
低三位的含义如下:
l I/O Space:如果PCI设备实现了IO空间,该字段用来控制是否接收总线上对IO空间的访问。如果PCI设备没有IO空间,该字段不可写。
l Memory Space:如果PCI设备实现了内存空间,该字段用来控制是否接收总线上对内存空间的访问。如果PCI设备没有内存空间,该字段不可写。
l Bus Master:控制pci设备是否具有作为Master角色的权限。
status字段用来记录pci设备的状态信息,virtio-blk是(0x10 = 0x10000),status各字段含义如下图:
其中有一位是Capabilities List,它是PCI规范定义的附加空间标志位,Capabilities List的意义是允许在PCI设备配置空间之后加上额外的寄存器,这些寄存器由Capability List组织起来,用来实现特定的功能,附加空间在64字节配置空间之后,{BANNED}最佳大不能超过256字节。以virtio-blk为例,它标记了这个位,因此在virtio-blk设备配置空间之后,还有一段空间用来实现virtio-blk的一些特有功能。1表示capabilities pointer字段(0x34)存放了附加寄存器组的起始地址。这里的地址表示附加空间在PCI设备空间内的偏移。
关于Capability 我们稍后再介绍,我们先看下前64字节的基本配置空间中,Legacy设备和morden设备的不同:
{BANNED}中国第一处是device id部分,0x1000- 0x1040表示是legacy设备, 0x1040- 0x107f表示是modern,例如网卡(virtio_net)可以是0x1000(legacy时)也可以是0x1041(morden时),legacy的device id,在此基础上加0x40即是Modern PCI设备的device id。所以按照标准driver识别device id如果在 0x1000- 0x1040就是传统的virtio device id,但实际上并非如此,有些情况为了向前兼容实现了morden接口的设备也会使用lagecy的device id,所以我们可以看到驱动的判断并不是简单以device id为准的。
另一处就是上面说的capabilities pointer字段(0x34),因为在lagecy设备的情况,其设备的关键属性是直接放在其{BANNED}中国第一个BAR空间的,而没有专门的Capability,所以其capabilities pointer字段(0x34)指向不是virtio的Capability,而是仅有的通用的MSI-X Capability,而modern情况下其指向的是virtio的定制Capability。
其他部分的配置空间没有什么特殊地方,如下图所示,不再过多介绍。
扩展配置空间即0x40~0xFF(64-266)这段配置空间,在这段空间主要存放一些与MSI或者MSI-X中断机制和电源管理相关的Capability结构。此外virtio spec设计了自己的配置空间,用来实现virtio-pci的功能。pci通过status字段的capabilities list bit标记自己在64字节预定义配置空间之后有附加的寄存器组,capabilities pointer会存放寄存器组链表的头部指针,这里的指针代表寄存器在配置空间内的偏移。
PCI spec中描述的capabilities list格式如下,第1个字节存放capability ID,标识后面配置空间实现的是哪种capability,第2个字节存放下一个capability的地址。capability ID查阅参见pci spec3.0 附录H。virtio-blk实现的capability有两种,一种是MSI-X( Message Signaled Interrupts - Extension),ID为0x11,一种是Vendor Specific,ID为0x9(VIRTIO_PCI_CAP_PCI_CFG),后面一种capability设计目的就是让厂商实现自己的功能。
在virtio morden的规范下,virtio的很多设备信息就是存放在多个virtio(ID为0x9)的capabilty中的,准确的说真正的信息不一定是在capabilty结构用,因为capabilty大小有限,如果信息较多,这些信息会存放在设备的BAR空间中,capabilty仅仅是存放这些信息在BAR空间的具体偏移。根据virtio spec的规范,要实现virtio-pci的capabilty,其布局应该如下:
点击(此处)折叠或打开
对应字段含义如下:
(1)cap_vndr:0x09,标识为virtio特有的capability;
(2)cap_next:指向下一个capability在PCI配置空间的位置(offset);
(3)cap_len:capability的具体长度,包含 virtio_pci_cap结构;
(4)cfg_type:标识不同的virtio capability类型,具体有如下几个取值
点击(此处)折叠或打开
注意:设备可以为每个类型的capability提供多个结构,例如有些实现中使用IO访问要比memory访问效率更高,则会提供两个相同的capability,一个位于IO BAR,另一个位于memory BAR,如果IO BAR可用则使用IO BAR的资源,否则fallback到memory BAR。
(1) bar:取值0~5,对应PCI配置空间中的6个BAR寄存器,表示这个capability是位于哪个BAR空间的,当然这个BAR空间可以是个IO BAR也可以是个memory BAR;
(2) offset:表示这个capability在对应BAR空间的offset;
(3) length:表示这个capability的结构长度;
可以看到virtio设备有多个类型的capabilty结构,下面我们来一一分析。
l Common configuration
即virtio设备的通用配置,对应的capabilty type为VIRTIO_PCI_CAP_COMMON_CFG,其在DPDK中定义如下:
点击(此处)折叠或打开
这个结构前半部分描述的是设备的全局信息,后半部分描述的具体队列的信息。看到这里不知道大家有没有注意到一个问题,就是这里只有一份队列信息,如果是多队列情况下如何获取或者配置每个队列的信息呢?我们还是看一下DPDK(18.11) virtio-net的多队列初始化流程。其中关键函数是virtio_alloc_queues。
点击(此处)折叠或打开
对于每个队列调用virtio_init_queue,其具体函数内容这里不再分析,主要是分配和初始化struct virtqueue结构。其相关数据结构关系如下图:
其中virtio_init_queue中{BANNED}最佳后会调用setup_queue,对于morden设备就是modern_setup_queue函数:
点击(此处)折叠或打开
我们看到这里会把软件分配的desc地址,avail ring地址,以及used ring地址设置到硬件对应的common_cfg中,并且通过common_cfg->queue_select来区分设置不同队列。如果后端是软件实现的话(如vhost_user),每一次这个写硬件操作就会触发SET_VRING_BASE的消息协商。所以队列的desc ring,avail ring,used ring都是在guest os的软件内存,设置到硬件上的仅仅上他们的地址。
此外,我们知道virtio队列有TXQ,RXQ,还有CtrlQ,但是在这个结构里面怎么没看到队列类型呢?我们看下DPDK的virtio_net是如何判断virtio的队列类型的
点击(此处)折叠或打开
可以看到奇数vq就是TXQ,偶数vq就是RXQ,CQ在{BANNED}最佳后。
l Notification configuration
对应的capabilty type为VIRTIO_PCI_CAP_NOTIFY_CFG,其在DPDK中定义如下:
点击(此处)折叠或打开
这个配置主要用来描述通知后端队列的地址(notify的地址),具体地址计算方式如下:
cap.offset + queue_notify_off * notify_off_multiplier
cap.offset和notify_off_multiplier直接从硬件的capabilty 获取即可,cap.offset指向对应BAR空间的offset,而queue_notify_off是来自前面讲述的pci_common_cfg。可以看到如果notify_off_multiplier为0,则所有队列会使用同一个地址notify。否则就会使用多个地址。
在virtio0.5(Legacy模式)中,所有队列就会共享一个notify寄存器,驱动向寄存器中写入不同的地址来通知后端收取不同队列的数据。这样在大流量情况下多队列notify会产生瓶颈,在morden设备中可以采用不同队列不同地址的方式减少notify争抢提升性能。
此外如果设备支持 VIRTIO_F_NOTIFICATION_DATA,即notify时携带数据,则每个队列的notify地址需要有4字节,即
cap.length >= queue_notify_off * notify_off_multiplier + 4否则,每个队列的notify需要至少两字节,即cap.length >= queue_notify_off * notify_off_multiplier + 4
l ISR status
ISR status这个capabilty就是原有的struct virtio_pci_cap cap结构,主要用于产生INT#x中断,其指向的内容至少一个字节,即mem_resource[cap.bar].addr + cap.offset指向的至少一个字节长度。并且这个字节只有两个bit有效,其他作为保留。如下,{BANNED}中国第一个bit表示队列事件通知,第二个bit表示设备配置变化通知。
如果设备不支持MSI-X capability的话,在设备配置变化或者需要进行队列kick通知时,就需要用到ISR capability。
l Device-specific configuration
VIRTIO_PCI_CAP_DEVICE_CFG 类型的capability用来存储设备特有的配置信息,如virtio-net情况其配置信息如下:
点击(此处)折叠或打开
l PCI configuration access
PCI configuration access类型的capability是一种特殊的capability,它是为了提供给驱动另一种访问pci 配置的方法,驱动可以通过配置 cap.bar, cap.length, cap.offset以及pci_cfg_data来读写对应PCI BAR中的指定offset以及指定length的内容。
以上我们介绍的capability都是morden设备才有的,而lagecy(virtio 0.95)是没有这些capability的。那么lagecy的相关配置是如何存放的呢?我们看下virtio spec是怎么说的:
Transitional devices MUST present part of configuration registers in a legacy configuration structure in BAR0 in the first I/O region of the PCI device.
即legacy的这些配置信息都是存放在PCI设备的{BANNED}中国第一个IO BAR0的,不过这里其实有点分歧,这些配置是需要存放在BAR0,但是BAR0必须要是I/O BAR吗?不能说memory BAR吗?其实是可以的,只是早期一些驱动(比如DPDK 21.05之前)就默认legacy设备的{BANNED}中国第一个BAR是I/O BAR,但是其实当前很多智能网卡(DPU)通过硬件模拟virtio设备,所有设备都是在一个PCIe树上,pio资源是有限的,所以一般都采用memory BAR实现,所以后续DPDK也对此进行了修改。详情可见如下patch:
https://lore.kernel.org/dpdk-dev/b34311c7-5b09-a1f6-1957-c9e19bb2a273@intel.com/T/
Lagecy virtio设备的common configuration在PCIe BAR0上的layout如下图所示:
这里有个需要注意的地方,就是设备队列的地址(queue address)是32位的,而在morden设备capability中的common configuration的 queue address是64位,这意味着什么呢?就是lagecy设备情况下,驱动分配队列地址时物理地址必须在16T内存一下,为什么是16T是因为一个page 4k(12bit),由于队列地址都是page对齐,所以32位地址{BANNED}最佳大描述32+12=44bit的地址空间。这点从内核驱动的lagecy设备驱动加载函数virtio_pci_legacy_probe初始化dma_mask 与 coherent_dma_mask的情况也可看出。
dma_mask 与 coherent_dma_mask 这两个参数表示它能寻址的物理地址的范围,内核通过这两个参数分配合适的物理内存给 device。 dma_mask 是 设备 DMA 能访问的内存范围, coherent_dma_mask 则作用于申请 一致性 DMA 缓冲区(如virtio 队列地址)。因为不是所有的硬件都能够支持 64bit 的地址宽度。如果 addr_phy 是一个物理地址,且 (u64)addr_phy <= *dev->dma_mask,那么该 device 就可以寻址该物理地址。如果 device 只能寻址 32 位地址,那么 mask 应为 0xffffffff。依此类推。相反收发报文buf的地址没有这个限制,可以使用dma_mask (对virtio就是整个64位地址空间)。
此外lagecy也可选的支持MSI-X,layout如下,紧随着(如果存在)common configuration。
在之后就是一些device-specific configuration了。总而言之,lagecy的各种配置都是存放于PCIe设备的BAR 0中的,并且不支持capability。
{BANNED}最佳后我们看一下virio_net的lagecy和morden设备配置空间的真实布局对比。如下图
lagecy设备配置空间
Morden设备的配置空间
Lagecy设备BAR0 为I/O BAR
Lagecy设备BAR0 为memory BAR
Morden设备配置空间
PCIe规范在PCI规范的基础上,将配置空间扩展到4KB,即0x100~0xFFF这段配置空间是PCIe设备特有的。PCIe扩展配置空间中用于存放PCIe设备独有的一些Capability结构,而PCI设备不能使用这段空间。不过目前virtio设备也没有使用这段空间。
PCI配置空间和内存空间是分离的,PCI内存空间根据不同设备实现不同其大小和个数也不同,这些PCI设备的内部存储空间我们称之为BAR空间,因为它的基地址存放在配置空间的BAR寄存器中。设备出厂时,这些空间的大小和属性都写在Configuration BAR寄存器里面,然后上电后,系统软件读取这些BAR,分别为其分配对应的系统内存空间,并把相应的内存基地址写回到BAR。(BAR的地址其实是PCI总线域的地址,CPU访问的是存储器域的地址,CPU访问PCIe设备时,需要把总线域地址转换成存储器域的地址。)如下图所示说明配置空间和BAR空间的关系。
我们以一个morden的virtio-net设备为例,看下PCI配置空间和BAR空间的关系:
X86处理器通过定义两个IO端口寄存器,分别为CONFIG_ADDRESS和CONFIG_DATA寄存器,其地址为0xCF8和0xCFC。通过在CONFIG_ADDRESS端口填入PCI设备的BDF和要访问设备寄存器编号,在CONFIG_DATA上写入或者读出PCI配置空间的内容来实现对配置空间的访问。
PCIe规范在PCI规范的基础上,将配置空间扩展到4KB。原来的CF8/CFC方法仍然可以访问所有PCIe设备配置空间的头255B,但是该方法访问不了剩下的(4K-255)配置空间。怎么办呢?Intel提供了另外一种PCIe配置空间访问方法:通过将配置空间映射到Memory map IO(MMIO)空间,对PCIe配置空间可以像对内存一样进行读写访问了。如图
因此对PCI配置空间的访问方式有两种:
1. 传统方式,写IO端口0xCFCh和0xCF8h。只能访问PCI/PCIe设备的开始256个字节(因为PCI设备的配置空间本来就只有256个字节);
2. PCIe的方式,就是上面提到的mmio方式,它可以访问4K个字节的配置空间。
参考