凯发app官方网站-凯发k8官网下载客户端中心 | | 凯发app官方网站-凯发k8官网下载客户端中心
  • 博客访问: 3137738
  • 博文数量: 190
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7172
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(190)

文章存档

2023年(2)

2022年(4)

2021年(12)

2020年(8)

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

相关博文
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·

分类: linux

2023-05-07 15:23:53

从virtio看pcie设备实现-凯发app官方网站

—lvyilong316

virtiolegacymodern模式

我们经常在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设备空间布局讲起,对比lagecymorden的区别的同时也更深入的了解pcie设备的空间布局。当前morden相对lagecy除了pcie不同,还有其他特性的不同,如packed queue的支持等,不过这些不是本文的重点,这里只关注pcie相关的。

pcie设备空间布局

pcipcie设备有自己独立的地址空间。地址空间又可以分为两类:一类是配置空间,这是每个pci设备必须具备的,用来描述pci设备的一些关键属性;另一类是pci设备内部的一些存储空间,这类空间根据不同pci设备的实现不同而不同,由于这类空间是通过配置空间的bar寄存器进行地址映射,所以也称作bar空间。

pci配置空间

pci spec规定了pci设备必须提供的单独地址空间:配置空间(configuration space。而配置空间具体又可以分为三个部分:

1. 64个字节(其地址范围为0x00~0x3f)是所有pci设备必须支持的,而其中前16字节对所有类型的pci设备格式都相同,之后的空间格式因类型而不同,对前16字节空间我称它为通用配置空间

2. 此外pci/pci-x还扩展了0x40~0xff64-266这段配置空间,在这段空间主要存放一些与msi或者msi-x中断机制和电源管理相关的capability结构

3. pcie规范在pci规范的基础上,将配置空间扩展到4kb,也就是256-4k这段配置空间是pcie设备所特有的;

基本配置空间

基本配置空间是只pci设备必须支持的前64字节配置空间,其中通用配置空间是指pci配置空间的前16字节,以virtio设备为例,其通用配置空间如下:

 

具体virtio-blk配置空间的内容可以通过lspci命令查看到,如下

 

16字节中有4个地方用来识别virtio设备

vendor id:厂商id,用来标识pci设备出自哪个厂商,这里是0x1af4,来自red hat

device id:厂商下的产品id,传统virtio-blk设备,这里是0x1001

revision id:厂商决定是否使用,设备版本id,这里未使用

header typepci设备类型,0x00(普通设备),0x01pci bridge),0x02cardbus bridge)。virtio是普通设备,这里是0x00

command字段用来控制pci设备,打开某些功能的开关,virtio-blk设备是(0x0507 = 0b1010111),command的各字段含义如下图

 

低三位的含义如下:

i/o space:如果pci设备实现了io空间,该字段用来控制是否接收总线上对io空间的访问。如果pci设备没有io空间,该字段不可写。

memory space:如果pci设备实现了内存空间,该字段用来控制是否接收总线上对内存空间的访问。如果pci设备没有内存空间,该字段不可写。

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可以是0x1000legacy也可以是0x1041morden),legacydevice id,在此基础上加0x40即是modern pci设备的device id。所以按照标准driver识别device id如果在 0x1000- 0x1040就是传统的virtio device id,但实际上并非如此,有些情况为了向前兼容实现了morden接口的设备也会使用lagecydevice id,所以我们可以看到驱动的判断并不是简单以device id为准的。

另一处就是上面说的capabilities pointer字段(0x34,因为在lagecy设备的情况,其设备的关键属性是直接放在其{banned}中国第一个bar空间的,而没有专门的capability所以其capabilities pointer字段(0x34指向不是virtiocapability,而是仅有的通用的msi-x capability,而modern情况下其指向的是virtio的定制capability

其他部分的配置空间没有什么特殊地方,如下图所示,不再过多介绍。

扩展配置空间(capability)

扩展配置空间即0x40~0xff64-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 附录hvirtio-blk实现的capability有两种,一种是msi-xmessage signaled interrupts - extension),id0x11,一种是vendor specificid0x9virtio_pci_cap_pci_cfg,后面一种capability设计目的就是让厂商实现自己的功能。

virtio morden的规范下,virtio的很多设备信息就是存放在多个virtioid0x9)的capabilty中的,准确的说真正的信息不一定是在capabilty结构用,因为capabilty大小有限,如果信息较多,这些信息会存放在设备的bar空间中,capabilty仅仅是存放这些信息在bar空间的具体偏移。根据virtio spec的规范,要实现virtio-pcicapabilty,其布局应该如下


点击(此处)折叠或打开

  1. struct virtio_pci_cap {
  2.     u8 cap_vndr; /* generic pci field: pci_cap_id_vndr */
  3.     u8 cap_next; /* generic pci field: next ptr. */
  4.     u8 cap_len; /* generic pci field: capability length */
  5.     u8 cfg_type; /* identifies the structure. */
  6.     u8 bar; /* where to find it. */
  7.     u8 padding[3]; /* pad to full dword. */
  8.     le32 offset; /* offset within bar. */
  9.     le32 length; /* length of the structure, in bytes. */
  10. };


对应字段含义如下:

1cap_vndr0x09,标识为virtio特有的capability

2cap_next:指向下一个capabilitypci配置空间的位置(offset);

3cap_lencapability的具体长度,包含 virtio_pci_cap结构;

4cfg_type:标识不同的virtio capability类型,具体有如下几个取值


点击(此处)折叠或打开

  1. /* common configuration */
  2. #define virtio_pci_cap_common_cfg 1
  3. /* notifications */
  4. #define virtio_pci_cap_notify_cfg 2
  5. /* isr status */
  6. #define virtio_pci_cap_isr_cfg 3
  7. /* device specific configuration */
  8. #define virtio_pci_cap_device_cfg 4
  9. /* pci configuration access */
  10. #define virtio_pci_cap_pci_cfg 5


注意:设备可以为每个类型的capability提供多个结构,例如有些实现中使用io访问要比memory访问效率更高,则会提供两个相同的capability,一个位于io bar,另一个位于memory bar,如果io bar可用则使用io bar的资源,否则fallbackmemory bar

(1) bar:取值0~5,对应pci配置空间中的6bar寄存器,表示这个capability是位于哪个bar空间的,当然这个bar空间可以是个io bar也可以是个memory bar

(2) offset:表示这个capability在对应bar空间的offset

(3) length:表示这个capability的结构长度;

可以看到virtio设备有多个类型的capabilty结构,下面我们来一一分析。

common configuration

  virtio设备的通用配置,对应的capabilty typevirtio_pci_cap_common_cfg,其在dpdk中定义如下:


点击(此处)折叠或打开

  1. /* fields in virtio_pci_cap_common_cfg: */
  2. struct virtio_pci_common_cfg {
  3.     /* about the whole device. */
  4.     uint32_t device_feature_select;    /* read-write */
  5.     uint32_t device_feature;    /* read-only */
  6.     uint32_t guest_feature_select;    /* read-write */
  7.     uint32_t guest_feature;        /* read-write */
  8.     uint16_t msix_config;        /* read-write */
  9.     uint16_t num_queues;        /* read-only */
  10.     uint8_t device_status;        /* read-write */
  11.     uint8_t config_generation;    /* read-only */

  12.     /* about a specific virtqueue. */
  13.     uint16_t queue_select;        /* read-write */
  14.     uint16_t queue_size;        /* read-write, power of 2. */
  15.     uint16_t queue_msix_vector;    /* read-write */
  16.     uint16_t queue_enable;        /* read-write */
  17.     uint16_t queue_notify_off;    /* read-only */
  18.     uint32_t queue_desc_lo;        /* read-write */
  19.     uint32_t queue_desc_hi;        /* read-write */
  20.     uint32_t queue_avail_lo;    /* read-write */
  21.     uint32_t queue_avail_hi;    /* read-write */
  22.     uint32_t queue_used_lo;        /* read-write */
  23.     uint32_t queue_used_hi;        /* read-write */
  24. };


这个结构前半部分描述的是设备的全局信息,后半部分描述的具体队列的信息。看到这里不知道大家有没有注意到一个问题,就是这里只有一份队列信息,如果是多队列情况下如何获取或者配置每个队列的信息呢?我们还是看一下dpdk18.11virtio-net的多队列初始化流程。其中关键函数是virtio_alloc_queues


点击(此处)折叠或打开

  1. static int
  2. virtio_alloc_queues(struct rte_eth_dev *dev)
  3. {
  4.     struct virtio_hw *hw = dev->data->dev_private;
  5.     uint16_t nr_vq = virtio_get_nr_vq(hw);
  6.     uint16_t i;
  7.     int ret;

  8.     hw->vqs = rte_zmalloc(null, sizeof(struct virtqueue *) * nr_vq, 0);
  9.     if (!hw->vqs) {
  10.         pmd_init_log(err, "failed to allocate vqs");
  11.         return -enomem;
  12.     }

  13.     for (i = 0; i < nr_vq; i) {
  14.         ret = virtio_init_queue(dev, i);
  15.         if (ret < 0) {
  16.             virtio_free_queues(hw);
  17.             return ret;
  18.         }
  19.     }

  20.     return 0;
  21. }


对于每个队列调用virtio_init_queue,其具体函数内容这里不再分析,主要是分配和初始化struct virtqueue结构。其相关数据结构关系如下图:

其中virtio_init_queue中{banned}最佳后会调用setup_queue,对于morden设备就是modern_setup_queue函数:


点击(此处)折叠或打开

  1. static int
  2. modern_setup_queue(struct virtio_hw *hw, struct virtqueue *vq)
  3. {
  4.     uint64_t desc_addr, avail_addr, used_addr;
  5.     uint16_t notify_off;

  6.     if (!check_vq_phys_addr_ok(vq))
  7.         return -1;

  8.     desc_addr = vq->vq_ring_mem;
  9.     avail_addr = desc_addr vq->vq_nentries * sizeof(struct vring_desc);
  10.     used_addr = rte_align_ceil(avail_addr offsetof(struct vring_avail,
  11.                              ring[vq->vq_nentries]),
  12.                  virtio_pci_vring_align);

  13.     rte_write16(vq->vq_queue_index, &hw->common_cfg->queue_select);

  14.     io_write64_twopart(desc_addr, &hw->common_cfg->queue_desc_lo,
  15.                  &hw->common_cfg->queue_desc_hi);
  16.     io_write64_twopart(avail_addr, &hw->common_cfg->queue_avail_lo,
  17.                  &hw->common_cfg->queue_avail_hi);
  18.     io_write64_twopart(used_addr, &hw->common_cfg->queue_used_lo,
  19.                  &hw->common_cfg->queue_used_hi);

  20.     notify_off = rte_read16(&hw->common_cfg->queue_notify_off);
  21.     vq->notify_addr = (void *)((uint8_t *)hw->notify_base
  22.                 notify_off * hw->notify_off_multiplier);

  23.     rte_write16(1, &hw->common_cfg->queue_enable);
  24.     return 0;
  25. }


我们看到这里会把软件分配的desc地址,avail ring地址,以及used ring地址设置到硬件对应的common_cfg中,并且通过common_cfg->queue_select来区分设置不同队列。如果后端是软件实现的话(如vhost_user),每一次这个写硬件操作就会触发set_vring_base的消息协商。所以队列的desc ringavail ringused ring都是在guest os的软件内存,设置到硬件上的仅仅上他们的地址。

此外,我们知道virtio队列有txqrxq,还有ctrlq,但是在这个结构里面怎么没看到队列类型呢?我们看下dpdkvirtio_net是如何判断virtio的队列类型的


点击(此处)折叠或打开

  1. static inline int
  2. virtio_get_queue_type(struct virtio_hw *hw, uint16_t vtpci_queue_idx)
  3. {
  4.     if (vtpci_queue_idx == hw->max_queue_pairs * 2)
  5.         return vtnet_cq;
  6.     else if (vtpci_queue_idx % 2 == 0)
  7.         return vtnet_rq;
  8.     else
  9.         return vtnet_tq;
  10. }


可以看到奇数vq就是txq,偶数vq就是rxqcq在{banned}最佳后。

notification configuration

对应的capabilty typevirtio_pci_cap_notify_cfg,其在dpdk中定义如下:


点击(此处)折叠或打开

  1. struct virtio_pci_notify_cap {
  2.     struct virtio_pci_cap cap;
  3.     uint32_t notify_off_multiplier;    /* multiplier for queue_notify_off. */
  4. };


   这个配置主要用来描述通知后端队列的地址(notify的地址),具体地址计算方式如下:

cap.offset queue_notify_off * notify_off_multiplier

cap.offsetnotify_off_multiplier直接从硬件的capabilty 获取即可,cap.offset指向对应bar空间的offset,而queue_notify_off是来自前面讲述的pci_common_cfg。可以看到如果notify_off_multiplier0,则所有队列会使用同一个地址notify。否则就会使用多个地址。

virtio0.5legacy模式)中,所有队列就会共享一个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

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

device-specific configuration

virtio_pci_cap_device_cfg 类型的capability用来存储设备特有的配置信息,如virtio-net情况其配置信息如下:


点击(此处)折叠或打开

  1. struct virtio_net_config {
  2.     /* the config defining mac address (if virtio_net_f_mac) */
  3.     uint8_t mac[ether_addr_len];
  4.     /* see virtio_net_f_status and virtio_net_s_* above */
  5.     uint16_t status;
  6.     uint16_t max_virtqueue_pairs;
  7.     uint16_t mtu;
  8. } __attribute__((packed));


pci configuration access

pci configuration access类型的capability是一种特殊的capability,它是为了提供给驱动另一种访问pci 配置的方法,驱动可以通过配置 cap.bar, cap.length, cap.offset以及pci_cfg_data来读写对应pci bar中的指定offset以及指定length的内容。

 

以上我们介绍的capability都是morden设备才有的,而lagecyvirtio 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}中国第一个bari/o bar,但是其实当前很多智能网卡(dpu)通过硬件模拟virtio设备,所有设备都是在一个pcie树上,pio资源是有限的,所以一般都采用memory bar实现,所以后续dpdk也对此进行了修改。详情可见如下patch:

lagecy virtio设备的common configurationpcie bar0上的layout如下图所示:

 

这里有个需要注意的地方,就是设备队列的地址(queue address)是32位的,而在morden设备capability中的common configurationqueue address64位,这意味着什么呢?就是lagecy设备情况下,驱动分配队列地址时物理地址必须在16t内存一下,为什么是16t是因为一个page 4k12bit),由于队列地址都是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-xlayout如下,紧随着(如果存在)common configuration

 

 在之后就是一些device-specific configuration了。总而言之,lagecy的各种配置都是存放于pcie设备的bar 0中的,并且不支持capability

{banned}最佳后我们看一下virio_netlagecymorden设备配置空间的真实布局对比。如下图

 

lagecy设备配置空间

 

morden设备的配置空间

 

lagecy设备bar0 i/o bar

 

lagecy设备bar0 memory bar

 

morden设备配置空间

 

pcie扩展配置空间

pcie规范在pci规范的基础上,将配置空间扩展到4kb,即0x100~0xfff这段配置空间是pcie设备特有的pcie扩展配置空间中用于存放pcie设备独有的一些capability结构,而pci设备不能使用这段空间。不过目前virtio设备也没有使用这段空间。

pci bar空间

pci配置空间和内存空间是分离的pci内存空间根据不同设备实现不同其大小和个数也不同,这些pci设备的内部存储空间我们称之为bar空间,因为它的基地址存放在配置空间的bar寄存器中。设备出厂时,这些空间的大小和属性都写在configuration bar寄存器里面,然后上电后,系统软件读取这些bar,分别为其分配对应的系统内存空间,并把相应的内存基地址写回到bar。(bar的地址其实是pci总线域的地址,cpu访问的是存储器域的地址,cpu访问pcie设备时,需要把总线域地址转换成存储器域的地址。)如下图所示说明配置空间和bar空间的关系。

我们以一个mordenvirtio-net设备为例,看下pci配置空间和bar空间的关系:

 

 

pci配置空间的访问方式

x86处理器通过定义两个io端口寄存器,分别为config_addressconfig_data寄存器,其地址为0xcf80xcfc。通过在config_address端口填入pci设备的bdf和要访问设备寄存器编号,在config_data上写入或者读出pci配置空间的内容来实现对配置空间的访问。

pcie规范在pci规范的基础上,将配置空间扩展到4kb。原来的cf8/cfc方法仍然可以访问所有pcie设备配置空间的头255b,但是该方法访问不了剩下的(4k-255)配置空间。怎么办呢?intel提供了另外一种pcie配置空间访问方法:通过将配置空间映射到memory map iommio)空间,对pcie配置空间可以像对内存一样进行读写访问了。如图

 

因此对pci配置空间的访问方式有两种:

1. 传统方式,写io端口0xcfch0xcf8h。只能访问pci/pcie设备的开始256个字节(因为pci设备的配置空间本来就只有256个字节);
2. pcie的方式,就是上面提到的mmio方式,它可以访问4k个字节的配置空间。

 

参考

阅读(599) | 评论(0) | 转发(0) |
0

上一篇:

下一篇:iotlb和ats

给主人留下些什么吧!~~
")); function link(t){ var href= $(t).attr('href'); href ="?url=" encodeuricomponent(location.href); $(t).attr('href',href); //setcookie("returnouturl", location.href, 60, "/"); }
网站地图