将晦涩难懂的技术讲的通俗易懂
分类: 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:
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个字节的配置空间。
参考