virtio的legacy和modern模式

转自:http://blog.chinaunix.net/uid-28541347-id-5875921.html
从virtio看PCIe设备实现
—lvyilong316
virtio的legacy和modern模式
我们经常在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相关的。

PCIe设备空间布局
PCI或PCIe设备有自己独立的地址空间。地址空间又可以分为两类:一类是配置空间,这是每个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~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。

其他部分的配置空间没有什么特殊地方,如下图所示,不再过多介绍。
在这里插入图片描述
扩展配置空间(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,其布局应该如下:

struct virtio_pci_cap {
    u8 cap_vndr; /* Generic PCI field: PCI_CAP_ID_VNDR */
    u8 cap_next; /* Generic PCI field: next ptr. */
    u8 cap_len; /* Generic PCI field: capability length */
    u8 cfg_type; /* Identifies the structure. */
    u8 bar; /* Where to find it. */
    u8 padding[3]; /* Pad to full dword. */
    le32 offset; /* Offset within bar. */
    le32 length; /* Length of the structure, in bytes. */
};

对应字段含义如下:

(1)cap_vndr:0x09,标识为virtio特有的capability;

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

(3)cap_len:capability的具体长度,包含 virtio_pci_cap结构;

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

/* Common configuration */
#define VIRTIO_PCI_CAP_COMMON_CFG 1
/* Notifications */
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2
/* ISR Status */
#define VIRTIO_PCI_CAP_ISR_CFG 3
/* Device specific configuration */
#define VIRTIO_PCI_CAP_DEVICE_CFG 4
/* PCI configuration access */
#define VIRTIO_PCI_CAP_PCI_CFG 5

注意:设备可以为每个类型的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中定义如下:

/* Fields in VIRTIO_PCI_CAP_COMMON_CFG: */
struct virtio_pci_common_cfg {
    /* About the whole device. */
    uint32_t device_feature_select;    /* read-write */
    uint32_t device_feature;    /* read-only */
    uint32_t guest_feature_select;    /* read-write */
    uint32_t guest_feature;        /* read-write */
    uint16_t msix_config;        /* read-write */
    uint16_t num_queues;        /* read-only */
    uint8_t device_status;        /* read-write */
    uint8_t config_generation;    /* read-only */

    /* About a specific virtqueue. */
    uint16_t queue_select;        /* read-write */
    uint16_t queue_size;        /* read-write, power of 2. */
    uint16_t queue_msix_vector;    /* read-write */
    uint16_t queue_enable;        /* read-write */
    uint16_t queue_notify_off;    /* read-only */
    uint32_t queue_desc_lo;        /* read-write */
    uint32_t queue_desc_hi;        /* read-write */
    uint32_t queue_avail_lo;    /* read-write */
    uint32_t queue_avail_hi;    /* read-write */
    uint32_t queue_used_lo;        /* read-write */
    uint32_t queue_used_hi;        /* read-write */
};

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

static int
virtio_alloc_queues(struct rte_eth_dev *dev)
{
    struct virtio_hw *hw = dev->data->dev_private;
    uint16_t nr_vq = virtio_get_nr_vq(hw);
    uint16_t i;
    int ret;

    hw->vqs = rte_zmalloc(NULL, sizeof(struct virtqueue *) * nr_vq, 0);
    if (!hw->vqs) {
        PMD_INIT_LOG(ERR, "failed to allocate vqs");
        return -ENOMEM;
    }

    for (i = 0; i < nr_vq; i++) {
        ret = virtio_init_queue(dev, i);
        if (ret < 0) {
            virtio_free_queues(hw);
            return ret;
        }
    }

    return 0;
}

对于每个队列调用virtio_init_queue,其具体函数内容这里不再分析,主要是分配和初始化struct virtqueue结构。其相关数据结构关系如下图:
在这里插入图片描述
其中virtio_init_queue中{BANNED}最佳后会调用setup_queue,对于morden设备就是modern_setup_queue函数:

static int
modern_setup_queue(struct virtio_hw *hw, struct virtqueue *vq)
{
    uint64_t desc_addr, avail_addr, used_addr;
    uint16_t notify_off;

    if (!check_vq_phys_addr_ok(vq))
        return -1;

    desc_addr = vq->vq_ring_mem;
    avail_addr = desc_addr + vq->vq_nentries * sizeof(struct vring_desc);
    used_addr = RTE_ALIGN_CEIL(avail_addr + offsetof(struct vring_avail,
                             ring[vq->vq_nentries]),
                 VIRTIO_PCI_VRING_ALIGN);

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

    io_write64_twopart(desc_addr, &hw->common_cfg->queue_desc_lo,
                 &hw->common_cfg->queue_desc_hi);
    io_write64_twopart(avail_addr, &hw->common_cfg->queue_avail_lo,
                 &hw->common_cfg->queue_avail_hi);
    io_write64_twopart(used_addr, &hw->common_cfg->queue_used_lo,
                 &hw->common_cfg->queue_used_hi);

    notify_off = rte_read16(&hw->common_cfg->queue_notify_off);
    vq->notify_addr = (void *)((uint8_t *)hw->notify_base +
                notify_off * hw->notify_off_multiplier);

    rte_write16(1, &hw->common_cfg->queue_enable);
    return 0;
}

我们看到这里会把软件分配的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的队列类型的

static inline int
virtio_get_queue_type(struct virtio_hw *hw, uint16_t vtpci_queue_idx)
{
    if (vtpci_queue_idx == hw->max_queue_pairs * 2)
        return VTNET_CQ;
    else if (vtpci_queue_idx % 2 == 0)
        return VTNET_RQ;
    else
        return VTNET_TQ;
}

可以看到奇数vq就是TXQ,偶数vq就是RXQ,CQ在{BANNED}最佳后。

l Notification configuration

对应的capabilty type为VIRTIO_PCI_CAP_NOTIFY_CFG,其在DPDK中定义如下:

struct virtio_pci_notify_cap {
    struct virtio_pci_cap cap;
    uint32_t notify_off_multiplier;    /* Multiplier for queue_notify_off. */
};

这个配置主要用来描述通知后端队列的地址(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情况其配置信息如下:

struct virtio_net_config {
    /* The config defining mac address (if VIRTIO_NET_F_MAC) */
    uint8_t mac[ETHER_ADDR_LEN];
    /* See VIRTIO_NET_F_STATUS and VIRTIO_NET_S_* above */
    uint16_t status;
    uint16_t max_virtqueue_pairs;
    uint16_t mtu;
} __attribute__((packed));

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扩展配置空间
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空间的关系。

在这里插入图片描述
我们以一个morden的virtio-net设备为例,看下PCI配置空间和BAR空间的关系:在这里插入图片描述
PCI配置空间的访问方式
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个字节的配置空间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值