DPDK系列之二十一DPDK的IOVA分析

一、IOVA

IOVA,IO虚拟地址。在DPDK中上层的EAL(环境抽象层)负责管理的一部分功能中就包含将硬件设备的寄存器映射到内存中,以供其它驱动程序来应用。也就是说,用户态的进程可以直接使用IO地址并执行IO操作。在前面已经提到过,这些地址可以分为物理地址(PA)和IO虚拟地址即IOVA。上层并不对二者区分即对应用层来说,对二者是不敏感的。用户态进程中看到的都是IOVA地址。
PA的IOVA模式的优势在于在内核空间的应用中并且对于所有硬件都可以使用,它的缺点就是如果对内存操作对权限有要求时,就麻烦了。同样如果内存碎片较多的情况下,可能无法分配内存,会导致整个DPDK初始化的失败。为了解决这些问题,一般来说,就会使用更大分页,比如1G并且在启动时就引导系统使用大页。可明眼人一眼就会看出,这只是一种治标的办法,正所谓头疼医了头,脚疼医了脚。
而VA的IOVA模式 ,就需要一个IOMMU来进行地址的转换和分析。它等于额外又抽象了一层,一般做过设计的都明白,抽象大多意味着效率的降低(零成本抽象除外)。它的优势在于,内存的处理不完全受限于真实的物理内存的限制,也不需要一些特殊的权限。特别在云环境下(虚拟环境下IOMMU更合适),应用DPDK就有了更广泛的应用方式。当然,它的缺点也不少:硬件不一定支持IOMMU或者干脆平台就没有这个,软件不支持或者IOMMU受限等等。
在正常的情况下,DPDK默认是选择使用PA的IOVA模式,这样做一个是安全另外一个是适用性广,但如果条件允许还是建议使用VA的IOVA模式。在DPDK17.11更高以上版本,可以使用命令:
–iova-mode
来自动选择合适的模式。

二、IOVA的应用

既然是IO操作,理论上讲,这块就和传统的驱动的功能类似。DPDK中,对硬件的中断映射和寄存器映射都需要内核的协助。它需要绑定到PCI,这个玩儿过计算机的人基本都明白。而这个PCI一个特点是并未被绑定到特定的一些设备集中。写过硬件驱动都知道,一般在驱动中会写死一些设备的类型ID。所以理论上讲它可以和任意此类型的设备通用。但开发者往往都知道,这里面的水有多深。
DPDK中,在用户空间(UIO)中,由于其本身的限制(其使用的为igb_uio),只能使用PA的IOVA模式,这也就限定了UIO中的应用;而在高版本中推荐使用的VFIO内核驱动中(Linux3.6),它特地与IOMMU进行了开发,所以在VFIO中可以选择前面的两种模式来进行处理(但PA模式下的权限问题仍然存在)。等到了内核的更高版本(>=4.5),在设置了enable_unsafe_noiommu_mode选项后,可以在没有IOMMU的情况下使用VFIO。这个就更有优势了。
当然,在DPDK中,PMD(软件轮询模式驱动程序)及一些相关软件并不需要PCI来操作驱动,它们通过标准的内核基础架构来操作硬件,这样就可以忽略到上面提到的IOVA,换句话说,这个就无所谓了。

三、数据结构和源码

先看下相关的地址定义:

/** Physical address */
typedef uint64_t phys_addr_t;
#define RTE_BAD_PHYS_ADDR ((phys_addr_t)-1)

/**
 * IO virtual address type.
 * When the physical addressing mode (IOVA as PA) is in use,
 * the translation from an IO virtual address (IOVA) to a physical address
 * is a direct mapping, i.e. the same value.
 * Otherwise, in virtual mode (IOVA as VA), an IOMMU may do the translation.
 */
typedef uint64_t rte_iova_t;
#define RTE_BAD_IOVA ((rte_iova_t)-1)

在前面分析包括在后面分析的数据结构中,都可以看这两个数据类型的应用。下面看一下相关的转换代码:

//\dpdk-stable-19.11.14\lib\librte_eal\linux\eal\eal_memory.c
rte_iova_t
rte_mem_virt2iova(const void *virtaddr)
{
	if (rte_eal_iova_mode() == RTE_IOVA_VA)
		return (uintptr_t)virtaddr;
	return rte_mem_virt2phy(virtaddr);
}

再看一下VFIO中:

struct user_mem_map {
	uint64_t addr;
	uint64_t iova;
	uint64_t len;
};
//\lib\librte_eal\linux\eal\eal_vfio.h
struct vfio_iommu_type {
	int type_id;
	const char *name;
	bool partial_unmap;
	vfio_dma_user_func_t dma_user_map_func;
	vfio_dma_func_t dma_map_func;
};

再看一下模式判断相关:

/* IOMMU types we support */
static const struct vfio_iommu_type iommu_types[] = {
	/* x86 IOMMU, otherwise known as type 1 */
	{
		.type_id = RTE_VFIO_TYPE1,
		.name = "Type 1",
		.partial_unmap = false,
		.dma_map_func = &vfio_type1_dma_map,
		.dma_user_map_func = &vfio_type1_dma_mem_map
	},
	/* ppc64 IOMMU, otherwise known as spapr */
	{
		.type_id = RTE_VFIO_SPAPR,
		.name = "sPAPR",
		.partial_unmap = true,
		.dma_map_func = &vfio_spapr_dma_map,
		.dma_user_map_func = &vfio_spapr_dma_mem_map
	},
	/* IOMMU-less mode */
	{
		.type_id = RTE_VFIO_NOIOMMU,
		.name = "No-IOMMU",
		.partial_unmap = true,
		.dma_map_func = &vfio_noiommu_dma_map,
		.dma_user_map_func = &vfio_noiommu_dma_mem_map
	},
};
//4\lib\librte_eal\common\eal_common_bus.c
/*
 * Get iommu class of devices on the bus.
 */
enum rte_iova_mode
rte_bus_get_iommu_class(void)
{
	enum rte_iova_mode mode = RTE_IOVA_DC;
	bool buses_want_va = false;
	bool buses_want_pa = false;
	struct rte_bus * bus;

	TAILQ_FOREACH(bus, &rte_bus_list, next) {
		enum rte_iova_mode bus_iova_mode;

		if (bus->get_iommu_class == NULL)
			continue;

		bus_iova_mode = bus->get_iommu_class();
		RTE_LOG(DEBUG, EAL, "Bus %s wants IOVA as '%s'\n",
			bus->name,
			bus_iova_mode == RTE_IOVA_DC ? "DC" :
			(bus_iova_mode == RTE_IOVA_PA ? "PA" : "VA"));
		if (bus_iova_mode == RTE_IOVA_PA)
			buses_want_pa = true;
		else if (bus_iova_mode == RTE_IOVA_VA)
			buses_want_va = true;
	}
	if (buses_want_va && !buses_want_pa) {
		mode = RTE_IOVA_VA;
	} else if (buses_want_pa && !buses_want_va) {
		mode = RTE_IOVA_PA;
	} else {
		mode = RTE_IOVA_DC;
		if (buses_want_va) {
			RTE_LOG(WARNING, EAL, "Some buses want 'VA' but forcing 'DC' because other buses want 'PA'.\n");
			RTE_LOG(WARNING, EAL, "Depending on the final decision by the EAL, not all buses may be able to initialize.\n");
		}
	}

	return mode;
}

再看下内核中对IOMMU的支持:

//\drivers\bus\pci\linux\pci.c
#if defined(RTE_ARCH_X86)
bool
pci_device_iommu_support_va(const struct rte_pci_device *dev)
{
#define VTD_CAP_MGAW_SHIFT	16
#define VTD_CAP_MGAW_MASK	(0x3fULL << VTD_CAP_MGAW_SHIFT)
	const struct rte_pci_addr *addr = &dev->addr;
	char filename[PATH_MAX];
	FILE *fp;
	uint64_t mgaw, vtd_cap_reg = 0;

	snprintf(filename, sizeof(filename),
		 "%s/" PCI_PRI_FMT "/iommu/intel-iommu/cap",
		 rte_pci_get_sysfs_path(), addr->domain, addr->bus, addr->devid,
		 addr->function);

	fp = fopen(filename, "r");
	if (fp == NULL) {
		/* We don't have an Intel IOMMU, assume VA supported */
		if (errno == ENOENT)
			return true;

		RTE_LOG(ERR, EAL, "%s(): can't open %s: %s\n",
			__func__, filename, strerror(errno));
		return false;
	}

	/* We have an Intel IOMMU */
	if (fscanf(fp, "%" PRIx64, &vtd_cap_reg) != 1) {
		RTE_LOG(ERR, EAL, "%s(): can't read %s\n", __func__, filename);
		fclose(fp);
		return false;
	}

	fclose(fp);

	mgaw = ((vtd_cap_reg & VTD_CAP_MGAW_MASK) >> VTD_CAP_MGAW_SHIFT) + 1;

	/*
	 * Assuming there is no limitation by now. We can not know at this point
	 * because the memory has not been initialized yet. Setting the dma mask
	 * will force a check once memory initialization is done. We can not do
	 * a fallback to IOVA PA now, but if the dma check fails, the error
	 * message should advice for using '--iova-mode pa' if IOVA VA is the
	 * current mode.
	 */
	rte_mem_set_dma_mask(mgaw);
	return true;
}
#elif defined(RTE_ARCH_PPC_64)
bool
pci_device_iommu_support_va(__rte_unused const struct rte_pci_device *dev)
{
	return false;
}
#else
bool
pci_device_iommu_support_va(__rte_unused const struct rte_pci_device *dev)
{
	return true;
}
#endif

enum rte_iova_mode
pci_device_iova_mode(const struct rte_pci_driver *pdrv,
		     const struct rte_pci_device *pdev)
{
	enum rte_iova_mode iova_mode = RTE_IOVA_DC;

	switch (pdev->kdrv) {
	case RTE_KDRV_VFIO: {
#ifdef VFIO_PRESENT
		static int is_vfio_noiommu_enabled = -1;

		if (is_vfio_noiommu_enabled == -1) {
			if (rte_vfio_noiommu_is_enabled() == 1)
				is_vfio_noiommu_enabled = 1;
			else
				is_vfio_noiommu_enabled = 0;
		}
		if (is_vfio_noiommu_enabled != 0)
			iova_mode = RTE_IOVA_PA;
		else if ((pdrv->drv_flags & RTE_PCI_DRV_NEED_IOVA_AS_VA) != 0)
			iova_mode = RTE_IOVA_VA;
#endif
		break;
	}

	case RTE_KDRV_IGB_UIO:
	case RTE_KDRV_UIO_GENERIC:
		iova_mode = RTE_IOVA_PA;
		break;

	default:
		if ((pdrv->drv_flags & RTE_PCI_DRV_NEED_IOVA_AS_VA) != 0)
			iova_mode = RTE_IOVA_VA;
		break;
	}
	return iova_mode;
}

其实IOVA和IOMMU这些东西就是一个地址的控制方式,IOMMU可以更好的为IOVA服务。反正在上层应用眼里,没有物理和虚拟的一说,它只关心操作的地址,至于地址最终怎么处理,不是人家关心的事儿。就和把钱存储到银行一样,你只管把钱送进去,至于银行把钱干啥了,不要关心。

四、分析

希望国内的程序员有机会有时间就向底层发展,这才是未来的方向。上层的应用,红火了十年,发现现在基本走到了一个十字路口。向左、向右,还是继续向前?仁者见仁。但无论怎么走,都脱离不在底层的支持,没有底座,再好的大厦也经不起风吹雨打。

猜你喜欢

转载自blog.csdn.net/fpcc/article/details/131348677