Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3427041
  • 博文数量: 198
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7246
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

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

文章分类

全部博文(198)

文章存档

2023年(9)

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设备实现

—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:

https://lore.kernel.org/dpdk-dev/b34311c7-5b09-a1f6-1957-c9e19bb2a273@intel.com/T/

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个字节的配置空间。

 

参考

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

上一篇:从virtio看iommu和DMA的关系

下一篇:iotlb和ATS

给主人留下些什么吧!~~