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