ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

DPDK的iova地址模式

2021-11-13 16:02:54  阅读:547  来源: 互联网

标签:rte RTE IOVA 地址 mode mempool iova DPDK


本文参考的代码版本为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

标签:rte,RTE,IOVA,地址,mode,mempool,iova,DPDK
来源: https://blog.csdn.net/leiyanjie8995/article/details/121227740

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有