Linux图形子系统之GEM内存管理

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内幕,linux)