万书网 > 文学作品 > 深度探索Linux操作系统 > 8.2 显存

8.2 显存



Intel的GPU集成到芯片组中,一般没有专用显存,通常是由BIOS从系统物理内存中分配一块空间给GPU作专用显存。一般而言,BIOS会有个默认的分配规则,有的BIOS也会为用户留有接口,用户可以通过BIOS设置显存的大小。如对于具有1GB物理内存的系统来说,可以划分256MB内存给GPU用作显存。

但是这种静态的分配方式带来的问题之一就是如何平衡系统与显示占用的内存,究竟分配多少内存给GPU才能在系统常规使用和运行图形计算密集的应用(如3D应用)之间达到最优。如果分配给GPU的显存少了,那么在进行图形处理时性能必然会降低。而单纯提高分配给GPU的显存,也可能会造成系统的整体性能降低。而且,过多的分配内存给显存,那么当不运行3D应用时,就是一种内存浪费。毕竟,用户的使用模式不会是一成不变的,比如对于一个程序员来说,在编程之余也可能会玩一些游戏。但是我们显然不能期望用户根据具体运行应用的情况,每次都进入BIOS修改内存分配给显存的大小。

为了最优利用内存,一种方式就是不再从内存中为GPU分配固定的显存,而是当GPU需要时,直接从系统内存中分配,不使用时就归还给系统使用。但是CPU和GPU毕竟是两个完全独立的处理器,虽然现在CPU和GPU正在走融合之路,但是它们依然有自己的地址空间。显然,我们不能允许CPU和GPU彼此独立地去使用物理内存,这样必然会导致冲突,也正是因为这个原因,才有了我们前面提到的BIOS会从物理内存中划分一块区域给GPU,这样CPU和GPU才能井水不犯河水,分别使用属于自己的存储区域。



8.2.1 动态显存技术


为了解决这个矛盾,Intel的开发者们开发了动态显存技术(Dynamic  Video  Memory  Technology),相比于以前在内存中为GPU开辟专用显存,使用动态显存技术后,显存和系统可以按需动态共享整个主存。

动态显存中关键的是GART(graphics  address  remapping  table),也被称为GTT(graphics  translation  table),它是GPU直接访问系统内存的关键。事实上,这是CPU和GPU的融合过程中的一个产物,最终,CPU和GPU有可能完全实现统一的寻址。

GTT就是一个表格,或者说就是一个数组,表格中的每一个表项占用4字节,或者指向物理内存中的一个页面,或者设置为无效。整个GTT所能寻址的范围就代表了GPU的逻辑寻址空间,如512KB大小的GTT可以寻址512MB的显存空间(512K/4*4KB=512MB),如图8-2所示。

图 8-2 显存映射

这是一种动态按需从内存中分配显存的方式,GTT中的所有表项不必全部都映射到实际的物理内存,完全可以按需映射。而且当GTT中的某个表项指向的内存不再被GPU使用时,可以收回为系统所用。通过这种动态按需分配的方式,达到系统和GPU最优分享内存。内核中的DRM模块设计了特殊的互斥机制,保证CPU和GPU独立寻址物理内存时不会发生冲突。

我们注意到,GPU是通过GTT访问内存的(内存中用作显存的部分),所以GPU首先要访问GTT,但是,GTT也是在内存中。显然,这又是一个先有鸡还是先有蛋的问题。因此,需要另外一个协调人出现,这个协调人就是BIOS。在BIOS中,仍然需要在物理内存中划分出一块对操作系统不可见、专用于显存的存储区域,这个区域通常也称为Graphics  Stolen  Memory。但是相比于以前动辄分配几百兆的专用显存给GPU,这个区域要小多了,一般几MB就足矣,如我们前面讨论的寻址512MB的显存,只需要一个512KB的GTT表。

BIOS负责在Graphics  Stolen  Memory中建立GTT表格,初始化GTT表格的表项,更重要的是,BIOS负责将GTT的相关信息,如GTT的基址,写入到GPU的PCI的配置寄存器(PCI  Configuration  Registers),这样,GPU可以直接找到GTT了。BIOS中初始化GTT的代码大致如下:



在上面代码中,变量gfxMemAddr代表Graphics  Stolen  Memory的起始地址,gttMemStart代表GTT的起始地址,指针pGttEntry指向GTT的表项。代码第8~10行初始化了这几个变量,在初始化时,pGttEntry指向GTT表的开始,为后面填充GTT表作准备。

BIOS从物理内存的最顶端分配一块区域作为Graphics  Stolen  Memory,然后在这块区域中分配一块区域用作GTT,并将GTT所在的地址写入GPU的PCI的配置寄存器,见代码第5~6行。操作系统启动后将从这个寄存器中读取GTT表的地址,其中PCI_REG_GTT表示GPU中用作保存GTT地址的PCI配置寄存器。

在初始化时,GTT只需要映射Graphics  Stolen  Memory区域即可,当然GTT占用的空间就无需映射了。代码第12~16行就是映射GSM中除GTT以外的显存的,即gfxMemAddr到gttMemStart之间的部分。

显存的其余部分需要时动态按需分配,所以代码第18~21行的while循环就是将GTT中的表项设置为无效,亦即尚未分配显存。

在操作系统启动后,显存的分配和回收就由操作系统负责了,因此操作系统需要访问GTT。但是,GTT存储在操作系统不可见的Graphics  Stolen  Memory中,那么操作系统如何找到GTT呢?这就是BIOS将GTT的地址设置到GPU的PCI配置寄存器中的原因。在操作系统启动后,将从GPU的PCI配置寄存器中获取如GTT的基址等信息,代码如下:



在内核中,对于Intel不同系列的GPU,都有相应的GTT驱动,比如分别有针对i8xx、i915、sandybridge等型号的GTT驱动模块。

以i915系列的GPU为例,我们在其GTT驱动的函数i9xx_setup中可见,其使用pci_read_config_dword从GPU的PCI的配置寄存器I915_PTEADDR中读取GTT的基址等相关信息,然后将i9xx_setup读取的GTT的地址保存到gtt_bus_addr,这个变量就是用来保存GPU的GTT的地址的,后面我们在讨论显存绑定时会再次见到这个变量。

当然在GTT的驱动模块中,也包含操作GTT表的函数,如更新GTT表项的函数write_entry,后面在讨论Buffer  Object绑定到GTT时,我们会看到这个函数。




8.2.2 Buffer  Object

与CPU相比,GPU中包含大量的重复的计算单元,非常适合如像素、光影处理、3D坐标变换等大量同类型数据的密集运算。因此,很多程序为了能够使用GPU的加速功能,都试图和GPU直接打交道。因此,系统中可能有多个组件或者程序同时使用GPU,如Mesa中的3D驱动、X的2D驱动以及一些直接通过帧缓冲驱动直接操作帧缓冲的应用等。但是多个程序并发访问GPU,一旦逻辑控制不好,势必导致系统工作极不稳定,严重者甚至使GPU陷入一个混乱的状态。

而且,如果每个希望使用GPU加速的组件或程序都需要在自身的代码中加入操作GPU的代码,也使开发过程变得非常复杂。

于是,为了解决这一乱象,开发者们在内核中设计了DRM模块,所有访问GPU的操作都通过DRM统一进行,由DRM来统一协调对GPU的访问,如图8-3所示。

图 8-3 内核中的DRM模块

DRM的核心是显存的管理,当前内核的DRM模块中包含两个显存管理机制:GEM和TTM。TTM先于GEM开发,但是Intel的工程师认为TTM比较复杂,所以后来设计了GEM来替代TTM。目前内核中的ATI和NVIDA的GPU驱动仍然使用TTM,所以GEM和TTM还是共存的,但是GEM占据主导地位。

GEM抽象了一个数据结构Buffer  Object,顾名思义,就是一块缓冲区,但是比较特别,是GPU使用的一块缓冲区,也就是一块显存。比如一个颜色缓冲的像素阵列保存在一个Buffer  Object,绘制命令以及绘制所需数据也分别保存在各自的Buffer  Object,等等。笔者实在找不到一个准确的中文词汇来代表Buffer  Object,所以只好使用这个英文名称。开发者习惯上也将Buffer  Object简称为BO,后续为了行文方便,我们有时也使用这个简称,其定义如下:



其中两个关键的字段是filp和name。

对于一个BO来说,可能会有多个组件或者程序需要访问它。GEM使用Linux的共享内存机制实现这一需求,字段filp指向的就是BO对应的共享内存,代码如下:



既然多个组件需要访问BO,GEM为每个BO都分配了一个名字。当然这个名字不是一个简单的字符,它是一个全局唯一的ID,各个组件使用这个名字来访问BO。

BO可以占用一个页面,也可以占用多个页面。但是,通常BO都是占用整数个页面,即BO的大小一般是4KB的整数倍。在i915的BO的结构体定义中,数据项pages指向的就是BO占用的页面的链表,这里并不是使用的简单的链表,结构体sg_table使用了散列技术。具体代码如下:



为了可以被GPU访问,BO使用的内存页面还要映射到GTT。这个映射过程也比较直接,就是将BO所在的页面填入到GTT的表项中。以i915为例,下面这个函数就是获取BO占据的页面:



注意上面代码中使用黑体标识的filp,它指向了BO对应的共享内存区。显然,获取BO的页面实际就是获取这块共享内存的页面,代码中函数shmem_read_mapping_page_gfp就是做这件事的。当然BO可能对应多个页面,所以这里是一个循环,并将每个获取的页面放到散列表中,最后使BO中的指针pages指向这个页面散列表。

将BO的对应页表写入到GTT的表项中的代码如下:



函数intel_gtt_insert_sg_entries在内核的Intel的GTT驱动模块中,其实现代码如下:



函数intel_gtt_insert_sg_entries遍历BO对应页面的散列表,依次调用GTT驱动中的函数write_entry将页面的地址写入到GTT的表项中。GPU当然不能理解CPU使用的虚拟地址了,所以函数sg_dma_address返回的是页面的物理地址。i915系列GPU的GTT驱动的write_entry函数如下:



函数i830_write_entry逻辑非常简单,尤其是对于了解驱动的读者而言更是如此,其与我们向某个内存地址处赋值无本质区别。这里,addr就是页面的地址,intel_private.gtt是GTT的基址,entry是GTT中具体的表项。

读者可能有个疑问:GTT不是在BIOS划分给GPU专用的Graphics  Stolen  Memory中吗?那么CPU怎么可以寻址GTT,更新GTT的表项呢?内核中的GTT驱动模块已经考虑到了这点,在GTT模块初始化时,其使用ioremap将GTT所在地址映射到了CPU的地址空间,代码如下所示:



看到变量gtt_bus_addr是不是很熟悉?没错,在前面讨论i915的GTT驱动中的函数i9xx_setup时我们看到,i9xx_setup从GPU的PCI配置寄存器读取的GTT的基址就记录在这个变量中。

综上,我们看到,BO本质上就是一块共享内存,对于CPU来说BO与其他内存没有任何差别,但是BO又是特别的,它被映射进了GTT,所以它既可以被CPU寻址,也可以被GPU寻址,如图8-4所示。

图 8-4 Buffer  Object

为了方便程序使用内核的DRM模块,开发者们开发了库libdrm。在库libdrm中BO的定义如下:



其中两个重要的数据项是offset和virtual。

事实上,BO只是DRM抽象的在内核空间代表一块显存的一个数据结构。那么GPU是怎么找到BO的呢?如同CPU使用地址寻址一个内存单元一样,GPU也使用地址寻址。GPU根本不关心什么BO,它只认显存的地址。因此,每一个BO在显存的地址空间中,都有一个唯一的地址,GPU通过这个地址寻址,这就是offset的意义。offset是BO在显存地址空间中的虚拟地址,显存使用线性地址寻址,任何一个显存地址都是从起始地址的偏移,这就是offset命名的由来。offset通过GTT即可映射到实际的物理地址。当我们向GPU发出命令访问某个BO时,就使用BO的成员offset。

有时需要将BO映射到用户空间,其中数据项virtual就是记录映射的基址。

前面,我们讨论了BO的本质。下面我们从使用的角度看一看CPU与GPU又是如何使用BO的。BO是显存的基本单元,所以从保存像素阵列的帧缓冲,到CPU下达给GPU的指令和数据,全部使用BO承载。下面,我们分别从软件渲染和硬件渲染两个角度看看BO的使用。

(1)软件渲染

当GPU不支持某些绘制操作时,代表帧缓冲的BO将被映射到用户空间,用户程序直接在BO上使用CPU进行软件绘制。从这里我们也可以看出,DRM巧妙的设计使得BO非常方便地在显存和系统内存之间进行角色切换。

(2)硬件渲染

当GPU支持绘制操作时,用户程序则将命令和数据等复制到保存命令和数据的BO,然后GPU从这些BO读取命令和数据,按照BO中的指令和数据进行渲染。

库libdrm中提供了函数drm_intel_bo_subdata和drm_intel_bo_get_subdata,在程序中一般使用这两个函数将用户空间的命令和数据复制到内核空间的BO读者也会见到dri_bo_subdata和dri_bo_get_subdata。对于Intel的驱动来说,后面两个函数分别是前面两个函数的别名而已。后面讨论具体渲染过程时,我们会经常看到这几个函数。