概述
Linux图形子系统之GEM内存管理
- 引言
- 1 创建与映射
- 1.1 创建GEM
- 1.2 映射对象到文件
- 1.3 映射对象到用户空间
- 1.4 对象同步
- 2 内存分配
- 2.1 数据结构
- 2.1.1 内存管理结构
- 2.1.2 内存节点结构
- 2.2 分配算法
- 2.3 常见用法
引言
drm对内存使用抽象成GEM对象,用户空间通过句柄或文件映射的方式访问。
1 创建与映射
1.1 创建GEM
drm_mode_create_dumb_ioctl是DRM_IOCTL_MODE_CREATE_DUMB的处理函数,它直接调用了drm_mode_create_dumb函数,该函数通过参数解析和检查后,调用drm_driver的dumb_create回调函数。
dumb_create回调的常规实现如下:
int xxx_gem_dumb_create(struct drm_file *file,
struct drm_device *dev,
struct drm_mode_create_dumb *args)
{
# 略略略... ...
obj = kzalloc(sizeof(*obj), GFP_KERNEL);
drm_gem_private_object_init(dev, &obj->base, args->size);
obj->base.funcs = &xxx_gem_object_funcs;
# 略略略... ...
ret = drm_gem_object_create(file, obj->base, &handle);
if (ret)
return error;
args->handle = handle;
return 0;
# 略略略... ...
}
整个函数流程概况:
- 首先,分配一块驱动的GEM扩展结构体(内部嵌套drm_gem_object),通过drm_gem_private_object_init对嵌套drm_gem_object的base字段初始化后,继续初始化扩展的自定义字段。
- 然后,通过drm_gem_object_create为对象创建一个句柄。内部实现为通过idr_alloc为对象分配一个句柄;如果驱动实现了drm_gem_object_funcs的open回调,则调用,反之,若驱动实现了drm_driver的gem_open_object回调,则调用该回调。
注:一般gem扩展结构体的字段包括对象对应的内存信息记录
1.2 映射对象到文件
drm_mode_mmap_dumb_ioctl是DRM_IOCTL_MODE_MAP_DUMB的处理函数,该函数优先调用drm_driver的dumb_map_offset回调,如果没有实现则调用默认的处理函数drm_gem_dumb_map_offset:
int drm_gem_dumb_map_offset(struct drm_file *file, struct drm_device *dev,
u32 handle, u64 *offset)
{
struct drm_gem_object *obj;
int ret;
obj = drm_gem_object_lookup(file, handle);
if (!obj)
return -ENOENT;
/* Don't allow imported objects to be mapped */
if (obj->import_attach) {
ret = -EINVAL;
goto out;
}
ret = drm_gem_create_mmap_offset(obj);
if (ret)
goto out;
*offset = drm_vma_node_offset_addr(&obj->vma_node);
out:
drm_gem_object_put_unlocked(obj);
return ret;
}
整个函数流程概况:
- 首先,通过drm_gem_object_lookup函数在file中通过handle查找出drm_gem_object;
- 对于导入的对象(dma buffer导入),在默认函数中是禁止映射到文件;
- 然后,通过drm_gem_create_mmap_offset映射文件内偏移。将drm_gem_object的vma_node添加到drm_device的vma_offset_manager中管理;
- 最后,通过drm_vma_node_offset_addr获取对象vma_node的偏移。该偏移是相对于整个文件内的偏移。用户态用该偏移去mmap对应的drm file实现对gem内存的访问。
注:在对象释放的时候,需要通过drm_gem_free_mmap_offset去归还占用的offset
1.3 映射对象到用户空间
drm_gem_mmap函数是drm_file的mmap默认实现:
int drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)
{
# 略略略... ...
drm_vma_offset_lock_lookup(dev->vma_offset_manager);
node = drm_vma_offset_exact_lookup_locked(dev->vma_offset_manager,
vma->vm_pgoff,
vma_pages(vma));
if (likely(node)) {
obj = container_of(node, struct drm_gem_object, vma_node);
if (!kref_get_unless_zero(&obj->refcount))
obj = NULL;
}
drm_vma_offset_unlock_lookup(dev->vma_offset_manager);
# 略略略... ...
ret = drm_gem_mmap_obj(obj, drm_vma_node_size(node) << PAGE_SHIFT,
vma);
drm_gem_object_put_unlocked(obj);
return ret;
}
整个函数流程概况:
- 首先,通过vma->vm_pgoff获取node,转换为gem对象。vma->vm_pgoff为mmap调用时用户请求的文件偏移,该偏移是通过DRM_IOCTL_MODE_MAP_DUMB设置的;
- 然后,调用函数drm_gem_mmap_obj映射obj。在drm_gem_mmap_obj函数中,对vma相关字段赋值,其中包括:如果驱动实现了drm_gem_object_funcs的vm_ops回调,则将其设置为vma的vm_ops回调,反之,若驱动实现了drm_driver的gem_vm_ops回调,则用该回调设置vma的vm_ops回调;同时,将obj赋值给vma的vm_private_data字段。
gem对象或驱动需要实现vm_ops回调,该回调的open/close默认实现为drm_gem_vm_open/drm_gem_vm_close。vm_ops的fault实现过程如下:
vm_fault_t xxx_gem_fault(struct vm_fault *vmf)
{
struct vm_area_struct *area = vmf->vma;
struct drm_gem_object *obj = area->vm_private_data;
struct drm_xxx_gem_object *xxx_obj = to_intel_bo(obj);
pgoff_t page_offset;
u64 page;
page_offset = (vmf->address - area->vm_start) >> PAGE_SHIFT;
page = gem_xxx_get_one_page(obj, page_offset);
ret = vmf_insert_pfn(vma, vmf->address, page >> PAGE_SHIFT);
if (ret)
return VM_FAULT_SIGBUS;
return VM_FAULT_NOPAGE;
}
注: 此外,还有如下实现方式:
- 在xxx_gem_fault中,通过vm_insert_page映射,返回VM_FAULT_NOPAGE;
- 在xxx_gem_fault中,获取struct page*并赋值给vmf->page,返回0;
- 在drm_file的mmap回调中,通过remap_pfn_range一次性将obj所有物理页映射到vma;
1.4 对象同步
在多个渲染上下文中,可能存在同时访问某个GEM对象的可能。为了解决资源竞争的问题,提供了两个函数:
- drm_gem_lock_reservations:用于对多个需要使用的GEM对象加锁;
- drm_gem_unlock_reservations:用于解锁占用的多个GEM对象;
注:本质上是加解锁gem对象的resv->lock,这是一个ww_mutex,用于保护渲染上下文对gem添加读写fence的过程。实际的使用过程就是:先获取上一个上下文添加的dma fence,等待其触发;然后根据访问方式,插入一个dma fence;过程中是支持共享读/互斥写。
2 内存分配
上面只是介绍了GEM对象的创建和映射。具体对内存的管理,一般的驱动要么用drm提供的内存块管理,或者自定义实现方式。
drm内部提供了drm_mm_init和drm_mm_insert_node/drm_mm_remove_node函数管理设备内存块。前者用于初始化内存块的范围;后者用于内存的申请和释放。
2.1 数据结构
2.1.1 内存管理结构
struct drm_mm {
# 略略略... ...
struct list_head hole_stack;
struct drm_mm_node head_node;
struct rb_root_cached interval_tree;
struct rb_root_cached holes_size;
struct rb_root holes_addr;
unsigned long scan_active;
};
字段描述:
- head_node是drm_mm_init初始化的最大的空闲内存块,然后将所有空闲的和已分配的内存串成一个链表。
- holes_size/holes_addr、hole_stack表示所有的空闲内存结点。hole_stack按分配时间反序排列的链表;holes_size是空闲内存块的大小降序的红黑树;holes_addr是按空闲内存节点地址升序的红黑树;
- interval_tree是将所以分配的内存节点按地址升序的红黑树。
2.1.2 内存节点结构
struct drm_mm_node {
unsigned long color;
u64 start;
u64 size;
/* private: */
struct drm_mm *mm;
struct list_head node_list;
struct list_head hole_stack;
struct rb_node rb;
struct rb_node rb_hole_size;
struct rb_node rb_hole_addr;
# 略略略... ...
u64 hole_size;
# 略略略... ...
};
字段描述:
- start/size定义了节点的起始地址和大小。特别地,对于初始化添加的内存块,start等于地址尾部、size为负数;
- node_list是用于串联从当前节点分配的内存结点;hole_stack用于串联到drm_mm的hole_stack;rb用于插入到drm_mm的interval_tree;rb_hole_size用于插入到drm_mm的holes_size;rb_hole_addr用于插入到drm_mm的rb_hole_addr;
- hole_size表示空闲内存的大小。
2.2 分配算法
整个内存分配主要实现在函数drm_mm_insert_node_in_range中,相关概况如下:
- 首先,找到一个块合适的空闲内存块。如果通过drm_mm_insert_node进入,则在合格的内存块中选择最小的空闲内存块。由于存在内存对齐等因素,所以可能会经过多次选择;
- 然后,从选取的空闲内存块中分配内存到传入的节点,设置相关字段的值。
- 然后,将已分配的内存块添加到空闲内存块的node_list链表后面,再将已分配的内存块添加到drm_mm的interval_tree中,继续将空闲内存块从drm_mm的holes_size、holes_addr、hole_stack中移除;
- 最后,如果已分配的内存块的开始地址大于空闲内存区间的开始地址,需要将选取的空闲内存放回drm_mm的holes_size、holes_addr、hole_stack中;同样,如果已分配的内存块的末端地址小于空闲内存区间的末端地址,则将已分配内存作为一个空闲内存,添加到drm_mm的holes_size、holes_addr、hole_stack中;
注:整个分配算法特别有意思的是struct drm_mm的head_node的size为负数,其答案就在函数add_hole中,通过这种巧妙的设计让空闲内存和已分配内存的数据结构统一。
2.3 常见用法
常规中,一般在驱动程序中扩展struct drm_device结构,使其内嵌一个struct drm_mm结构,然后设备创建的时候通过drm_mm_init对其初始化;在扩展的GEM对象中嵌套struct drm_mm_node,使用内存的时候通过drm_mm_insert_node分配内存,不再需要的时候,通过drm_mm_remove_node释放。
注:操作过程中,需要添加锁保护。
最后
以上就是专一黑猫为你收集整理的Linux图形子系统之GEM内存管理引言1 创建与映射2 内存分配的全部内容,希望文章能够帮你解决Linux图形子系统之GEM内存管理引言1 创建与映射2 内存分配所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复