1. 程式人生 > >linux GPU上多個buffer間的同步 —— ww_mutex、dma-fence的使用 筆記

linux GPU上多個buffer間的同步 —— ww_mutex、dma-fence的使用 筆記

原文連結:https://www.cnblogs.com/yaongtime/p/14111134.html   WW-Mutexes   在GPU中一次Render可能會涉及到對多個buffer的引用。 所以在command buffer提交到GPU前,需要等到所有依賴的buffer可用。 因為這些buffer可能被多個裝置或程序所共享,所以相比單個buffer,增加了deadlock的風險。 這不能簡單地通過一個 buffer mutex鎖來等待buffer可用,因為這些buffer通常受控於應用程式. 比如Vertex shader中用到的vertex data: input attributes buffer 和 vertex index buffer,或者是fragment shader中用到的texel buffer、uniform buffer等,以及用作render的frambuffer。 所以沒有一個機制能保證這些buffer以相同的順序出現在各個共享程序中。 為解決這樣的問題linux kernel中引入了WW-Mutexes鎖。 WW-Mutexes與mutex是本質上是相同的,加鎖的方式也類似。   WW-Mutexes的工作機制大概是,首先將要引用的buffer的鎖加入到一個list裡面,然後依次對list中的鎖進行上鎖操作,對單個鎖的獲取可能會失敗,即該鎖已被其他人佔用。 當出現鎖獲取失敗時,接下來WW-Mutexes會有分兩種情況來解決衝突: 1.如果當前正在加鎖的程序(transaction)比已加鎖的程序新(younger),那麼當前程序的加鎖操作會被終止,停止之前釋放(unlock)已成功獲取到的鎖,然後等待重新對list的鎖進行再次加鎖操作。 2.如果當前正在加鎖的程序(transaction)比已加鎖的程序舊(older),那麼當前程序會等待,直到佔用該鎖的程序釋放鎖。   舉個例子: A:     lock-list: B0, B1, B2, B3     locked:    B0, B1, B2     locking:   B3 B:     lock-list: B1, B3, B4     locked:    B3     locking:   B1 1.如上有A、B兩個程序,假如B比A晚(younger)啟動,B程序正在對B1進行加鎖,但是B1已被A程序上鎖了,所以B程序加鎖失敗,因為B比A新,所以B需要釋放到它已獲取到的鎖B4,然後重新等待對B1,B3,B4的加鎖。 2.相反,如果B比A早(older)啟動,那麼B對B1加鎖失敗後,B會等待,直到B1被A釋放。接著看A的情況,A正在對B3進行加鎖(假設A在B開始等待後對B3加鎖),但B3已被B鎖住,按同樣的規則,這是A比B新,那麼A要被中斷,並釋放掉已獲取到的B0、B1、B2,並重新開始下一輪對B0, B1, B2, B3進行加鎖。因為A釋放掉B1,那麼B就能停止等待,獲取到B1了。一旦獲取到B lock-list中所有buffer的鎖後,B就能對這些buffer進行相應的操作,完畢後再釋放掉所有的鎖,A程序也就有機會重新獲取到所需的鎖了。   使用方法: 官方文件列舉了3種用法,這裡只列出一種,其他請參考:https://www.kernel.org/doc/html/latest/locking/ww-mutex-design.html   /* 靜態初始化一個 ww_class */ static DEFINE_WW_CLASS(ww_class);   /* 要被加鎖的物件,在其中嵌入 struct ww_mutex lock */ struct obj {       struct ww_mutex lock;       /* obj data */ };   /* 需要獲取的物件組成的list */ struct obj_entry {       struct list_head head;       struct obj *obj; };   int lock_objs(struct list_head *list, struct ww_acquire_ctx *ctx) {       struct obj *res_obj = NULL;       struct obj_entry *contended_entry = NULL;       struct obj_entry *entry;          /* 加鎖前對ww_acquire_ctx進行初始化 */       ww_acquire_init(ctx, &ww_class);     retry:       /* 一次從list中取出要加鎖的物件,並對其進行加鎖操作 */       list_for_each_entry (entry, list, head) {               if (entry->obj == res_obj) {                       res_obj = NULL;                       continue;               }           /* 加鎖操作,如果出現衝突,且當前程序較舊,會等待在 ww_mutex_lock()中,與mutex_lock()類似 */               ret = ww_mutex_lock(&entry->obj->lock, ctx);               if (ret < 0) {             /* 加鎖失敗,並且當前進行較新,當前進行將被終止繼續獲取剩餘的鎖,記錄下衝突物件 */                       contended_entry = entry;                       goto err;               }       }         ww_acquire_done(ctx);       return 0;   err:       /* 在進行下一輪加鎖前,釋放掉已獲取到的鎖 */       list_for_each_entry_continue_reverse (entry, list, head)               ww_mutex_unlock(&entry->obj->lock); /* 與mutex_unlock類似 */         if (res_obj)               ww_mutex_unlock(&res_obj->lock);         if (ret == -EDEADLK) {         /* 在開始下一輪的加鎖前,使用ww_mutex_lock_slow()獲取上一輪有衝突的鎖,ww_mutex_lock_slow()會一直休眠,直到該鎖可用為止 */               /* we lost out in a seqno race, lock and retry.. */               ww_mutex_lock_slow(&contended_entry->obj->lock, ctx);               res_obj = contended_entry->obj;         /* 跳轉到下一輪的加鎖操作 */               goto retry;       }       ww_acquire_fini(ctx);         return ret; }   void unlock_objs(struct list_head *list, struct ww_acquire_ctx *ctx) {     struct obj_entry *entry;       list_for_each_entry (entry, list, head)         ww_mutex_unlock(&entry->obj->lock);  //依次釋放list中的鎖       ww_acquire_fini(ctx); }   dma_resv   GEM object主要是提供了graphics memory manager,正是前文中提到的GPU buffer物件(linux kernel中還有其他的buffer管理物件)。 本文主要整理了GEM的中用到的同步方法,不對其他方面做講解。 GEM中主要用到WW-Mutexes和dma-fence來做同步,而這兩者被封裝到dma_resv中。 而dma_resv實際上是提供了所謂的隱式同步(implicit synchronization、implicit fence)。 reservation object提供了管理共享和獨佔fence的機制。 一個reservation object上只能新增一個獨佔fence(通常對於寫操作),或新增多個共享fence(讀操作)。 這類似於RCU的概念,一個reservation object管理的物件能支援併發的read操作,但是隻支援同時一個寫入操作。   Dma-fence是用在kernel內部的跨裝置(cross-device)的DMA操作同步原語,比如GPU向framebuffer做rendering,而displaying在讀取framebuffer前需要確保GPU已完成rendering操作,即讀操作之前,確保寫操作已完成。 Dma-fence通常有兩種狀態,signaled 和 unsignaled。在這裡,通常unsignaled表示buffer還在被使用,signaled表示buffer已使用完畢。 因為Dma-fence是為跨裝置間的同步而設計,這裡有多種使用dma-fence方式: 1、explicit fencing:單個dma-fence通過以檔案描述符(file descriptor)的形式暴露給使用者層,使用者層可以把該檔案描述符傳遞給其他程序,因為是對應用層可見的,所以叫這類dma-fence為explicit fencing。 2、implicit fencing:其實就是對使用者層不可見的dma-fence,通常儲存在dma_resv中,在通過dma_buf在核心中傳遞。   GEM buffer object的定義如下(省略了與本文無關的成員): struct drm_gem_object {     … …   struct dma_resv *resv;   struct dma_resv _resv;     … … };   resv     Pointer to reservation object associated with the this GEM object.     Normally (resv == &**_resv**) except for imported GEM objects. _resv     A reservation object for this GEM object.     This is unused for imported GEM objects.   GEM中對WW-Mutexes和dma-fence是通過dma_resv來實現的,dma_resv的定義如下: struct dma_resv {     struct ww_mutex lock;     seqcount_ww_mutex_t seq;       struct dma_fence __rcu *fence_excl;     struct dma_resv_list __rcu *fence; };   我們最終要關注的物件實際上是dma_resv。 簡單的說,我們關注的buffer物件,在這裡就是一個GEM物件,而這個GEM物件的同步操作是由GEM中的dma_resv提供的。 因為在這片文章中,不會涉及buffer同步以為的內容(例如backing memory),所以接下來在討論dma_resv時,實際上就是在討論單個GEM物件的同步,也即是單個buffer物件的同步。   前文已將談到,在GPU的操作中涉及到多buffer的同步互斥問題,需要一次性準備好GPU的pipeline上所需要的buffer。 當使用這組buffer時,很可能這組buffer也被其他人使用。 如果針對單個buffer加鎖(如mutex),會有死鎖的風險(deadlock),比如A、B兩個程序都需要同時引用兩個buffer,分別對兩個buffer加鎖,A獲得buffer0,B獲得buffer1,當A在對buffer1加鎖就會死鎖,同樣的B也會在加鎖buffer0時死鎖。 所以就引入了WW-Mutexes來解決這樣的衝突,Linux DRM中的GEM提供了對WW-Mutexes的支援。。 進一步,我們發現在GPU上,對buffer的操作有讀有寫,比如texture buffer、uniform buffer是隻讀的,framebuffer可讀可寫。 寫操作必須是獨佔式的,但讀操作卻可以被共享,所以又引入了dma-fence來達到這樣的目的。 dma_resv把WW-Mutexes和dma-fence相結合,達到多buffer間同步的最優化。   使用步驟: kernel中已經做了很好的封裝,涉及到幾個函式的呼叫,我總結的步驟如下: 1、呼叫drm_gem_lock_reservations()獲取GPU一次rendering所用到的buffer的鎖ww_mutex 2、成功獲取到所有buffer的ww_mutex鎖後,針對每個buffer在GPU中的使用情況新增不同的dma-fence,      如果GPU中會讀取某個buffer,則通過函式dma_resv_add_shared_fence()新增一個共享dma-fence;      如果GPU會寫每個buffer,則通過函式dma_resv_add_excl_fence()新增一個獨佔的dma-fence。       注意在呼叫dma_resv_add_excl_fence()前,需要確保在這之前新增的share fence均處於unsignaled狀態,就是確保寫之前,讀操作已全比完成。 3、完成fence的新增後,呼叫drm_gem_unlock_reservations()釋放這組buffer的ww_mutex 4、接下來,當其他程序或裝置要對某個buffer做操作前,需要判斷dma-fence的情況。      假如我要讀取framebuffer的內容用於螢幕顯示,那就是讀之前,需要確保寫結束,呼叫函式dma_resv_get_excl_rcu(), 讀取獨佔dma-fence,確保其為unsignaled狀態。      例如,我們看看KMS的atomic中的plane frambuffer 的操作:      讀取獨佔dma-fence:      int drm_gem_fb_prepare_fb(struct drm_plane *plane,                   struct drm_plane_state *state)     {         struct drm_gem_object *obj;         struct dma_fence *fence;           if (!state->fb)             return 0;           obj = drm_gem_fb_get_obj(state->fb, 0);         fence = dma_resv_get_excl_rcu(obj->resv);         drm_atomic_set_fence_for_plane(state, fence);           return 0;     }        在KMS的atomic操作中,會等待獨佔dma-fence被signal,程式碼如下:          int drm_atomic_helper_wait_for_fences(struct drm_device *dev,                           struct drm_atomic_state *state,                           bool pre_swap)     {         struct drm_plane *plane;         struct drm_plane_state *new_plane_state;         int i, ret;             for_each_new_plane_in_state(state, plane, new_plane_state, i) {             if (!new_plane_state->fence)                 continue;                 WARN_ON(!new_plane_state->fb);                 /*              * If waiting for fences pre-swap (ie: nonblock), userspace can              * still interrupt the operation. Instead of blocking until the              * timer expires, make the wait interruptible.              */             ret = dma_fence_wait(new_plane_state->fence, pre_swap);             if (ret)                 return ret;                 dma_fence_put(new_plane_state->fence);             new_plane_state->fence = NULL;         }             return 0;     }   程式碼簡析: 函式drm_gem_lock_reservations()的加鎖過程就是,上文中提到的ww_mutexes的典型用法程式碼如下: int drm_gem_lock_reservations(struct drm_gem_object **objs, int count,               struct ww_acquire_ctx *acquire_ctx) {     int contended = -1;     int i, ret;       ww_acquire_init(acquire_ctx, &reservation_ww_class);   retry:     if (contended != -1) {         struct drm_gem_object *obj = objs[contended];           ret = dma_resv_lock_slow_interruptible(obj->resv,                                  acquire_ctx);         if (ret) {             ww_acquire_done(acquire_ctx);             return ret;         }     }       for (i = 0; i < count; i++) {         if (i == contended)             continue;           ret = dma_resv_lock_interruptible(objs[i]->resv,                                 acquire_ctx);         if (ret) {             int j;               for (j = 0; j < i; j++)                 dma_resv_unlock(objs[j]->resv);               if (contended != -1 && contended >= i)                 dma_resv_unlock(objs[contended]->resv);               if (ret == -EDEADLK) {                 contended = i;                 goto retry;             }               ww_acquire_done(acquire_ctx);             return ret;         }     }       ww_acquire_done(acquire_ctx);       return 0; }   參考文件: https://www.kernel.org/doc/html/latest/locking/ww-mutex-design.html https://www.kernel.org/doc/html/latest/gpu/drm-mm.html &nbs