参考内核代码: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