AMD GPU内存管理(1):概览

参考内核代码:Linux-6.1/driver/gpu/drm/amd

HMM

待更新......

dumb buffer create/map

        在AMDGPU的Graphics业务中,用到了GEM(Graphics Execution Manager),它是用于内核内部管理图形缓冲区,用户空间进程可以通过GEM来创建、处理和销毁GPU中视频内容的内存对象。如下是AMD GPU注册得drm_driver回调

static const struct drm_driver amdgpu_kms_driver = {
    ......
    .dumb_create = amdgpu_mode_dumb_create,
    .dumb_map_offset = amdgpu_mode_dumb_mmap,
    ......
};

        其中:DRM_IOCTL_MODE_CREATE_DUMB中调用amdgpu_mode_dumb_create来分配dumb buffer。这里需要注意amdgpu_mode_dumb_create最终仍然是调用了TTM来分配内存。

int drm_mode_create_dumb(struct drm_device *dev,
			 struct drm_mode_create_dumb *args,
			 struct drm_file *file_priv)
{
    ......
	return dev->driver->dumb_create(file_priv, dev, args);
}
......
int drm_mode_create_dumb_ioctl(struct drm_device *dev,
			       void *data, struct drm_file *file_priv)
{
	return drm_mode_create_dumb(dev, data, file_priv);
}

        当然除了使用TTM外,drm中还提供了基于CMA(Contiguous Memory Allocator)的GEM的API来分配和释放内存,如下所示,另外这这个场景中的dumb该怎么理解?只支持连续物理内存,基于kernel中通用CMA API实现,多用于小分辨率简单场景?

drm_gem_cma_dumb_create() - create a dumb buffer object
drm_gem_cma_free() - free resources associated with a CMA GEM
.......

        对于dumb buffer操作还提供了dumb_map_offset,看代码可以发现其在DRM_IOCTL_MODE_MAP_DUMP的ioctl中被调到。


int drm_mode_mmap_dumb_ioctl(struct drm_device *dev,
                 void *data, struct drm_file *file_priv)
{
    struct drm_mode_map_dumb *args = data;

    if (!dev->driver->dumb_create)
        return -ENOSYS;

    if (dev->driver->dumb_map_offset)
        return dev->driver->dumb_map_offset(file_priv, dev,
                            args->handle,
                            &args->offset);
    else
        return drm_gem_dumb_map_offset(file_priv, dev, args->handle,
                           &args->offset);
}

        那为什么需要这个ioctl呢?假设用户DRM_IOCTL_MODE_CREATE_DUMB申请了多个dumb buffer,那么要对哪一个做mmap()系统调用操作呢?如何才能让mmap()知道对哪一个dumb buffer进程操作呢?只能通过其offset参数来workaround来告诉mmap()具体要操作哪个。而上面这个DRM_IOCTL_MODE_MAP_DUMP就是为了为了完成这个工作的,传入create dumb buffer的handle后,它最终会返回一个offset(DRM文档称其为fake offset)到用户,然后用户拿这个offset就可以进程mmap()操作了。也由此可见用户mmap()时传进去的offset并不是真正的内存偏移量,而是一个gem object的索引值。通过该索引,drm驱动就可以准确找到当前要操作的是哪个gem object,进而获得相对应的物理buffer, 并对其做真正的mmap()操作。

amdkfd memory alloc/map

        不仅在上面的AMD Graphics业务,在AMD Compute业务中也同样用到了同样的机制,其代码实现思路与上述相似:

/* AMDKFD_IOC_ALLOC_MEMORY_OF_GPU时通过TTM来分配一块物理地址范围(只是做标记),
最终也会返回给用户一个handle, 然后AMDKFD_IOC_MAP_MEMORY_TO_GPU会根据
handle找到对应的物理地址范围并为GPU建立页表,这样GPU就能访问这块内存了。
但是当CPU想访问的话怎么操作?只需要在用户层open renderDx,接着在mmap()时,
传入AMDKFD_IOC_ALLOC_MEMORY_OF_GPU返回的offset即可 */
	AMDKFD_IOCTL_DEF(AMDKFD_IOC_ALLOC_MEMORY_OF_GPU,
			kfd_ioctl_alloc_memory_of_gpu, 0),
	AMDKFD_IOCTL_DEF(AMDKFD_IOC_MAP_MEMORY_TO_GPU,
			kfd_ioctl_map_memory_to_gpu, 0),


/* 在amdkfd中有二种方式来为GPU建立页表,一种是通过CPU来建立,一种是过DMA来建立。*/
    amdgpu_vm_cpu_update - helper to update page tables via CPU
    amdgpu_vm_sdma_update - execute VM update

        对于GTT和VRAM这二种类型的内存,在通过AMDKFD_IOC_ALLOC_MEMORY_OF_GPU分配BO的时候可以调用drm_vma_offset_add 将bo的范围加载到ttm_device(bdev) 红黑树,然后得到一个fake offset,并返回给用户层。

        然后在用户层mmap()时传入renderDx的fd和这个offset。这样mmap()就会调用到drm_driver所注册的mmap回调(fb->mmap),在该回调中可以注册vm_ops的回调。这样当在用户层访问 mmap() 返回的虚拟地址va时候,会发生缺页异常,然后会调用 vma_ops->fault 函数,完成CPU侧的映射。

prime

        在AMDGPU的Graphics业务中用到了prime,prime在DRM中其实是一种buffer共享机制,它是基于dma-buf实现的

static const struct drm_driver amdgpu_kms_driver = {
    ......
    .prime_handle_to_fd = drm_gem_prime_handle_to_fd,
    .prime_fd_to_handle = drm_gem_prime_fd_to_handle,
    .gem_prime_import = amdgpu_gem_prime_import,
    .gem_prime_mmap = drm_gem_prime_mmap,
    ......
};

除了Graphics业务,在Compute业务(amdkfd)也同样使用了dma_buf。

	AMDKFD_IOCTL_DEF(AMDKFD_IOC_GET_DMABUF_INFO,
				kfd_ioctl_get_dmabuf_info, 0),

	AMDKFD_IOCTL_DEF(AMDKFD_IOC_IMPORT_DMABUF,
				kfd_ioctl_import_dmabuf, 0),

mmu noitify

        从下面截取Linux-6.0内核版本的AMD GPU内核代码,从AMD提供了amdgpu_mn_unregister和amdgpu_mn_unregister的二个API(一般用在UMD userptr类型的memory中)实现可以看出,在HSA(Compute,for amdkfd)和GFX(Graphics)业务中都注册了mmu_notify回调。当使用到的CPU页表发生改变时都会调用该回调,如果使用时userpter类型的memory都是pinned(注意pin意思是不会被swap出去,而reserved是预留下来别人申请不到)的,cpu页表就不会变,也就调不到这里了,若是pin住的话,除非是页面的一些读写权限属性的改变才会调用

/**
 * amdgpu_mn_invalidate_gfx - callback to notify about mm change
 * @mni: the range (mm) is about to update
 * @range: details on the invalidation
 * @cur_seq: Value to pass to mmu_interval_set_seq()
 *
 * Block for operations on BOs to finish and mark pages as accessed and
 * potentially dirty.
 */
static bool amdgpu_mn_invalidate_gfx(struct mmu_interval_notifier *mni,
				     const struct mmu_notifier_range *range,
				     unsigned long cur_seq)
{
	struct amdgpu_bo *bo = container_of(mni, struct amdgpu_bo, notifier);
	struct amdgpu_device *adev = amdgpu_ttm_adev(bo->tbo.bdev);
	long r;

	if (!mmu_notifier_range_blockable(range))
		return false;

	mutex_lock(&adev->notifier_lock);

	mmu_interval_set_seq(mni, cur_seq);

	r = dma_resv_wait_timeout(bo->tbo.base.resv, DMA_RESV_USAGE_BOOKKEEP,
				  false, MAX_SCHEDULE_TIMEOUT);
	mutex_unlock(&adev->notifier_lock);
	if (r <= 0)
		DRM_ERROR("(%ld) failed to wait for user bo\n", r);
	return true;
}

/**
 * amdgpu_mn_invalidate_hsa - callback to notify about mm change
 *
 * @mni: the range (mm) is about to update
 * @range: details on the invalidation
 * @cur_seq: Value to pass to mmu_interval_set_seq()
 *
 * We temporarily evict the BO attached to this range. This necessitates
 * evicting all user-mode queues of the process.
 */
static bool amdgpu_mn_invalidate_hsa(struct mmu_interval_notifier *mni,
				     const struct mmu_notifier_range *range,
				     unsigned long cur_seq)
{
	struct amdgpu_bo *bo = container_of(mni, struct amdgpu_bo, notifier);
	struct amdgpu_device *adev = amdgpu_ttm_adev(bo->tbo.bdev);

	if (!mmu_notifier_range_blockable(range))
		return false;

	mutex_lock(&adev->notifier_lock);

	mmu_interval_set_seq(mni, cur_seq);

	amdgpu_amdkfd_evict_userptr(bo->kfd_bo, bo->notifier.mm);
	mutex_unlock(&adev->notifier_lock);

	return true;
}

static const struct mmu_interval_notifier_ops amdgpu_mn_hsa_ops = {
	.invalidate = amdgpu_mn_invalidate_hsa,
};

static const struct mmu_interval_notifier_ops amdgpu_mn_gfx_ops = {
	.invalidate = amdgpu_mn_invalidate_gfx,
};

/**
 * amdgpu_mn_register - register a BO for notifier updates
 *
 * @bo: amdgpu buffer object
 * @addr: userptr addr we should monitor
 *
 * Registers a mmu_notifier for the given BO at the specified address.
 * Returns 0 on success, -ERRNO if anything goes wrong.
 */
int amdgpu_mn_register(struct amdgpu_bo *bo, unsigned long addr)
{
	if (bo->kfd_bo)
		return mmu_interval_notifier_insert(&bo->notifier, current->mm,
						    addr, amdgpu_bo_size(bo),
						    &amdgpu_mn_hsa_ops);
	return mmu_interval_notifier_insert(&bo->notifier, current->mm, addr,
					    amdgpu_bo_size(bo),
					    &amdgpu_mn_gfx_ops);
}

/**
 * amdgpu_mn_unregister - unregister a BO for notifier updates
 *
 * @bo: amdgpu buffer object
 *
 * Remove any registration of mmu notifier updates from the buffer object.
 */
void amdgpu_mn_unregister(struct amdgpu_bo *bo)
{
	if (!bo->notifier.mm)
		return;
	mmu_interval_notifier_remove(&bo->notifier);
	bo->notifier.mm = NULL;
}
enum mmu_notifier_event {
	MMU_NOTIFY_UNMAP = 0,
	MMU_NOTIFY_CLEAR,
	MMU_NOTIFY_PROTECTION_VMA,
	MMU_NOTIFY_PROTECTION_PAGE,
	MMU_NOTIFY_SOFT_DIRTY,
	MMU_NOTIFY_RELEASE,
	MMU_NOTIFY_MIGRATE,
	MMU_NOTIFY_EXCLUSIVE,
};

        其中若用户想对userpter类型的memory做pin操作可以,可以自己使用内核提供的get_user_pages或get_user_pages_fast来执行pin操作。会增加PAGE的计数(调用get_page()),所以此PAGE不会被swap out的。

long get_user_pages(unsigned long start, unsigned long nr_pages,
		unsigned int gup_flags, struct page **pages,
		struct vm_area_struct **vmas)
{
    ......
}
int get_user_pages_fast(unsigned long start, int nr_pages,
			unsigned int gup_flags, struct page **pages)
{
    ......
}

比如,Linux内核态使用get_user_pages_fast将用户态进程使用的内存在内核态分配一块虚拟地址进行页表映射,此时相当于有两个页表映射同一块物理内存,如果此时用户态进程将该页表换出 内核访问的这片内存不就出错了 如何保证内核使用完之前内存不被换出呢? 

答:get_user_pages_fast() 会通过调用get_page()增加PAGE的计数,所以此PAGE不会被换出的(这里需要注意没有显式get_page()的page都有可能被迁移)。

最简单的情况是try_to_unmap()是会返回SWAP_AGAIN。这个PAGE就又被放回LRU了。

就是PTE已经被换成SWAP了,也没有关系。下次PAGE FAULT时,再从SWAP CACHE中找出这个页。

TLB invalidate

flush tlb/使tlb条目无效,对Driver来说只需要配置寄存器即可,比如对于compute业务的amdkfd来说直接使用如下函数来invalidate tbl。

void kfd_flush_tlb(struct kfd_process_device *pdd, enum TLB_FLUSH_TYPE type)

IOMMU/ATS

        大多数GPU设备一般自身是带有类似MMU的地址转换单元的,即使不使用IOMMU也是可以正常进行GVA到SPA之间的地址转换的。所以可以在这类GPU设备中不使能IOMMU/ATS。

使用IOMMU/ATS来进程GVA到SPA的转换

        IOMMU用途之一便是对设备向系统内存发起的DMA进程地址转换,IOMMU支持legacy mode和scalable mode,其中scalable mode可以支持比较高级的PASID和nested translate功能(但是目前大部分主板都不支持scalable mode?)

        其实只需要IOMMU就已经可以正常工作了,为什么还要开ATS(Address Translation Service)呢?ATS的提出主要是为了缓解iommu硬件iova转换的压力。尤其是当设备上有大量的DMA working sets时,ATS能够有效减少因为PCIe链路压力过大导致的设备性能抖动。ATS由位于PCIe设备上的ATC(Address Translation Cache) 和 Translaion Agent(TA,通常也是位于iommu硬件上)组成。ATC的作用可以跟cpu端的TLB来做类比,因此它也经常被称为Device TLB。ATC里面存储的主要是iova到hpa的映射关系,当ATC发生miss的时候需要跟TA之间进行一些交互。

如何在驱动中使能IOMMU/ATS?

/* device_iommu_mapped - Returns true when the device DMA is 
translated by an IOMMU */
if (device_iommu_mapped(&mdev->pdev->dev)) {
    pr_info("%s: Non-passthrough IOMMU detected, Enable ATS!\n", __func__);
    mdev->ats_enable = true;
} else {
    pr_info("%s: Disable ATS!\n", __func__);
    mdev->ats_enable = false;
}

...........................................................................
/* enable/disable ATS */
if (mdev->ats_enable)
    pci_enable_ats(mdev->pdev, PAGE_SHIFT);
else
   pci_disable_ats(mdev->pdev); 

...........................................................................
/* GPU自身页表PTE设置、地址转换单元的设置等,比如设置不对GVA进行翻译,
   直接把GVA发往PCIe,让IOMMU/ATS来翻译 */
if (mdev->ats_enable) {
    //PTE设置;
    //地址转换单元相关寄存器设置;
}

reference:

Linux x86-64 IOMMU详解(二)——SWIOTLB(软件IOMMU)_C is My Best Friend的博客-CSDN博客_swiotlb

https://lists.freedesktop.org/archives/amd-gfx/2021-January/057943.html

你可能感兴趣的:(GPU,驱动开发,linux)