GEM(Graphics Execution Manager)主要完成:内存申请释放、指令执行、执行命令时的光圈管理(what?!)。缓存对象的申请主要与linux提供的shmem层相关。设备相关操作如指令执行、pinning、buffer读写、映射、域所有权的转移等,还是归设备驱动的ioctl。
初始化
使用GEM的驱动必须在struct drm_driver->driver_features置DRIVER_GEM位,DRM core将会在执行load操作前自动初始化GEM core,这就意味着会构建一个DRM内存管理器,这个DRM内存管理器为对象的申请提供地址空间池。
在KMS设置上,如果硬件需要的话,驱动在核心GEM初始化后,需要申请并初始化环形缓冲区,这部分一般不由GEM管理,而是需要独立的初始化到自己的DRM MM对象。
创建GEM对象
GEM将创建GEM对象和其后的申请内存操作分成了两个不同的操作。GEM对象由struct drm_gem_object表示。驱动一般需要用私有信息来扩展GEM对象,因此struct drm_gem_object都是嵌入在驱动私有GEM结构体内的。创建一个GEM对象,驱动为自有GEM对象申请内存,并通过drm_gem_object_init(struct drm_device* ,struct drm_gem_object *,size_t )来初始化嵌入在其中的struct drm_gem_object。
GEM使用shmem来申请匿名页内存,drm_gem_object_init将会根据传入的size_t创建一个shmfs,并将这个shmfs file放到struct drm_gem_object的filp区。当图形硬件使用系统内存,这些内存就会作为对象的主存储直接使用,否则就会作为后备内存。驱动负责调用shmem_read_mapping_page_gfp()做实际物理页面的申请,初始化GEM对象时驱动可以决定申请页面,或者延迟到需要内存时再申请(需要内存时是指:用户态访问内存发生缺页中断,或是驱动需要启动DMA用到这段内存)。
匿名页面内存并不是一定需要的,嵌入式设备一般需要物理连续系统内存,驱动可以创建GEM对象儿没有shmfs,并调用drm_gem_private_object_init()初始化此私有GEM对象,不过这样的话,私有GEM对象的存储管理就需要驱动自行完成。
GEM对象的生命周期
所有GEM对象都有GEM core做引用计数,drm_gem_object_get()加数,drm_gem_object_put()减数,执行drm_gem_object_get()调用者必须持有struct drm_device的struct_mutex锁,而为了方便也提供了drm_gem_object_put_unlocked()也可以不持锁操作。当最后一个对GEM对象的引用释放时,GEM core调用struct drm_driver gem_free_object_unlocked,这一操作对于使能了GEM的驱动来说是必须,并且必须释放所有相关资源。driver实现接口void (*gem_free_object)(struct drm_gem_object *obj),负责释放所有GEM对象资源,这其中包括要调用drm_gem_object_release()来释放GEM core创建的资源。
GEM对象命名
GEM对象有本地handle、全局名称和文件描述符,都是32bit数。
对于本地handle:drm_gem_handle_create()创建GEM对象handle,这个函数拿着DRM file的指针和GEM对象,算得一个局部唯一handle,这个handle可以通过drm_gem_handle_delete()来删除,drm_gem_object_lookup()可以由handle找出对应的GEM对象。handle仅是一个对GEM对象的引用,在handle销毁时减去这一引用。
对于全局名称:可以在进程之间传递,不过在DRM API中,全局名称不能直接指到GEM对象,通过ioctl的DRM_IOCTL_GEM_FLINK (转成对象)和DRM_IOCTL_GEM_OPEN(转回全局名称),DRM core做此转换。GEM也支持通过PRIME dma-buf文件描述符的缓存共享,基于GEM的驱动必须使用提供的辅助函数来实现exoprting和importing。共享文件描述符比可以被猜测的全局名称更安全,因此是首选的缓存共享机制。通过GEM全局名称进行缓存共享仅在传统用户态支持。更进一步的说,PRIME由于其基于dma-buf,还允许跨设备缓存共享。
GEM对象映射
因为映射操作费时,GEM更多使用ioctl将内存映射到用户态,通过读写访问缓存;但是当随机访问缓存多的时候,如使用了软渲染,直接访问GEM对象高效。mmap系统调用不能直接映射GEM对象,因为GEM对象没有自己的文件描述符。目前同时存在两种map GEM对象到用户态的办法,一是用驱动自己的ioctl做映射操作,钩子后面挂上do_mmap(),不鼓励,比描述了;另一种方法是对DRM文件描述符使用mmap直接映射GEM对象,虽然GEM对象没有文件描述符,但是通过mmap(void*addr,size_t length,int port,int flags,int fd, off_t offset)中的offset参数,DRM可以找出这个GEM对象,为了能识别这个offset,驱动必须先对这个GEM对象执行了drm_gem_create_mmap_offset(),这样得到的offset值需要先通过驱动指定的方式传给用户态程序,然后就可以通过mmap传下来,找到这个GEM对象。GEM core提供有一个辅助方法drm_gem_mmap()来处理对象映射,这个方法可以直接设置为mmap文件操作的处理函数,它将会由offset查出GEM对象,然后设置VMA操作到struct drm_driver的gem_vm_ops;注意drm_gem_mmap()不映射内存到用户态,需要依赖于驱动提供的缺页处理函数做页面的映射,因此要用drm_gem_mmap(),驱动必须将struct drm_driver的gem_vm_ops填入,也就是填好
struct vm_operations_struct{
void (*open)(struct vm_area_struct* area);
void (*close)(struct vm_area_struct* area);
vm_fault_t (*fault)(struct vm_fault* vamp);
}
其中,open/close需要更新GEM对象的引用计数,驱动可以用drm_gem_vm_open()和drm_gem_vm_close()辅助函数直接作为open/close的处理函数;fault操作的处理函数负责在缺页发生时映射独立页面到用户态,驱动可以根据内存申请方案在缺页发生时申请页面,也可以在GEM对象建立时申请。对于没有MMU的平台,还有其他考虑,但是不予介绍。
内存对齐
当一个对象的后备页面映射到设备或用于指令缓存,这个页面就会被刷到内存并标记为CPU与GPU都会写入,比如:GPU渲染到一个对象,渲染完后CPU访问这个对象,那么这个对象就必须与CPU的内存视图保持一致,这就需要通过调用GPU缓存外刷等操作。这种CPU-GPU内存对齐的管理就由驱动指定的ioctl提供:看看对象当前域,必要的话要做刷新或同步使对象转到想要的域,不过对象可能会忙(比如正在被渲染),这时候就会发生阻塞,等待渲染完成,再做刷新或同步。
指令执行
提供指令执行接口给client可能是GEM最重要的功能,client程序构建指令缓存并提交到GEM,而这个指令缓存会引用到很多已经申请过的内存对象;这时候,GEM小心翼翼的绑定所有的对象到GTT,执行这个指令缓存,然后在client们之间提供必要的同步。这个过程中一般还会有扔掉一些对象,以及再绑进一些对象(开销很大),然后还要提供重映射将GTT的偏移量对client们隐藏起来。client们也要注意提交的指令缓存所引用的内存对象不要超过GTT的要求,如果超过则会被GEM拒绝,然后就不会有渲染操作执行了。一般来说,如果指令缓存中的这段渲染操作中的很多对象都需要申请fence寄存器,那么就要注意不要超过可以提供给client使用的fence寄存器的总数。这些资源的管理都要从libdrm重的客户端中抽象出来。