linux GPU上多個buffer間的同步之ww_mutex、dma_fence的使用 筆記


 
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;
}
 
參考文檔:
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM