网上从服务器和虚拟化层面介绍SR-IOV应用的文章很多了。
本文重点从支持SR-IOV的设备(EP)及其驱动来讨论。
对于SR-IOV的设备(EP)来说,无非就是一个device通过物理功能(PF)虚拟出关联的若干个虚拟功能(VF)。
host的驱动通过SR-IOV Extended Capability识别并配置VF使能,加载VF驱动,使得只有一个物理端口的pcie设备在软件层面
体现出多个pcie设备。在虚拟化应用上可以将虚拟功能VF分配给不同的客户机进行使用。
sysfs下,支持SR-IOV的device提供sriov_numvfs来控制VF的打开和关闭.VF打开后一般需要加载单独的VF驱动。
rom sysfs:
'nr_virtfn' is number of VFs to be enabled
echo 'nr_virtfn' > \ /sys/bus/pci/devices//sriov_numvfs
关闭VF
echo 0 > \ /sys/bus/pci/devices//sriov_numvfs
实例
检查、配置vf数量,因为还没配置,所以是0
[root@g1 ~]# cat /sys/class/infiniband/mlx5_1/device/mlx5_num_vfs
0
这里我测试把OFED驱动中的vf数量配置成1
echo 1> /sys/class/infiniband/mlx5_1/device/mlx5_num_vfs
在PCI总线上,用lspci可以看到ConnectX-6对应的设备出现了多次,其中Function 1是刚才配置好的Virtual Function
[root@g1 ~]# lspci|grep Mellanox
04:00.0 Infiniband controller: Mellanox Technologies MT28908 Family [ConnectX-6]
04:00.1 Infiniband controller: Mellanox Technologies MT28908 Family [ConnectX-6 Virtual Functio
首先回顾一下Routing ID,Routing ID就是BDF number,即采用Bus Number、Device Number和Function Number来确定目标设备的位置的id。
SR-IOV Extended Capability中用FirstVF Offset和VF Stride来标记VF的Routing ID。VF的Routing ID是以PF的Routing ID值为参考来计算的。
FirstVF Offset:第一个VF相对PF的Routing ID的偏移量
VF Stride: 相邻VF之间的Routing ID的偏移量(步进值)
PF的Routing ID在PF枚举之后就已经分配好了。PF的驱动程序通过配置SR-IOV Extended Capability,打开这个PF关联的VF之后,通过FirstVF Offset和VF Stride就能计算出VF们的Routing ID
这个计算也非常简单,按照BDF的位域规则累加就可以了,linux下pcie的device 和 function number是一起编码的(就是devfn),计算代码如下:
/* vf_bus = pf_bus + (pf_devfn + offset + stride * vf_id) >> 8 */
int pci_iov_virtfn_bus(struct pci_dev *dev, int vf_id)
{
if (!dev->is_physfn)
return -EINVAL;
return dev->bus->number + ((dev->devfn + dev->sriov->offset +
dev->sriov->stride * vf_id) >> 8);
}
/* vf_devfn = (pf_devfn + offset + stride * vf_id) & 0xff */
int pci_iov_virtfn_devfn(struct pci_dev *dev, int vf_id)
{
if (!dev->is_physfn)
return -EINVAL;
return (dev->devfn + dev->sriov->offset +
dev->sriov->stride * vf_id) & 0xff;
}
VF的BAR[n]空间是通过PF的SR-IOV Capability中每个VF_BAR[n]来分配的,和VF Configuration Space的BAR无关。
SR-IOV Capability中VF_BAR[n]的介绍如下:
9.3.3.14 VF BAR0 (Offset 24h), VF BAR1 (Offset 28h), VF BAR2 (Offset 2Ch), VF BAR3 (Offset
30h), VF BAR4 (Offset 34h), VF BAR5 (Offset 38h)
These fields must define the VF’s Base Address Registers (BARs). These fields behave as normal PCI BARs, as described in Section 7.5.1 . They can be sized by writing all 1s and reading back the contents of the BARs as described in Section 7.5.1.2.1 , complying with the low order bits that define the BAR type fields.
These fields may have their attributes affected by the VF Resizable BAR Extended Capability (see Section 9.3.7.5 ) if it is implemented.
The amount of address space decoded by each BAR shall be an integral multiple of System Page Size.
这些字段必须定义VF的基地址寄存器(BARs)。这些字段的作用与正常的PCI BARs相似,如7.5.1节所述。它们可以通过写入全1并按照第7.5.1.2.1节中描述的那样读入BAR的内容来调整大小,通过低BIT可以判断BAR的类型和属性。
如果实现了VF可调整大小的BAR扩展功能,这些字段的属性可能会受到影响(参见9.3.7.5节)
Each VF BARn, when “sized” by writing 1s and reading back the contents, describes the amount of address space consumed and alignment required by a single Virtual Function, per BAR. When written with an actual address value, and VF Enable and VF MSE are Set, the BAR maps NumVFs BARs. In other words, the base address is the address of the first VF BARn associated with this PF and all subsequent VF BARn address ranges follow as described below.
每个VF BARn通过写入1并读取内容来“调整大小”时,每个VF BAR都描述了单个VF所消耗的地址空间量和所需的对齐方式。当使用实际的地址值写入时,并且设置了VF Enable和VF MSE,则VF BAR映射了NumVFs个 BAR空间。换句话说,VF BAR的起始地址是与这个PF关联的第一个VF BAR的起始地址,所有后续VF的 BAR地址空间则依次向后排列。
VF BARs shall only support 32-bit and 64-bit memory space. PCI I/O Space is not supported in VFs. Bit 0 of any
implemented VF BARx must be RO 0b except for a VF BARx used to map the upper 32 bits of a 64-bit memory VF BAR pair.
The alignment requirement and size read is for a single VF, but when VF Enable is Set and VF MSE is Set, the BAR contains the base address for all (NumVFs) VF BARn
VF BARs只能支持32位和64位内存空间映射。VFs中不支持PCI I/O空间。所有VF BARx的bit0必须是只读的0值,除了用于映射64位内存VF BAR上32位的VF BARx。
对齐要求和大小读取是针对单个VF的,但是当使能VF Enable和VF MSE时,该BAR实际上包含所有(NumVFs多个) VF BAR的BAR地址空间
简单的总结一下:
1. PF 的VF_BAR[n]行为上是和常规的BAR是一样的(全写1来确定大小...等等分配机制)
2. 但是PF 的VF_BAR 地址空间分配之后,代表的含义与PF自己的BAR不同。VF_BAR对应的是PF关联的每个VF的BAR空间。
3. VF1的BAR空间完全与PF 的VF_BAR 地址空间相同,也就是PF 的VF_BAR[0-5]空间刚好就是对应第一个VF的bar[0-5]
4. 那后续的VFn的空间在哪呢,VFn的每一个BAR[n]空间都依次在VF1的bar[n]后依次排列(大小是相同的)
5. 也就是说,虽然VF_BAR只显式的看到一份VF的BAR空间,但实际上有NumVFs份 BAR空间在每个VF_BAR后依次存在,然后对应就是每个VF的bar。(类似一个矩阵,PF有关联的NumVFs个VF,每个VF有6个BAR。)
(这个讲的有点绕了,看图或者代码会好理解一点)
下面这张图展示了PF的BAR0, VF BAR0,和每个VF的BAR0空间的关系。其他的BARn也都是一样的。
linux 驱动分配VF BAR空间的代码如下:
PF驱动调用sriov_init的时候先按照SR-IOV Capability中每个VF_BAR[n] * TotalVF 来分配PCI空间的resource放在PF 的dev->resource[i + PCI_IOV_RESOURCES]中,这个时候VF还没使能和枚举。
static int sriov_init(struct pci_dev *dev, int pos)
{
int i, bar64;
int rc;
int nres;
u32 pgsz;
u16 ctrl, total;
struct pci_sriov *iov;
struct resource *res;
struct pci_dev *pdev;
...
for (i = 0; i < PCI_SRIOV_NUM_BARS; i++) {
res = &dev->resource[i + PCI_IOV_RESOURCES];
/*
* If it is already FIXED, don't change it, something
* (perhaps EA or header fixups) wants it this way.
*/
if (res->flags & IORESOURCE_PCI_FIXED)
bar64 = (res->flags & IORESOURCE_MEM_64) ? 1 : 0;
else
bar64 = __pci_read_base(dev, pci_bar_unknown, res,
pos + PCI_SRIOV_BAR + i * 4);
if (!res->flags)
continue;
if (resource_size(res) & (PAGE_SIZE - 1)) {
rc = -EIO;
goto failed;
}
iov->barsz[i] = resource_size(res);
/* 重点在这一行,驱动按照VF_BAR分配的空间又强行乘以VF的total数量 */
res->end = res->start + resource_size(res) * total - 1;
dev_info(&dev->dev, "VF(n) BAR%d space: %pR (contains BAR%d for %d VFs)\n",
i, res, i, total);
i += bar64;
nres++;
}
...
}
然后在使能VF的时候sriov_enable->pci_iov_add_virtfn,再从PF之前申请到的dev->resource[i + PCI_IOV_RESOURCES]依次把对应的resource空间拿出去赋给每个VF 的dev->resource,然后再进行request_resource
int pci_iov_add_virtfn(struct pci_dev *dev, int id, int reset)
{
int i;
int rc = -ENOMEM;
u64 size;
char buf[VIRTFN_ID_LEN];
struct pci_dev *virtfn;
struct resource *res;
struct pci_sriov *iov = dev->sriov;
struct pci_bus *bus;
...
for (i = 0; i < PCI_SRIOV_NUM_BARS; i++) {
res = &dev->resource[i + PCI_IOV_RESOURCES];
if (!res->parent)
continue;
virtfn->resource[i].name = pci_name(virtfn);
virtfn->resource[i].flags = res->flags;
size = pci_iov_resource_size(dev, i + PCI_IOV_RESOURCES);
virtfn->resource[i].start = res->start + size * id;
virtfn->resource[i].end = virtfn->resource[i].start + size - 1;
rc = request_resource(res, &virtfn->resource[i]);
BUG_ON(rc);
}
...
return rc;
}
打开VF之后,通过VF的BDF number访问VF的配置空间(和PF一样的)。
VF作为一个function也有自己Type 0 Configuration Space Header以及各种Capability存放在配置空间。
但是作为一个虚拟function,VF配置空间中的很多字段都是没用的,软件上应该直接采用PF的值。
因为配置空间主要是存放PCI的功能寄存器,而VF的电源,link,错误管理,BAR等其实完全是交给PF来管理的(VF和PF共享一个PCIe端口)。只有与VF独立功能相关的才会有VF单独的实现(例如MSI-X中断)。
例如:VF的Vendor ID和Device ID就是全F的无效值,软件上VF的Vendor ID应该直接采用PF的值,
Device ID应该从PF配置空间中的VF Device ID字段来获取。
pci_iov_add_virtfn中有这样一行代码:
virtfn->vendor = dev->vendor;
pci_read_config_word(dev, iov->pos + PCI_SRIOV_VF_DID, &virtfn->device);
其中dev就是 virtfn关联的PF。
目前看来,配置空间涉及的VF独立的功能有:
FLR(VF可以单独进行复位)
MSI/MSI-X(VF应该单独实现自己独立的MSI/X capabilities,从而每个VF在系统中都有独立的中断向量,但VF无法支持INTx)
总结:VF通过共享PF的PCIe端口,节省了很多连接上的开销。在独立功能的支持方面,VF主要需要实现其独立的BAR空间以及中断资源,用于实现VF的独立功能,以达到从PCIe层面的虚拟化。