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;
}
 
参考文档:
 

linux GPU上多个buffer间的同步 —— ww_mutex、dma-fence的使用 笔记的更多相关文章

  1. Linux内存管理机制中buffer和cache的区别

    Linux内存管理机制中buffer和cache的区别理解linux内存管理,需要深入了解linux内存的各个参数含义和规则,下面介绍一下Linux操作系统中内存buffer和cache的区别. Fr ...

  2. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

  3. (转)推荐一个在Linux/Unix上架设ASP.NET的 WEB服务器--Jexus

    在Linux/Unix上架设ASP.NET WEB服务器,有两个可选方式,一种是Mono+XSP,一种是Mono+Jexus,其它的方式,比如 Apache+mod_mono.Nginx+FastCg ...

  4. Linux平台上搭建apache+tomcat负载均衡集群

    传统的Java Web项目是通过tomcat来运行和发布的.但在实际的企业应用环境中,采用单一的tomcat来维持项目的运行是不现实的.tomcat 处理能力低,效率低,承受并发小(1000左右).当 ...

  5. Linux系统上的命令使用方法

    许多windows非常熟悉ipconfig命令行工具,它被用来获取网络接口配置信息并对此进行修改.Linux系统拥有一个类似的工具,也就是ifconfig(interfaces config).通常需 ...

  6. 在Linux服务器上运行Jupyter notebook server教程

    在Linux服务器上运行Jupyter notebook server教程 很多deep learning教程都推荐在jupyter notebook运行python代码,方便及时交互.但只在本地运行 ...

  7. Linux内存管理Swap和Buffer Cache机制

    Linux内存管理Swap和Buffer Cache机制 一个完整的Linux系统主要有存储管理,内存管理,文件系统和进程管理等几方面组成,贴出一些以前学习过的一个很好的文章.与大家共享!以下主要说明 ...

  8. [翻译]现代Linux系统上的栈溢出攻击【转】

    转自:http://www.codeweblog.com/%E7%BF%BB%E8%AF%91-%E7%8E%B0%E4%BB%A3linux%E7%B3%BB%E7%BB%9F%E4%B8%8A%E ...

  9. windows平台是上的sublime编辑远程linux平台上的文件

    sublime是个跨平台的强大的代码编辑工具,不多说. 想使用sublime完毕linux平台下django网站的代码编辑工作以提高效率(原来使用linux下的vim效率较低,适合编辑一些小脚本). ...

随机推荐

  1. matlab随机系数矩阵产生以及矩阵的可视化函数

    clc; clear all; close all; n = 100;%所产生矩阵的大小 A= sprandsym(n,0.015,0.1,1);%产生系数矩阵函数: spy(A)矩阵图形化相当于im ...

  2. MySQL中函数总结

    SQL中提供的函数: version() 查询当前数据库版本 user()   查询当前登录用户 database()    查询当前所在数据库 uuid()   返回uuid的值,分布式情况下数据库 ...

  3. C# Attribute特性 泛型<T> 方法的out ref this(扩展方法) Equals与==

    out ref out和ref的使用场景其实不一样.out适合在方法内返回一个额外的结果参数,而ref适合直接改变一个在方法外面的值,方法改变外部就改变,无需重新定义接住返回值再使用. out可以在方 ...

  4. matplotlib学习日记(一)------图表组成元素

      1.使用函数绘制matplotlib的图表组成元素 (1)函数plot---变量的变化趋势 import matplotlib.pyplot as plt import numpy as np x ...

  5. C#中的深度学习(五):在ML.NET中使用预训练模型进行硬币识别

    在本系列的最后,我们将介绍另一种方法,即利用一个预先训练好的CNN来解决我们一直在研究的硬币识别问题. 在这里,我们看一下转移学习,调整预定义的CNN,并使用Model Builder训练我们的硬币识 ...

  6. Jquery真的不难~第一回 编程基础知识

    Jquery真的不难~第一回 编程基础知识   回到目录 前言 说Jquery之前,先来学习一下Javascript(以后简称为JS)语言中的基础知识问题,其时对于每种编程语言来说基础知识都是大同小异 ...

  7. Java中jna的用法

    (1)jna是对jni的封装,让java使用者能更好的使用本地的动态库 (2)使用jna需要下载jna的jar包,该jar包就是对jni的封装,所以在调用效率上来讲,jna是要比jni低一点的,不过对 ...

  8. sql操作数据库(3)-->外键约束、数据库表之间的关系、三大范式、多表查询、事务

    外键约束 在新表中添加外键约束语法: constraint 外键约束名称 foreign key(外键的字段名称) references 主表表名(主键字段名) 在已有表中添加外键约束:alter t ...

  9. git基础-撤销操作

    ---恢复内容开始--- 撤销操作 在任何阶段,你都有可能想要撤销某些操作. 当我们提交完了代码,发现漏掉了几个文件没有添加,后者提交信息写错了,此时,可以运行--amend选项的提交命令尝试重新提交 ...

  10. js概念和ECMAScript

    概念 ​ ​就是一门浏览器客户端的脚本语言 运行在客户端浏览器中的,每一个浏览器都有JavaScript的解析引擎. 脚本语言,不需要编译,直接就可以被浏览器解析执行. 好处: ​ 可以增强一些用户的 ...