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