linux DRM GEM 筆記

原文鏈接://www.cnblogs.com/yaongtime/p/14418357.html

在GPU上的各類操作中涉及到多種、多個buffer的使用。
通常我們GPU是通過圖像API來調用的,例如OPENGL、vulkan等,所以GPU上buffer的使用,實際上就是在這些圖像API中被使用。
例如在opengl es中,vertex/fragment shader、vertex index、vertex buffer object、uniform buffer object、texture、framebuffer等都需要一塊memory buffer來存儲對應的內容。
而在vulkan中有提供明確的memory管理規則,它把memory分成兩半來管理分別是:resource和backing memory。
resource有兩種類型buffer和image。
Backing memory也被叫做device memory。
不同類型的resource對Backing memory的需求不一樣,vulkan根據resource的屬性來分配對應的backing memory。
所以memory的管理在整個GPU的操作中起着重要作用。
 
Memory buffer究其本質就是ram上的段內存空間可被表示為:address + size。
如果支持MMU,虛擬地址連續,但物理地址不連續的一段內存。
 
因為linux系統的特點,應用層不能直接訪問物理地址等原因,所以需要linux kernel中提供一種方法來讓用戶層圖像API訪問device buffer。
GEM(Graphics Execution Manager)即是linux DRM中用於完成memory管理的內核基礎設施(不止這一種)。
 
GEM作為一種內存管理方式,並未覆蓋各種在userspace和kernel使用情況(use cases)。
GEM提供了一組標準的內存相關的操作給userspace,以及一組輔助函數給kernel drivers,kernel drivers還需要實現一些硬件相關的私有操作函數。
GEM所管理的memory具體類型、屬性是不可知的,我們並不知道它所管理的buffer對象包含了什麼。如果要獲知GEM所管理的buffer對象的具體內容和使用目的,需要kernel drivers自己實現一組私有的ioctl來獲取對應的信息。
 
一個實際的GEM對象所管理的memory類型與硬件平台密切相關,這裡我們主要討論嵌入式平台上的GPU的MEMORY管理。
嵌入式平台上GPU和CPU往往共享主存DDR,所以在本文中討論GEM的backing memory往往就是DDR上的某段物理內存頁(連續或非連續均可)。
這段物理內存會被CPU(分內核虛擬地址和用戶層虛擬地址)、GPU(虛擬地址和物理地址)訪問。
在CPU端訪問時,當在用戶層訪問時,需要通過GEM的mmap()規則映射成用戶層虛擬地址,在kernel中使用時需要映射成內核虛擬地址。
在GPU端訪問時,如果GPU支持MMU,GPU也使用MMU映射後虛擬地址,如果不支持MMU,GPU直接訪問物理地址。
 
userspace:
在userspace當需要創建一個新的GEM對象時,會通過調用driver私有的ioctl接口來獲取。
雖然不同driver設計的icotl接口不一樣,但是最終都通過返回一個handle給用戶層,來作為kernel中一個GEM對象的引用,而這個handle就是一個u32的整數。
所以一個kernel中的GEM對象被抽象為一個不透明的u32整數值,所以userspace對一個GEM對象的操作均透過這個handle來進行。
 
如前所述,GEM本質上是做內存管理,而內存上最常規的操作就是讀寫。
而在對內存讀寫上我們需要增加各種限制條件,這些條件可以是,比如誰可以在什麼時候寫入什麼地方,誰可以在什麼時候從哪個地址讀取等。
 
當在userspace需要訪問GEM buffer內存時,通常通過mmap()系統調用來映射GEM對象所包含的物理地址。
因為在userspace一個handle就代表一個GEM對象,在映射前還需要通過driver私有的ioctl返回一個pg_offset,作為一個mmap()的「off_t offset」參數。
詳細的討論將在mmap節展開。
 
Kernel space:
本文主要討論內容是kernel driver中對GEM的使用。
 
GEM對象的分配和它的backing memory的分配是分開的。
一個GEM對象通過struct drm_gem_object來表示,驅動程序往往需要把struct drm_gem_object嵌入到自己的私有數據結構中,主要用於內存對象的管理。
struct drm_gem_object對象中不包含內存分配的管理,Backing memory分配將在memory分配段討論。
 
在kernel中struct drm_gem_object的被定義為:
struct drm_gem_object {
 
 
  struct kref refcount;
  unsigned handle_count;
  struct drm_device *dev;
 
 
  struct file *filp;
  struct drm_vma_offset_node vma_node;
 
 
  size_t size;
 
 
  int name;
 
 
  struct dma_buf *dma_buf;
  struct dma_buf_attachment *import_attach;
  struct dma_resv *resv;
  struct dma_resv _resv;
 
 
  const struct drm_gem_object_funcs *funcs;
};
 
提供如下幾點管理:
1、對象本身的創建銷毀管理,引用計數等。
2、vma管理,主要是配合用戶層的調用mmap()時會用到。
3、shmem文件描述符獲取
4、在PRIME中用於import/export操作
5、同步操作
6、回調函數提供平台差異實現
 
struct drm_gem_object中主要是包含了通用的部分,存在平台差異化的地方通過兩個方法來解決。
一是通過一組回調函數接口,讓drivers提供各自的實現版本。
二是通過嵌入struct drm_gem_object到各自私有的數據結構中,來擴展GEM對象的管理內容。
 
在一個GEM對象上涉及到的操作或者是提供的功能如下:
1.create
2.backing memory
3.mmap
4.import/export
5.sync
 
1.首需要創建一個GEM對象
2.GEM是管理一段內存,那麼必然涉及到實際物理內存分配
3.GEM分配的內存要在用戶層能訪問,需要通過對mmap()的支持
4.GEM對象的內存可以來至driver自己的分配,同樣可以從外部模塊引入,也支持將GEM對象所管理的內存導出給其他模塊使用
5.當GEM對象被多個模塊使用時,就涉及到buffer數據的同步
 
GEM對象創建
一個GEM對象通常需要嵌入到driver私有數據結構中(類似於基類)。
目前的kernel中提供了helper函數,這些函數就是在嵌入了GEM對象的基礎上實現的。
kernel中提供了幾種常用到的GEM對象的擴展,我們會討論到CMA、shmem這兩種擴展,圍繞這兩者有相應的helper函數。
前文提到GEM把實際的內存配實際上留給了drivers自己實現,從CMA、shmem的名字即可知,這種擴展分別對應從CMA或shmem分配實際的物理內存。
 
物理內存分配
CMA(Contiguous Memory Allocator)是linux系統早期啟動時,預留的一段內存池。
CMA用於分配大塊的、連續的物理內存。
當GPU或display模塊不支持MMU時,使用CMA來分配內存是不錯的選擇。
 
CMA作為GEM對象的內存分配:
struct drm_gem_cma_object {
  struct drm_gem_object base;
  dma_addr_t paddr;
  struct sg_table *sgt;
  void *vaddr;
};
 
base:GEM對象
paddr:分配的內存物理地址
sgt:通過PRIME導入的scatter/gather table,這個table上的物理地址必須保證是連續的。
vaddr:分配的內存虛擬地址
 
函數drm_gem_cma_create()通過調用__drm_gem_cma_create()完成一個struct drm_gem_cma_object對象分配和初始化,然後通過dma_alloc_wc()分配指定大小的內存。
 
struct drm_gem_cma_object *drm_gem_cma_create(struct drm_device *drm,
                          size_t size)
{
    struct drm_gem_cma_object *cma_obj;
    int ret;
 
 
    size = round_up(size, PAGE_SIZE);
 
 
    cma_obj = __drm_gem_cma_create(drm, size);
    if (IS_ERR(cma_obj))
        return cma_obj;
 
 
    cma_obj->vaddr = dma_alloc_wc(drm->dev, size, &cma_obj->paddr,
                      GFP_KERNEL | __GFP_NOWARN);
    if (!cma_obj->vaddr) {
        drm_dbg(drm, “failed to allocate buffer with size %zu\n”,
             size);
        ret = -ENOMEM;
        goto error;
    }
 
 
    return cma_obj;
 
 
error:
    drm_gem_object_put(&cma_obj->base);
    return ERR_PTR(ret);
}
 
MMAP:
 
GEM對提供了mmap()的支持,通過映射後usersapce可以訪問GEM對象的backing memory。
一種是完全由driver自己提供私有的ioctl實現。
 
GEM建議的方式是走mmap系統調用:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
 
前文提到一個GEM對象在usersapce看來就是一個u32的不透明handle值,這個handle值不能直接和mmap配合使用。
所以要如何通過mmap來映射一個GEM對象,使其能在usersapce被訪問呢?
DRM是通過mmap的offset參數來識別出一個要被映射的GEM對象的。
在一個GEM對象能被mmap映射前,這個GEM對象會調用函數drm_gem_create_mmap_offset()去分配一個fake offset。
然後driver自身需要實現一個獨有的ioctl(),將這個fake offset傳遞給usersapce。
當usersapce拿到這個fake offset後,作為參數傳遞給mmap 的offset參數。
當mmap執行在kernel中DRM部分時,DRM通過這個offset參數返回GEM對象,獲取其上的backing memory,從而完成對這個GEM對象的映射。
 
如果GPU支持MMU,可以用shmem分配memory。
shmem分配的物理頁可能不連續,因為GPU支持MMU,所以GPU也能訪問這種不連續的物理頁。使用shmem可以充分利用缺頁異常分配memory的特性,真正建立頁表是在用戶空間對映射地址進行read/write時,觸發缺頁異常時,才執行虛擬地址到物理地址的映射。
而如果GEM對象的backing memory是CMA時,在mmap系統調用,進入kernel driver部分執行時,就需要完成用戶虛擬地址到物理地址的映射。
 
static struct drm_gem_cma_object *
__drm_gem_cma_create(struct drm_device *drm, size_t size)
{
    struct drm_gem_cma_object *cma_obj;
    struct drm_gem_object *gem_obj;
    int ret;
 
 
    if (drm->driver->gem_create_object)
        gem_obj = drm->driver->gem_create_object(drm, size);
    else
        gem_obj = kzalloc(sizeof(*cma_obj), GFP_KERNEL);
    if (!gem_obj)
        return ERR_PTR(-ENOMEM);
 
 
    if (!gem_obj->funcs)
        gem_obj->funcs = &drm_gem_cma_default_funcs;
 
 
    cma_obj = container_of(gem_obj, struct drm_gem_cma_object, base);
 
 
    ret = drm_gem_object_init(drm, gem_obj, size);
    if (ret)
        goto error;
 
 
    ret = drm_gem_create_mmap_offset(gem_obj);
    if (ret) {
        drm_gem_object_release(gem_obj);
        goto error;
    }
 
 
    return cma_obj;
 
 
error:
    kfree(cma_obj);
    return ERR_PTR(ret);
}
 
 
PRIME IMPORT/EXPORT
 
GPU上完成一幀圖像的渲染後,通常要送到display模塊去顯示,或是在有多個GPU的桌面機上,GPU間的buffer切換。
這都涉及到將本地內存對象共享給其他模塊(本質上是讓其他模塊訪問GPU渲染後的framebuffer)。
同樣其他模塊也可能指定一塊buffer,讓GPU把數據渲染在其之上,對GPU driver來說就需要引入某個內存buffer。
 
PRIME IMPORT/EXPORT是DRM的標準特性,GEM只是其中一個具體的實現的方式,因為本文只討論GEM,所以後續均討論不對這兩者做區分。
 
這種導入/導出操作均由userspace來發起,由driver來提供具體的實現。
之所以要有userspace來發起,因為driver不能預先知道何時需要導入/導出,也不能預先知道要導入/導出的去向來路。
DRM提供了兩個ioctl命令,分別對應導入和導出:
DRM_IOCTL_DEF(DRM_IOCTL_PRIME_HANDLE_TO_FD, drm_prime_handle_to_fd_ioctl, DRM_RENDER_ALLOW)
DRM_IOCTL_DEF(DRM_IOCTL_PRIME_FD_TO_HANDLE, drm_prime_fd_to_handle_ioctl, DRM_RENDER_ALLOW)
 
導出GEM:
我們知道在userspace一個GEM對象通過一個handle來表示。
當要把這個GEM對象導出,我們通過ioctl傳遞這個handle值給driver,然後driver會返回一個fd。
這個fd就是一個文件描述符,和通過open()系統調用返回的fd是同一個東西。
而這個fd可以通過UNIX domain sockets在進程間傳遞。
 
從driver中返回這個fd是通過dma_buf來實現的。
dma_buf是專門設計來供多個模塊間進行DMA共享的。
 
導入GEM:
GEM對象是由driver創建,但backing memory是通過dma_buf的來獲取。
 
這篇筆記寫的潦草了一點,希望以後有時間能補全。
 
參考鏈接: