DPDK的iova地址模式

本文参考的代码版本为DPDK20.11。

DPDK的内存管理模型不仅包括了基本的malloc free机制,还有针对网卡设备性能提升层面设计的[rte_mempool rte_muf]机制。rte_mempool和rte_mbuf主要是服务于设备dma收发数据的场景,rte_mempool是申请了整个内存池,真正使用的时候从这个内存池获取小块的地址空间就是rte_mbuf。

作为设备dma访问的空间,在rte_mbuf的结构体中不仅需要用供CPU使用的地址,还需要有pcie设备使用的地址,也就是iova。所以在相关结构体定义中都会有专门的iova地址定义,在rte_mbuf中是buf_iova字段。

struct rte_mbuf {
	RTE_MARKER cacheline0;

	void *buf_addr;           /**< Virtual address of segment buffer. */
	/**
	 * Physical address of segment buffer.
	 * Force alignment to 8-bytes, so as to ensure we have the exact
	 * same mbuf cacheline0 layout for 32-bit and 64-bit. This makes
	 * working on vector drivers easier.
	 */
	rte_iova_t buf_iova __rte_aligned(sizeof(rte_iova_t));

.....
}

如果对IOMMU有了解的话可以知道,没有开启IOMMU的情况下,CPU使用的是虚拟地址,设备访问内存使用的是物理地址。如果系统开启IOMMU,CPU使用基于MMU映射的VA,设备也可以使用基于IOMMU映射的IOVA。

那么buf_iova是什么样的地址?dpdk作为用户态驱动又是怎么获取的物理地址?首先来看一下iova的地址模式(RTE_IOVA_PA| RTE_IOVA_VA)。

一、iova地址模式

dpdk的初始化接口是rte_eal_init(),大部分的初始化都可以从这个函数入手。dpdk作为一个so库,常用的场景是被上层应用OVS调用,dpdk库的初始化参数是由OVS调用rte_eal_init()接口传入的。其中一个参数是--iova_mode,就是配置iova地址模式的。

--iova_mode=pa|va,用于显式指定iova是使用物理地址还是虚拟地址。

如果没有传入这个参数,默认值是RTE_IOVA_DC,表示没有定义过。那么dpdk会根据系统当前的配置(有没有开启iommu、有没有vfio)启动选择地址模式。

这部分代码实现在rte_eal_init接口中,

	/* if no EAL option "--iova-mode=<pa|va>", use bus IOVA scheme */
    //传入的--iova_mode参数parse到了internal_conf->iova_mode中,没有传入记为RTE_IOVA_DC
	if (internal_conf->iova_mode == RTE_IOVA_DC) {
        /*
        省略掉iova_mode的赋值过程,放到下面展开,
        在没有配置--iova_mode的时候,设置iova_mode.
        */
		rte_eal_get_configuration()->iova_mode = iova_mode;
	} else {
        //在传入--iova_mode参数的时候,直接使用传入的参数
		rte_eal_get_configuration()->iova_mode =
			internal_conf->iova_mode;
	}

在没有显式指定--iova_mode参数的时候,dpdk是怎么probe各项配置然后设置iova地址模式的呢。需要看上面代码段中省略的部分,iova_mode变量如何赋值。

1)rte_buf_get_iommu_class()

这个函数遍历了rte_bus_list里的所有总线单元(bus),通过bus->get_iommu_class()可获知bus的iova模式。

最后,如果全都设置为PA,则选择PA模式;如果全部设置为VA,则选择VA模式。

如果同时存在PA和VA,说明无法确定选择PA模式还是VA模式,返回RTE_IOVA_DC。由后面的代码继续探测。

dpdk下面主要就是pci的buf,看了一下pci_bus的get_iommu_class()接口实现,是检测了系统有没有开启intel-iommu,如果没有开启就设置为PA模式。

如果开启了intel-iommu,就检测每个设备加载的驱动,如果所有设备都是用VFIO驱动(且没有开启unsafe_noiommu_mode模式),则设置为VA模式。如果所有设备都使用UIO驱动,则设置为PA模式。如果有设备使用VFIO有设备使用UIO,设置为DC模式。由后面继续确定。

2)如果无法获取phys_addrs,选择VA模式。(如何获取物理地址,又是什么情况下获取不到phys_addr?后面讨论)

3)如果加载了rte_kni驱动,选择PA模式。(这个驱动我们没有用到,没有关注)

4)如果开启了IOMMU,选择VA模式。

5)上述的if()判断都不满足,选择PA模式。

最后添加一个判断,在4.10以前的内核上,如果可以获取物理地址 而且 加载了rte_kni驱动强制配置为PA模式。

/* autodetect the IOVA mapping mode */
enum rte_iova_mode iova_mode = rte_bus_get_iommu_class();

if (iova_mode == RTE_IOVA_DC) {
	RTE_LOG(DEBUG, EAL, "Buses did not request a specific IOVA mode.\n");

	if (!phys_addrs) {
		/* if we have no access to physical addresses,
			* pick IOVA as VA mode.
			*/
		iova_mode = RTE_IOVA_VA;
		RTE_LOG(DEBUG, EAL, "Physical addresses are unavailable, selecting IOVA as VA mode.\n");
#if defined(RTE_LIB_KNI) && LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0)
	} else if (rte_eal_check_module("rte_kni") == 1) {
		iova_mode = RTE_IOVA_PA;
		RTE_LOG(DEBUG, EAL, "KNI is loaded, selecting IOVA as PA mode for better KNI performance.\n");
#endif
	} else if (is_iommu_enabled()) {
		/* we have an IOMMU, pick IOVA as VA mode */
		iova_mode = RTE_IOVA_VA;
		RTE_LOG(DEBUG, EAL, "IOMMU is available, selecting IOVA as VA mode.\n");
	} else {
		/* physical addresses available, and no IOMMU
			* found, so pick IOVA as PA.
			*/
		iova_mode = RTE_IOVA_PA;
		RTE_LOG(DEBUG, EAL, "IOMMU is not available, selecting IOVA as PA mode.\n");
	}
}
#if defined(RTE_LIB_KNI) && LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0)
/* Workaround for KNI which requires physical address to work
	* in kernels < 4.10
	*/
if (iova_mode == RTE_IOVA_VA &&
		rte_eal_check_module("rte_kni") == 1) {
	if (phys_addrs) {
		iova_mode = RTE_IOVA_PA;
		RTE_LOG(WARNING, EAL, "Forcing IOVA as 'PA' because KNI module is loaded\n");
	} else {
		RTE_LOG(DEBUG, EAL, "KNI can not work since physical addresses are unavailable\n");
	}
}
#endif

二、iova地址初始化

上面说明了dpdk对于iova地址模式的选取,那么iova的地址又是什么时候初始化到驱动实际使用的rte_mbuf中的呢。首先要了解rte_mempool和rte_mbuf的含义。ovs会调用dpdk的接口创建一个rte_mempool,这是一个内存池,rte_mbuf都是从rte_mempool中申请的。

事实上,针对专门的应用场景(MTU),rte_mempool中的每个rte_mbuf都是固定长度的,所以rte_mempool在创建时整个内存空间的拓扑就已经确定了。在创建时就初始化好了里面的rte_mbuf空间,也会对每个rte_mbuf进行初始化,包括iova、地址等。

每个rte_mbuf的初始化是通过rte_pktmbuf_init()接口实现的,如下。所以看一下rte_mempool_virt2iova的实现,是从当前的rte_mbuf地址获取到了rte_mempool_objhdr结构体(说明内存中,在rte_mbuf的内存空间之前,是一个struct rte_mempool_objhdr结构,那么搜索一下hdr->iova是在哪里赋值的。

mbuf_size = sizeof(struct rte_mbuf) + priv_size;

m->buf_iova = rte_mempool_virt2iova(m) + mbuf_size;  

static inline rte_iova_t
rte_mempool_virt2iova(const void *elt)
{
	const struct rte_mempool_objhdr *hdr;
	hdr = (const struct rte_mempool_objhdr *)RTE_PTR_SUB(elt,
		sizeof(*hdr));
	return hdr->iova;
}
static void
mempool_add_elem(struct rte_mempool *mp, __rte_unused void *opaque,
		 void *obj, rte_iova_t iova)
{
	struct rte_mempool_objhdr *hdr;
	struct rte_mempool_objtlr *tlr __rte_unused;

	/* set mempool ptr in header */
	hdr = RTE_PTR_SUB(obj, sizeof(*hdr));
	hdr->mp = mp;
	hdr->iova = iova;
	STAILQ_INSERT_TAIL(&mp->elt_list, hdr, next);
	mp->populated_size++;

}

int
rte_mempool_populate_iova(struct rte_mempool *mp, char *vaddr,
	rte_iova_t iova, size_t len, rte_mempool_memchunk_free_cb_t *free_cb,
	void *opaque)
{
	i = rte_mempool_ops_populate(mp, mp->size - mp->populated_size,
		(char *)vaddr + off,
		(iova == RTE_BAD_IOVA) ? RTE_BAD_IOVA : (iova + off),
		len - off, mempool_add_elem, NULL);
}

最终看到,是在rte_create_mempool()这个最上层的函数里,获取了iova层层调用到了后面的数据。而iova又是根据memzone的iova获得的。所以对于采用物理地址的方式,memzone的iova在memzone申请的时候已经标注好了。

可以看到,如果rte_eal_iova_mode()是RTE_IOVA_VA,那么直接返回虚拟地址vaddr。否则会通过rte_mem_virt2memseg()函数获取到最原始的内存单元,就是rte_memseg,然后根据ms->iova计算出iova返回。

	mz->iova = rte_malloc_virt2iova(mz_addr);

rte_iova_t
rte_malloc_virt2iova(const void *addr)
{
	const struct rte_memseg *ms;
	struct malloc_elem *elem = malloc_elem_from_data(addr);

	if (elem == NULL)
		return RTE_BAD_IOVA;

	if (!elem->msl->external && rte_eal_iova_mode() == RTE_IOVA_VA)
		return (uintptr_t) addr;

	ms = rte_mem_virt2memseg(addr, elem->msl);
	if (ms == NULL)
		return RTE_BAD_IOVA;

	if (ms->iova == RTE_BAD_IOVA)
		return RTE_BAD_IOVA;

	return ms->iova + RTE_PTR_DIFF(addr, ms->addr);
}

那么ms->iova是在哪里赋值的,搜索ms->iova =,可以看到在一个alloc_seg的函数中,申请了rte_memseg结构,并对其进行初始化。在这里找到了赋值iova的最源头的接口rte_mem_virt2iova,这个函数的实现,如果是RTE_IOVA_VA模式,则直接使用virtaddr作为iova;如果是RTE_IOVA_PA模式,则返回物理地址。

rte_mem_virt2phy这个函数的实现很长,就不贴代码了。DPDK作为一个用户态的应用程序,不能像内核可以直接获得内存、设备的所有信息,DPDK利用了很多系统目录/sys  /proc  /var等,甚至可以获得物理地址(之前不了解还有这种操作)。此处就是通过/proc/self/pagemap这个文件,读取到自己进程下的内存映射信息,根据虚拟地址获取到真实的物理地址。

iova = rte_mem_virt2iova(addr);

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);
}

三、使用场景(virtio_driver为例)

说明一下rte_mbuf的使用场景,尤其是buf_iova变量。

因为virtio-net是收发方向各需要一个队列,所以rx和tx是分开的。对于rx方向来说,前端设备(dpdk里的驱动看做是前端设备使用的设备驱动)需要事先配置好available ring的descriptor以及descriptor对应的buffer,后端接收数据时从available ring获取消息放到used ring。

在eth_dev_ops的dev_start接口中,调用virtio_dev_rx_queue_setup_finish()接口做了上面的事情,所以会涉及到dma buffer的申请和使用。

m = rte_mbuf_raw_alloc(rxvq->mpool);

error = virtqueue_enqueue_recv_refill(vq, &m, 1);

virtqueue_enqueue_recv_refill(struct virtqueue *vq, struct rte_mbuf **cookie,
				uint16_t num)
{
    /*...省略...*/
    //start_dp就是 descriptor,addr就是给DMA使用的地址
	start_dp[idx].addr =                            
		VIRTIO_MBUF_ADDR(cookie[i], vq) +
		RTE_PKTMBUF_HEADROOM - hw->vtnet_hdr_size;
	start_dp[idx].len =
		cookie[i]->buf_len - RTE_PKTMBUF_HEADROOM +
		hw->vtnet_hdr_size;
	start_dp[idx].flags = VRING_DESC_F_WRITE;
	vq->vq_desc_head_idx = start_dp[idx].next;
	vq_update_avail_ring(vq, idx);
    /*...省略...*/
}

#define VIRTIO_MBUF_ADDR(mb, vq) ((mb)->buf_iova)  //地址使用的是rte_mbuf中记录的buf_iova

上一篇:hustoj 服务器配置


下一篇:python基础第二十七章:mixin设计模式