从内核模块暴露IB内存接口: ib_umem_get()/ib_umem_release() ,可让低级驱动程序控制何时调用 ib_umem_get() 来pin和 DMA 映射用户空间,该方案优于以前在调用 ib_uverbs_reg_mr 中的 reg_user_mr 前后的处理逻辑 。 还将这些函数移至 ib_core 模块而不是 ib_uverbs 中,以便使用它们的驱动程序模块不依赖于 ib_uverbs。 这具有许多优点:
INFINIBAND_USER_MEM, 默认开启
struct ib_umem {
struct ib_device *ibdev;
struct mm_struct *owning_mm;
u64 iova;
size_t length;
unsigned long address;
u32 writable : 1;
u32 is_odp : 1;
u32 is_dmabuf : 1;
struct sg_append_table sgt_append;
};
dma_buf 是内核中一个独立的子系统,提供了一个让不同设备、子系统之间进行共享缓存的统一框架,这里说的缓存通常是指通过DMA方式访问的和硬件交互的内存
struct ib_umem_dmabuf {
struct ib_umem umem;
struct dma_buf_attachment *attach;
struct sg_table *sgt;
struct scatterlist *first_sg;
struct scatterlist *last_sg;
unsigned long first_sg_offset;
unsigned long last_sg_trim;
void *private;
u8 pinned : 1;
};
...
region = ib_umem_get(pd->device, start, len, access) // PIN用户态内存(从start开始), 并映射设备DMA内存
npages = ib_umem_num_pages(umem) // 获取内存页数
iwmr->page_size = ib_umem_find_best_pgsz(region, pgsz_bitmap, virt) // 获取页大小
...
以E810注册内存(ibv_reg_mr)为例, 其调用栈如下:
ibv_reg_mr, Register a user memory region,
drivers/infiniband/hw/irdma/verbs.c
IB_USER_VERBS_CMD_REG_MR
ib_uverbs_reg_mr ->
uobj = uobj_get_write(UVERBS_OBJECT_MR, cmd.mr_handle, attrs)
ib_check_mr_access -> enum ib_access_flags
uobj_get_obj_read(pd, UVERBS_OBJECT_PD, cmd.pd_handle,
mr = pd->device->ops.reg_user_mr(pd, cmd.start, cmd.length, cmd.hca_va, cmd.access_flags, &attrs->driver_udata) -> .reg_user_mr = irdma_reg_user_mr <- DECLARE_UVERBS_WRITE
...
region = ib_umem_get -> pin住以及通过DMA映射的用户空间内存, IB/uverbs:将 ib_umem_get()/ib_umem_release() 导出到模块,导出 ib_umem_get()/ib_umem_release() 并让低级驱动程序控制何时调用 ib_umem_get() 来pin和 DMA 映射用户空间,而不是总是调用它 在调用低级驱动程序的 reg_user_mr 方法之前在 ib_uverbs_reg_mr() 中。 还将这些函数移至 ib_core 模块而不是 ib_uverbs 中,以便使用它们的驱动程序模块不依赖于 ib_uverbs。 这具有许多优点: - 从使通用代码成为可以根据特定设备的详细信息指示由设备特定代码使用或覆盖的库的角度来看,这是更好的设计。 - 不需要固定用户空间内存区域的驱动程序不需要承受调用 ib_mem_get() 的性能损失。 例如,虽然我没有尝试在此补丁中实现它,但 ipath 驱动程序应该能够避免固定内存并仅使用 copy_{to,from}_user() 来访问用户空间内存区域。 - 需要特殊映射处理的缓冲区可以由低级驱动程序识别。 例如,通过使用额外标志映射 CQ 缓冲区,可以解决用户空间中 mthca CQ 的一些特定于 Altix 的内存排序问题。 - 需要为内存区域以外的内容固定和 DMA 映射用户空间内存的驱动程序可以直接使用 ib_umem_get(),而不是使用其 reg_phys_mr 方法的额外参数进行黑客攻击。 例如,待合并的 mlx4 驱动程序需要引脚和 DMA 映射 QP 和 CQ 缓冲区,但不需要为这些缓冲区创建内存密钥。 因此,最干净的解决方案是 mlx4 在 create_qp 和 create_cq 方法中调用 ib_umem_get() -> 调用 ib_umem_get 来完成内存页面pin操作 + 将pin住的内存页面组织为DMA SG List
can_do_mlock -> [PATCH] IB uverbs:内存固定实现(pin),添加对固定用户空间内存区域并返回该区域中的页面列表的支持。 这包括根据 vm_locked 跟踪固定内存并防止非特权用户超过 RLIMIT_MEMLOCK
mmgrab(mm) -> 抓住内存(引用+1)
__get_free_page -> get continue pa(return va) -> https://blog.csdn.net/do2jiang/article/details/5450705
npages = ib_umem_num_pages(umem)
ib_umem_num_dma_blocks(umem, PAGE_SIZE) -> RDMA/umem:将 ib_umem_num_pages() 拆分为 ib_umem_num_dma_blocks(),ib_umem_num_pages() 只能由直接在 CPU 页面中使用 SGL 的事物使用。 构建 DMA 列表的驱动程序应使用新的 ib_num_dma_blocks(),它返回 rdma_umem_for_each_block() 将返回的块数。 要使 DMA 驱动程序通用,需要不同的实现。 仅当请求的页面大小为 < PAGE_SIZE 和/或 IOVA == umem->address 时,基于 umem->address 计算 DMA 块计数才有效。 相反,DMA 页数应在 IOVA 地址空间中计算,而不是 umem->address。 因此,IOVA 必须存储在 umem 内,以便可以用于这些计算。 现在默认将其设置为 umem->address 并在调用 ib_umem_find_best_pgsz() 时修复它。 这允许驱动程序安全地转换为 ib_umem_num_dma_blocks()
ALIGN ALIGN_DOWN / pgsz
cond_resched -> RDMA/umem:在 ib_umem_get() 中添加一个调度点,映射小至 64GB 可能需要 10 秒以上,触发 CONFIG_PREEMPT_NONE=y 的内核问题。 ib_umem_get() 已经在 x86_64 上以 2MB 为单位分割工作,在持久循环中添加 cond_resched() 足以解决问题。 请注意,sg_alloc_table() 仍然可以使用超过 100 毫秒,这也是有问题的。 这可能稍后在 ib_umem_add_sg_table() 中解决,按需在 sql 中添加新块. 在一些比较耗时的处理中如文件系统和内存回收的一些路径会调用cond_resched, 用cond_resched来进行检查是否具备调度时机, 对于非抢占式内核来说,在内核的很多地方,特别是文件系统操作和内存管理相关的一些耗时路径中,都已经被内核开发者识别出来,并使用cond_resched来减小延迟, cond_resched() 函数,它的功能是主动放权,等待下一次的调度运行, 参考: https://www.zhihu.com/question/35004859
pin_user_pages_fast -> 与 get_user_pages_fast() 几乎相同,只是设置了 FOLL_PIN。 有关函数参数的文档,请参阅 get_user_pages_fast(),因为此处的参数是相同的。 FOLL_PIN 意味着必须通过 unpin_user_page() 释放页面。 请参阅 Documentation/core-api/pin_user_pages.rst 了解更多详细信息。 请注意,如果返回的页面中有一个 zero_page,则其中不会有pin,并且 unpin_user_page() 不会从中删除pin -> mm/gup:从内部 GUP 函数中删除 vmas 数组,现在我们已经消除了所有使用 vmas 参数的 GUP API 的调用者,完全消除了它。 这消除了一类错误,其中 vmas 的保留时间可能比 mmap_lock 的时间长,因此我们不必担心在此操作期间锁定被删除,留下悬空指针。 这简化了 GUP API 并使其用途更加清晰 - 应用跟随标志,如果固定,则返回页面数组
is_valid_gup_args
WARN_ON_ONCE()会将调试信息输出到dmesg,类似于BUG_ON()打印的内容。与BUG_ON()的区别在于,WARN_ON_ONCE()仅会在第一次触发时打印调试信息
internal_get_user_pages_fast -> 获取用户空间页面: https://sanli-b.com.cn/posts/35092.html -> mm/gup:跟踪 FOLL_PIN 页面 添加对通过 FOLL_PIN 固定的页面的跟踪。 此跟踪是通过 page->_refcount 的重载来实现的:通过将 GUP_PIN_COUNTING_BIAS (1024) 添加到 refcount 来添加引脚。 这提供了固定的模糊指示,并且可能会出现误报(但这没关系)。 请参阅现有的 Documentation/core-api/pin_user_pages.rst 了解详细信息。 正如 pin_user_pages.rst 中提到的,有效设置 FOLL_PIN(通常通过 pin_user_pages*())的调用者需要最终通过 unpin_user_page() 释放此类页面。 另请注意 pin_user_pages.rst 中“TODO:适用于 1GB 及更大的大页面”部分中讨论的限制。 (该限制将在后续补丁中删除。)FOLL_PIN 标志的效果与 FOLL_GET 的效果类似,并且可以被视为“用于 DIO 和/或 RDMA 使用的 FOLL_GET”。 通过 FOLL_PIN 固定的页面可通过新函数调用进行识别: bool page_maybe_dma_pinned(struct page *page); 遇到这样的页面该怎么办,就留给以后的补丁了。 在[1]、[2]、[3]和[4]中对此进行了讨论。 这也会将 follow_page_mask() 中的 BUG_ON() 更改为 WARN_ON()
mm_set_has_pinned_flag
set_bit(MMF_HAS_PINNED, mm_flags) -> flag -> MMF_HAS_PINNED:该mm是否已固定任何页面。 当它变得稳定时,它可以在将来被 mm.pinned_vm 取代,或者自己成长为一个计数器。 我们现在对此非常积极:即使稍后取消固定的页面,我们仍然会在该 mm 的生命周期中保留此位设置,只是为了简单起见
untagged_addr -> 调用 untagged_addr() 宏直接去除指针标记
check_add_overflow
lockless_pages_from_mm -> mm/gup:重组 internal_get_user_pages_fast(),补丁系列“在gup_fast和copy_page_range()之间添加seqcount”,v4。 正如 Linus 所讨论和建议的,使用 seqcount 来结束 gup_fast 和 copy_page_range() 之间的小竞争。 Ahmed 确认 raw_write_seqcount_begin() 是在这种情况下使用的正确 API,并且它不会触发任何 lockdeps。 我能够使用两个线程对其进行测试,一个线程分叉,另一个使用 ibv_reg_mr() 快速触发 GUP。 将 copy_page_range() 修改为睡眠状态使窗口足够大,可以可靠地命中来测试逻辑。 此补丁(共 2 个):本系列中的下一个补丁使无锁流程变得更加复杂,因此将整个块移至新函数中并删除一定程度的缩进。 整理一些废话: - addr 始终与 start 相同,因此使用 start - 使用现代的 check_add_overflow() 进行计算 end = start + len - nr_pinned/pages << PAGE_SHIFT 需要 LHS 为 unsigned long 以避免移位溢出 ,将变量设置为 unsigned long 以避免在两个地方进行编码转换。 nr_pinned 缺少其演员 - ret 和 nr_pinned 的处理可以稍微简化一点 没有功能变化 -> https://gwzlchn.github.io/202208/rdma-stack-02/
IS_ENABLED
gup_fast_permitted
raw_read_seqcount
gup_pgd_range
read_seqcount_retry
unpin_user_pages_lockless
__gup_longterm_locked
sg_alloc_append_table_from_pages
ret = ib_dma_map_sgtable_attrs(device, &umem->sgt_append.sgt, DMA_BIDIRECTIONAL, dma_attr) -> Map a scatter/gather table to DMA addresses, 不清楚传输方向
if (ib_uses_virt_dma(dev))
nents = ib_dma_virt_map_sg(dev, sgt->sgl, sgt->orig_nents)
for_each_sg(sg, s, nents, i)
sg_dma_address(s) = (uintptr_t)sg_virt(s);
sg_dma_len(s) = s->length;
else
dma_map_sgtable(dev->dma_device, sgt, direction, dma_attrs)
nents = __dma_map_sg_attrs(dev, sgt->sgl, sgt->orig_nents, dir, attrs)
dma_direct_map_sg
is_pci_p2pdma_page -> MEMORY_DEVICE_PCI_P2PDMA
pci_p2pdma_map_segment
sg->dma_address = dma_direct_map_page(dev, sg_page(sg), sg->offset, sg->length, dir, attrs)
swiotlb_map(dev, phys, size, dir, attrs)
ib_copy_from_udata(&req, udata, min(sizeof(req), udata->inlen)
irdma_alloc_iwmr
iwmr->page_size = ib_umem_find_best_pgsz(region, pgsz_bitmap, virt)
umem->iova = va = virt
mask = pgsz_bitmap & GENMASK(BITS_PER_LONG - 1, bits_per((umem->length - 1 + virt) ^ virt))
for_each_sgtable_dma_sg(&umem->sgt_append.sgt, sg, i)
mask |= (sg_dma_address(sg) + pgoff) ^ va
va += sg_dma_len(sg) - pgoff
iwmr->page_cnt = ib_umem_num_dma_blocks(region, iwmr->page_size)
return (size_t)((ALIGN(umem->iova + umem->length, pgsz) - ALIGN_DOWN(umem->iova, pgsz))) / pgsz
case IRDMA_MEMREG_TYPE_QP
irdma_reg_user_mr_type_qp(req, udata, iwmr)
irdma_handle_q_mem(iwdev, &req, iwpbl, lvl)
irdma_setup_pbles -> 将用户页拷贝到PBLE中
case IRDMA_MEMREG_TYPE_QP
irdma_check_mem_contiguous
rdma_udata_to_drv_context
list_add_tail(&iwpbl->list, &ucontext->qp_reg_mem_list)
case IRDMA_MEMREG_TYPE_MEM
irdma_reg_user_mr_type_mem
lvl = iwmr->page_cnt != 1 ? PBLE_LEVEL_1 | PBLE_LEVEL_2 : PBLE_LEVEL_0
irdma_setup_pbles -> 使用位掩码重构 PBLE 函数来表示所需的 PBLE 级别,而使用 2 个参数 use_pble 和 lvl_one_only 使代码变得混乱
HW 使用主机内存作为许多协议上下文对象和队列状态跟踪的后备存储。 主机内存缓存 (HMC) 是负责管理存储在主机内存中的这些对象的组件。 添加函数和数据结构来管理 HMC 为各种对象使用的支持页面的分配, 本文主要分析inux内核intel/hns3/mlx5等RDMA驱动上下文内存管理机制优缺点: https://zhuanlan.zhihu.com/p/610503666, 实现物理缓冲区列表条目 (PBLE) 资源管理器来管理 PBLE HMC 资源对象池,
irdma_get_pble -> if (lvl)
get_lvl1_lvl2_pble
status = get_lvl1_pble(pble_rsrc, palloc)
irdma_prm_get_pbles(&pble_rsrc->pinfo, &lvl1->chunkinfo, palloc->total_cnt << 3, &lvl1->addr, &fpm_addr)
bits_needed = DIV_ROUND_UP_ULL(mem_size, BIT_ULL(pprm->pble_shift))
pchunk = (struct irdma_chunk *)chunk_entry
bit_idx = bitmap_find_next_zero_area(pchunk->bitmapbuf, pchunk->sizeofbitmap, 0, bits_needed, 0)
offset = bit_idx << pprm->pble_shift
bitmap_set(pchunk->bitmapbuf, bit_idx, bits_needed)
pprm->free_pble_cnt -= chunkinfo->bits_used << (pprm->pble_shift - 3)
palloc->level = PBLE_LEVEL_1
lvl1->idx = fpm_to_idx(pble_rsrc, fpm_addr)
get_lvl2_pble
...
add_pble_prm -> Host Memory Cache (HMC) 主机内存缓存, prm 页资源管理
get_sd_pd_idx(pble_rsrc, idx)
irdma_get_type
add_sd_direct
irdma_add_sd_table_entry
dma_alloc_coherent
memcpy(&sd_entry->u.pd_table.pd_page_addr, &dma_mem,
add_bp_pages -> 为段描述添加后端内存页 -> add backing pages for sd (BP)
irdma_pble_get_paged_mem
irdma_map_vm_page_list
vmalloc_to_page -> 找到由vmalloc( )所分配的内存的虚拟地址所映射的物理页,并返回该页的指针描述符
dma_map_page -> 将一页物理内存进行映射
irdma_add_sd_table_entry
irdma_add_pd_table_entry
dma_alloc_coherent
memcpy(&pd_entry->bp.addr, page, sizeof(pd_entry->bp.addr))
memcpy(pd_addr, &page_desc, sizeof(*pd_addr))
irdma_invalidate_pf_hmc_pd -> 使 PF 硬件中的 pd 缓存无效
writel(val, dev->hw_regs[IRDMA_PFHMC_PDINV])
irdma_prm_add_pble_mem
bitmap_zalloc
irdma_hmc_sd_one
irdma_set_sd_entry
or irdma_clr_sd_entry
dev->cqp->process_cqp_sds(dev, &sdinfo) -> irdma_update_sds_noccq
cqp_sds_wqe_fill
irdma_sc_cqp_get_next_send_wqe_idx
IRDMA_ATOMIC_RING_MOVE_HEAD(cqp->sq_ring, *wqe_idx, ret_code)
wqe = cqp->sq_base[*wqe_idx].elem -> 从DMA的VA基址依次往后填充
IRDMA_CQP_INIT_WQE(wqe) -> #define IRDMA_CQP_INIT_WQE(wqe) memset(wqe, 0, 64) -> WQE大小为64字节
switch (wqe_entries)
case 3:
...
irdma_get_cqp_reg_info
irdma_sc_cqp_post_sq(cqp)
irdma_cqp_poll_registers
list_add(&chunk->list, &pble_rsrc->pinfo.clist)
get_lvl1_lvl2_pble
irdma_copy_user_pgaddrs -> 将用户页面地址复制到本地 pble 的操作系统
sg_page -> 获取scatterlist所对应的page指针
rdma_umem_for_each_dma_block -> 迭代 umem 的连续 DMA 块
*pbl = rdma_block_iter_dma_address(&biter) -> 获取块迭代器持有的当前块的对齐 dma 地址
biter->__dma_addr & ~(BIT_ULL(biter->__pg_bit) - 1) -> aligned dma address
pbl = irdma_next_pbl_addr(pbl, &pinfo, &idx)
if (lvl)
irdma_check_mr_contiguous
irdma_check_mem_contiguous
if ((*arr + (pg_size * pg_idx)) != arr[pg_idx])
irdma_create_stag -> 随机stag
get_random_bytes
irdma_alloc_rsrc -> RDMA/irdma:注册辅助驱动程序并实现私有通道 OP,注册可以从 Intel PCI netdev 驱动程序 i40e 和ice 连接到辅助 RDMA 设备的辅助驱动程序。 实现私有通道操作,并注册网络通知程序
irdma_hwreg_mr(iwdev, iwmr, access) -> 发送cqp命令进行内存注册
irdma_alloc_and_get_cqp_request
irdma_get_mr_access
cqp_info->cqp_cmd = IRDMA_OP_MR_REG_NON_SHARED -> irdma_sc_mr_reg_non_shared
irdma_sc_cqp_get_next_send_wqe
...
set_64bit_val(wqe, 32, info->reg_addr_pa)
irdma_sc_cqp_post_sq
stag_info->reg_addr_pa = iwmr->pgaddrmem[0]
irdma_handle_cqp_op
irdma_put_cqp_request
rdma_restrack_new(&new_mr->res, RDMA_RESTRACK_MR)
rdma_restrack_set_name(&new_mr->res, NULL)
rdma_restrack_add(&new_mr->res)
内存类型:
enum memory_type {
/* 0 is reserved to catch uninitialized type fields */
MEMORY_DEVICE_PRIVATE = 1,
MEMORY_DEVICE_COHERENT,
MEMORY_DEVICE_FS_DAX,
MEMORY_DEVICE_GENERIC,
MEMORY_DEVICE_PCI_P2PDMA,
};
将 ZONE_DEVICE 内存专门化为多种类型,每种类型都有不同的用途。
MEMORY_DEVICE_PRIVATE:CPU 无法直接寻址的设备内存:CPU 既不能读取也不能写入私有内存。在这种情况下,我们仍然有支持设备内存的结构页。这样做可以简化实现,但重要的是要记住,在某些时候,必须将结构页视为不透明对象,而不是“普通”结构页。有关不可寻址内存的更完整讨论可以在 include/linux/hmm.h 和 Documentation/mm/hmm.rst 中找到。
MEMORY_DEVICE_COHERENT:从设备和 CPU 的角度来看缓存一致的设备内存。这用于具有高级系统总线(如 CAPI 或 CXL)的平台上。驱动程序可以使用 ZONE_DEVICE 和该内存类型热插拔设备内存。进程的任何页面都可以迁移到此类内存。但是,不应允许任何人固定此类内存,以便始终可以将其逐出。
MEMORY_DEVICE_FS_DAX:具有与系统 RAM 类似的访问语义(即 DMA 一致)并支持页面固定的主机内存。为了支持协调页面固定与其他操作,MEMORY_DEVICE_FS_DAX 会在页面取消固定并变为空闲时安排唤醒事件。此唤醒用于协调物理地址空间管理(例如:fs 截断/打孔)与固定页面(例如:设备 dma)。
MEMORY_DEVICE_GENERIC:具有与系统 RAM 类似的访问语义(即 DMA 一致)并支持页面固定的主机内存。例如,这由使用字符设备公开内存的 DAX 设备使用。MEMORY_DEVICE_PCI_P2PDMA:驻留在 PCI BAR 中的设备内存,旨在用于点对点事务
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。