DPDK virtio-net加载注意事项
——lvyilong316
关于DPDK virtio-net pmd的初始化流程我们前文有基于DPDK 17.11的分析,这篇文章主要介绍下其使用过程中的一些注意事项和加载初始化的细节补充。首先我们回顾一下17.11的virtio-net加载流程,如下图所示。而我们今天主要介绍有关的是图中标黄色的部分,即和virtio-net pcie映射有关的部分。
我们看下vtpci_init的实现。可以看到对于morden设备,仅需调用virtio_read_caps,而对于lagecy设备则需调用rte_pci_ioport_map。
-
int
-
vtpci_init(struct rte_pci_device *dev, struct virtio_hw *hw)
-
{
-
/*
-
* Try if we can succeed reading virtio pci caps, which exists
-
* only on modern pci device. If failed, we fallback to legacy
-
* virtio handling.
-
*/
-
if (virtio_read_caps(dev, hw) == 0) {
-
PMD_INIT_LOG(INFO, "modern virtio pci detected.");
-
virtio_hw_internal[hw->port_id].vtpci_ops = &modern_ops;
-
hw->modern = 1;
-
return 0;
-
}
-
-
PMD_INIT_LOG(INFO, "trying with legacy virtio pci.");
-
if (rte_pci_ioport_map(dev, 0, VTPCI_IO(hw)) < 0) {
-
if (dev->kdrv == RTE_KDRV_UNKNOWN &&
-
(!dev->device.devargs ||
-
dev->device.devargs->bus !=
-
rte_bus_find_by_name("pci"))) {
-
PMD_INIT_LOG(INFO,
-
"skip kernel managed virtio device.");
-
return 1;
-
}
-
return -1;
-
}
-
-
virtio_hw_internal[hw->port_id].vtpci_ops = &legacy_ops;
-
hw->modern = 0;
-
-
return 0;
-
}
其中virtio_read_caps的调用过程如下所示,可以看到根据设备使用的vfio驱动,或者igb_uio/uio_pci_generic,选择不同的map_resource函数。下面我们分别解析。
vfio mem map
使用vfio时的资源映射主要由函数pci_vfio_map_resource完成,其主要流程如下图所示:
这里我们针对其中一个个关键问题进行探讨:当我们在虚拟机中为了使用DPDK virtio-net而使用vfio时,是否一定需要支持iommu呢?
这里我们可以从以下调用逻辑看出关系:
pci_vfio_map_resource-->pci_vfio_map_resource_primary-->rte_vfio_setup_device-->vfio_get_group_no
从rte_vfio_setup_device中可以看出,vfio_get_group_no如果返回0则表示这个设备没有被vfio接管,所以直接返错。
-
int
-
rte_vfio_setup_device(const char *sysfs_base, const char *dev_addr,
-
int *vfio_dev_fd, struct vfio_device_info *device_info)
-
{
-
struct vfio_group_status group_status = {
-
.argsz = sizeof(group_status)
-
};
-
int vfio_group_fd;
-
int iommu_group_no;
-
int ret;
-
-
/* get group number */
-
ret = vfio_get_group_no(sysfs_base, dev_addr, &iommu_group_no);
-
if (ret == 0) {
-
RTE_LOG(WARNING, EAL, " %s not managed by VFIO driver, skipping\n",
-
dev_addr);
-
return 1;
-
}
-
//...
-
}
而vfio_get_group_no函数会尝试打开/sys/bus/pci/devices/$bdf/iommu_group来获取设备的iommu group,但是如果没有打开iommu设备是不会被加入iommu group的。
-
int
-
vfio_get_group_no(const char *sysfs_base,
-
const char *dev_addr, int *iommu_group_no)
-
{
-
char linkname[PATH_MAX];
-
char filename[PATH_MAX];
-
char *tok[16], *group_tok, *end;
-
int ret;
-
-
memset(linkname, 0, sizeof(linkname));
-
memset(filename, 0, sizeof(filename));
-
-
/* try to find out IOMMU group for this device */
-
snprintf(linkname, sizeof(linkname),
-
"%s/%s/iommu_group", sysfs_base, dev_addr);
-
-
ret = readlink(linkname, filename, sizeof(filename));
-
-
/* if the link doesn't exist, no VFIO for us */
-
if (ret < 0)
-
return 0;
-
//...
-
}
所以看其来是必须依赖VM内开启iommu吗?说道vm开启iommu其实有两个条件:
1. Qemu虚拟化支持viommu;
2. VM OS内部开启iommu,如下图所示:
但是目前qemu支持viomm由于稳定性和性能问题很少使用,所以一般情况是不支持的,所以仅仅OS开启iommu是没有什么意义的,如下图所示,OS开启iommu后直接绑定设备到vfio-pci是有问题的。
内核会打印如下日志:
Jul 9 01:04:28 localhost kernel: vfio-pci: probe of 0000:00:06.0 failed with error -22
对应内核代码如下:
-
static int vfio_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
-
{
-
int ret;
-
-
if (pdev->hdr_type != PCI_HEADER_TYPE_NORMAL)
-
return -EINVAL;
-
-
group = vfio_iommu_group_get(&pdev->dev);
-
if (!group)
-
return -EINVAL;
-
ret = vfio_add_group_dev(&pdev->dev, &vfio_pci_ops, vdev);
-
if (ret) {
-
vfio_iommu_group_put(group, &pdev->dev);
-
kfree(vdev);
-
return ret;
-
}
-
return ret;
-
}
22是EINVAL,看代码是iommu group导致的,再分析是因为机器没有iommu硬件单元,意思就是qemu没有模拟出iommu硬件单元,把模拟的iommu称为虚拟的iommu,就是viommu。
那是不是qemu不支持viommu,我们就无法在vm中使用vfio了呢?其实也不是这样,vfio模块有一个noiommu_mode的选项,如下所示,开启这个选项后设备即可绑定vfio-pci。
绑定后设备也会生成对应的iommu_group,只是都是0,如下图所示
而如果vfio开启了noiommu_mode,系统OS是否开启iommu也无所谓。
uio mem map
使用igb_uio或uio_pci_generic时的资源映射流程主要工作由pci_uio_map_resource来完成,其主要流程如下所示。
其中用于保存uio场景映射资源的关键结构maped_pci_resource如下所示。
其中的每一个pci_map结构对应设备的一个可用的BAR空间,PCI设备{BANNED}最佳大可用支持6个BAR空间,但是设备并不一定全部使用,这个可以通过读取设备的resource查看地址是否为0确认。如果设备使用了BAR0,则reource的第1行地址就不为0,并对应在设备目录下有resource0这个文件,但是注意这个文件是不能直接读取的。
pio map
在virtio_read_caps有如下逻辑,如果设备没有以下对应的CAPABILITY_LIST,则不认为是一个morden设备,而是一个lagecy设备。
-
if (hw->common_cfg == NULL || hw->notify_base == NULL ||
-
hw->dev_cfg == NULL || hw->isr == NULL) {
-
PMD_INIT_LOG(INFO, "no modern virtio pci device found.");
-
return -1;
-
}
这种情况就会调用rte_pci_ioport_map进行io资源映射(我们在vfio的mem资源映射过程中可以看到vfio是跳过io BAR的映射的),又会因为使用vfio和uio的不同调用不同的函数。
-
int
-
rte_pci_ioport_map(struct rte_pci_device *dev, int bar,
-
struct rte_pci_ioport *p)
-
{
-
int ret = -1;
-
-
switch (dev->kdrv) {
-
#ifdef VFIO_PRESENT
-
case RTE_KDRV_VFIO:
-
if (pci_vfio_is_enabled())
-
ret = pci_vfio_ioport_map(dev, bar, p);
-
break;
-
#endif
-
case RTE_KDRV_IGB_UIO:
-
ret = pci_uio_ioport_map(dev, bar, p);
-
break;
-
case RTE_KDRV_UIO_GENERIC:
-
#if defined(RTE_ARCH_X86)
-
ret = pci_ioport_map(dev, bar, p);
-
#else
-
ret = pci_uio_ioport_map(dev, bar, p);
-
#endif
-
break;
-
case RTE_KDRV_NONE:
-
#if defined(RTE_ARCH_X86)
-
ret = pci_ioport_map(dev, bar, p);
-
#endif
-
break;
-
default:
-
break;
-
}
-
-
if (!ret)
-
p->dev = dev;
-
-
return ret;
-
}
这里面问题是当使用uio时需要注意两点:
1. 如果是x86环境,pci_uio_ioport_map会通过以下路径获取对应的BAR的pio地址:
/sys/bus/pci/devices/$bdf/uio/portio/port%d/start,如果是其他架构(比如arm)则是从/sys/bus/pci/devices/$bdf/resource找pio BAR进行映射。但在新版本的DPDK(21.05后),对这两种方式进行了统一,都是使用sys下的resource进行映射;
2. 通过1可以看到无论是哪种方式DPDK都认为lagecy设备的BAR必须是一个PIO BAR,但这并不符合virtio规范,virtio中也没有这么规定,只是早期纯软件虚拟化大家也基本都是这么实现的。但是随着智能网卡、DPU的普及,这种PIO方式实现BAR的方式不再适用,因为对应x86来说,PIO的地址空间十分有限,如果都使用PIO作为设备BAR,则支持设备的数量将十分受限。因此基于智能网卡的很多实现也将lagecy设备的BAR实现为了mmio方式。但由于DPDK早期的实现,在21.05版本之前,如果使用uio是无法支持的。针对这点新版本的DPDK也做了修改。因此在这种智能网卡的虚拟化环境如果遇到这种问题,要么升级DPDK版本继续用uio,要么就改用vfio。
以下是对比DPDK17.11和DPDK 22.11的实现
-
int
-
pci_uio_ioport_map(struct rte_pci_device *dev, int bar,
-
struct rte_pci_ioport *p)
-
{
-
FILE *f;
-
char buf[BUFSIZ];
-
char filename[PATH_MAX];
-
uint64_t phys_addr, end_addr, flags;
-
int fd, i;
-
void *addr;
-
-
/* open and read addresses of the corresponding resource in sysfs */
-
snprintf(filename, sizeof(filename), "%s/" PCI_PRI_FMT "/resource",
-
rte_pci_get_sysfs_path(), dev->addr.domain, dev->addr.bus,
-
dev->addr.devid, dev->addr.function);
-
f = fopen(filename, "r");
-
if (f == NULL) {
-
RTE_LOG(ERR, EAL, "Cannot open sysfs resource: %s\n",
-
strerror(errno));
-
return -1;
-
}
-
for (i = 0; i < bar + 1; i++) {
-
if (fgets(buf, sizeof(buf), f) == NULL) {
-
RTE_LOG(ERR, EAL, "Cannot read sysfs resource\n");
-
goto error;
-
}
-
}
-
if (pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr,
-
&end_addr, &flags) < 0)
-
goto error;
-
if ((flags & IORESOURCE_IO) == 0) {
-
RTE_LOG(ERR, EAL, "BAR %d is not an IO resource\n", bar);
-
goto error;
-
//...
-
}
对比DPDK 22.11的实现可以看到无论是pio (IORESOURCE_IO)还是mmio( IORESOURCE_MEM)uio的iomap都做了支持。
-
int
-
pci_uio_ioport_map(struct rte_pci_device *dev, int bar,
-
struct rte_pci_ioport *p)
-
{
-
FILE *f = NULL;
-
char dirname[PATH_MAX];
-
char filename[PATH_MAX];
-
char buf[BUFSIZ];
-
uint64_t phys_addr, end_addr, flags;
-
unsigned long base;
-
int i, fd;
-
-
/* open and read addresses of the corresponding resource in sysfs */
-
snprintf(filename, sizeof(filename), "%s/" PCI_PRI_FMT "/resource",
-
rte_pci_get_sysfs_path(), dev->addr.domain, dev->addr.bus,
-
dev->addr.devid, dev->addr.function);
-
f = fopen(filename, "r");
-
if (f == NULL) {
-
RTE_LOG(ERR, EAL, "%s(): Cannot open sysfs resource: %s\n",
-
__func__, strerror(errno));
-
return -1;
-
}
-
-
for (i = 0; i < bar + 1; i++) {
-
if (fgets(buf, sizeof(buf), f) == NULL) {
-
RTE_LOG(ERR, EAL, "%s(): Cannot read sysfs resource\n", __func__);
-
goto error;
-
}
-
}
-
if (pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr,
-
&end_addr, &flags) < 0)
-
goto error;
-
-
if (flags & IORESOURCE_IO) {
-
base = (unsigned long)phys_addr;
-
if (base > PIO_MAX) {
-
RTE_LOG(ERR, EAL, "%s(): %08lx too large PIO resource\n", __func__, base);
-
goto error;
-
}
-
-
RTE_LOG(DEBUG, EAL, "%s(): PIO BAR %08lx detected\n", __func__, base);
-
} else if (flags & IORESOURCE_MEM) {
-
base = (unsigned long)dev->mem_resource[bar].addr;
-
RTE_LOG(DEBUG, EAL, "%s(): MMIO BAR %08lx detected\n", __func__, base);
-
} else {
-
RTE_LOG(ERR, EAL, "%s(): unknown BAR type\n", __func__);
-
goto error;
-
}
-
//...
-
}
相关patch为:https://lore.kernel.org/dpdk-dev/b34311c7-5b09-a1f6-1957-c9e19bb2a273@intel.com/T/
中断通知
下面看一下,如果前端DPDK使用virtio-net pmd,在收发包过程中notify后端的问题。首先lagecy设备和modern设备notify后端的流程是不一样的。lagecy设备根据使用vfio还是uio调用如下。
其中vfio会调用pci_vfio_ioport_write。
-
void
-
pci_vfio_ioport_write(struct rte_pci_ioport *p,
-
const void *data, size_t len, off_t offset)
-
{
-
const struct rte_intr_handle *intr_handle = &p->dev->intr_handle;
-
-
if (pwrite64(intr_handle->vfio_dev_fd, data,
-
len, p->base + offset) <= 0)
-
RTE_LOG(ERR, EAL,
-
"Can't write to PCI bar (%" PRIu64 ") : offset (%x)\n",
-
VFIO_GET_REGION_IDX(p->base), (int)offset);
-
}
可以看到lagecy设备如果使用vfio的情况会使用pwrite写文件fd的方式进行notify,这个在流量较大的情况会产生较高的sys开销。而在uio时调用的是pci_uio_ioport_write,如下所示,是通过内存映射方式notify,因此不会有系统调用开销。
-
void
-
pci_uio_ioport_write(struct rte_pci_ioport *p,
-
const void *data, size_t len, off_t offset)
-
{
-
const uint8_t *s;
-
int size;
-
uintptr_t reg = p->base + offset;
-
-
for (s = data; len > 0; s += size, reg += size, len -= size) {
-
if (len >= 4) {
-
size = 4;
-
#if defined(RTE_ARCH_X86)
-
outl_p(*(const uint32_t *)s, reg);
-
#else
-
*(volatile uint32_t *)reg = *(const uint32_t *)s;
-
#endif
-
} else if (len >= 2) {
-
size = 2;
-
#if defined(RTE_ARCH_X86)
-
outw_p(*(const uint16_t *)s, reg);
-
#else
-
*(volatile uint16_t *)reg = *(const uint16_t *)s;
-
#endif
-
} else {
-
size = 1;
-
#if defined(RTE_ARCH_X86)
-
outb_p(*s, reg);
-
#else
-
*(volatile uint8_t *)reg = *s;
-
#endif
-
}
-
}
-
}
如果是modern设备,其notify统一如下,采用内存映射直接访问地址的方式,也不会存在系统调用开销。
-
static void
-
modern_notify_queue(struct virtio_hw *hw __rte_unused, struct virtqueue *vq)
-
{
-
rte_write16(vq->vq_queue_index, vq->notify_addr);
-
}
当前我们如果在vm内部使用DPDK,又要使用vfio,为了避免notify引入的系统开销,可以关闭event_idx feature,这样就不需要前端去notify后端了。
阅读(30392) | 评论(0) | 转发(0) |