Heap
GPU驱动程序(DDK)使用heap管理设备虚拟内存和设备物理内存,应用启动时在应用层会创建
general svm heap、general heap、PDS code date heap、USC code heap等设备虚拟地址段(DEVMEM_HEAP),每个应用的设备虚拟地址都是独立的,类似CPU虚拟地址;同时,内核驱动模块探测到GPU设备时,会为设备的物理地址空间创建多个heap(PHYS_HEAP),包括PHYS_HEAP_GPU_LOCAL、PHYS_HEAP_GPU_PRIVATE、PHYS_HEAP_FW_MAIN、PHYS_HEAP_FW_CONFIG、PHYS_HEAP_FW_CODE等。关于heap的详细分析参看我写的另一篇文档《DDK heap的创建流程》。
RA
RA即resource arena,可以理解为资源池。heap使用RA管理内存,包括内存的申请、内存的释放等。每个物理地址heap(PHYS_HEAP)有一个RA,每个虚拟地址heap(DEVMEM_HEAP)有两个RA,详细见后面分析。firmware相关的FwMain设备虚拟地址空间、FwConfig设备虚拟地址空间也是通过RA进行管理。
RA通过伸展树(splay tree)管理内存。每个RA都有自己的伸展树。当调用RA_Create创建一个RA时,伸展树为空,随后可以调用RA_Add向RA中添加资源,添加资源后RA对应的伸展树就有了一个根节点,整个流程可以参见函数RA_Create_With_Span。
伸展树以标记(flag)为key进行组织,相同flag值属于同一个结点,不同的结点flag值不同。创建RA并添加资源时,传入的参数flag值为0,所以根节点的flag值为0。
flag表示内存的特性,例如表示使用的哪块物理内存(GPU Local、GPU Private、Fw Main、Fw Config、Fw code等),还表示GPU是否可读、可写该内存,CPU是否可读、可写该内存(可以通过配置MMU页表项实现)等等。
如下图一所示:RA对应的伸展树有node A、node B、node C、node D四个结点,它们的flag分别为flag A、flag B、flag C、flag D,它们的值都各不相同。 每个结点有一个free table(代码中写为buckets)。
之所以选择伸展树,是因为考虑到程序运行的局部性原理,即在一段时间内,整个程序的执行仅限于程序中的某一部分。而在伸展树中查找时,伸展树也会进行变换,使根节点的flag等于本次查找的flag值,一段时间内很可能多次分配同样flag值的资源,这样后续就直接从根节点分配而不需要查找子节点,节省了时间。
图1 splay tree
RA的数据结构如下,其中成员per_flags_buckets指向伸展树根节点:
/* resource allocation arena */
struct _RA_ARENA_
{
/* arena name for diagnostics output */
IMG_CHAR name[RA_MAX_NAME_LENGTH];
/* Spans / Imports within this arena are at least quantum sized
* and are a multiple of the uQuantum. This also has the effect of
* aligning these Spans to the uQuantum.
*/
RA_LENGTH_T uQuantum;
/* import interface, if provided */
PFN_RA_ALLOC pImportAlloc;
PFN_RA_FREE pImportFree;
/* Arbitrary handle provided by arena owner to be passed into the
* import alloc and free hooks
*/
void *pImportHandle;
IMG_PSPLAY_TREE per_flags_buckets;
/* resource segment list */
BT *pHeadSegment;
/* segment address to boundary tag hash table */
HASH_TABLE *pSegmentHash;
/* Lock for this arena */
POS_LOCK hLock;
/* Policies that govern the resource area */
RA_POLICY_T ui32PolicyFlags;
/* LockClass of this arena. This is used within lockdep to decide if a
* recursive call sequence with the same lock class is allowed or not.
*/
IMG_UINT32 ui32LockClass;
/* Total Size of the Arena */
IMG_UINT64 ui64TotalArenaSize;
/* Size available for allocation in the arena */
IMG_UINT64 ui64FreeArenaSize;
};
伸展树节点数据结构如下,其中成员psLeft指向该节点的左节点、成员psRight指向该节点的右节点,buckets为该节点的free list:
typedef struct img_splay_tree
{
/* left child/subtree */
struct img_splay_tree * psLeft;
/* right child/subtree */
struct img_splay_tree * psRight;
/* Flags to match on this span, used as the key. */
IMG_PSPLAY_FLAGS_T uiFlags;
#if defined(PVR_CTZLL)
/* each bit of this int is a boolean telling if the corresponding
bucket is empty or not */
IMG_ELTS_MAPPINGS bHasEltsMapping;
#endif
struct _BT_ * buckets[FREE_TABLE_LIMIT];
} IMG_SPLAY_TREE, *IMG_PSPLAY_TREE;
free table管理
free table用于管理空闲内存,free table有40个buckets[0]、buckets[1],...,buckets[39],每个结点指向一个链表,这些链表的结点由空闲内存块组成。当从内存池中分配内存时,用户需要指定内存flag,然后在splay tree中查找该flag,如果找到则从该结点关联的free list中查找空闲内存,在查找flag的过程中,splay tree会进行旋转操作,这个操作比较耗时。buckets[0]指向的链表中都是空闲内存大小为[ ,)字节的结点,buckets[1]指向的链表中都是空闲内存大小为[,)字节的结点,以此类推。BT(boundary tags)用于描述资源段,每个BT描述的资源都是连续的。
图2 free table
BT的数据结构如下:
/* boundary tags, used to describe a resource segment */
struct _BT_
{
enum bt_type
{
btt_free, /* free resource segment */
btt_live /* allocated resource segment */
} type; //表示该BT空闲还是已分配
unsigned int is_leftmost;
unsigned int is_rightmost;
unsigned int free_import;
/* The base resource and extent of this segment */
RA_BASE_T base;
RA_LENGTH_T uSize;
/* doubly linked ordered list of all segments within the arena */
struct _BT_ *pNextSegment;
struct _BT_ *pPrevSegment;
/* doubly linked un-ordered list of free segments with the same flags. */
struct _BT_ *next_free;
struct _BT_ *prev_free;
/* A user reference associated with this span, user references are
* currently only provided in the callback mechanism
*/
IMG_HANDLE hPriv;
/* Flags to match on this span */
RA_FLAGS_T uFlags;
};
typedef struct _BT_ BT;
重要成员变量含义如下:
- is_leftmost:指示该BT是否是某连续地址段的起始地址块。
- is_rightmost:指示该BT是否是某连续地址段的结束地址块。如果is_leftmost和is_rightmost都为0,则指示该BT是某连续地址段的中间块。如果is_leftmost和is_rightmost都为非0,则指示该BT还没有被分裂(split)为几个BT。
- free_import:如果为非0,则指示该BT从其他RA import(详细见下面分析)。
- pNextSegment:指向下一个BT。一个RA的所有BT(无论空闲还是已经分配)都会加入结构体_RA_ARENA_成员pHeadSegment指向的链表,链表中的BT就是通过BT的成员变量pNextSegment和pPrevSegment连接。当需要从一个大BT中分配一块小的资源时,一个大BT被分裂为多个BT后,通过该指针可以找到被分裂的相邻BT,这些相邻BT组成的地址段连续;但是通过该指针连接的相邻BT地址不一定是连续的,只有is_leftmost被设置的BT和is_rightmost被设置的BT及它们之间的BT组成的资源是连续的,如下图3、图4所示。
- pPrevSegment:指向前一个BT。详见上面成员变量pNextSegment分析。
- next_free:指向下一个空闲BT。
- prev_free:指向前一个空闲BT。
从RA分配资源
当从RA中分配资源时,需要根据分配资源的flag在伸展树中查找,如果伸展树的所有结点没有等于分配内存flag的值,则返回失败;否则,返回找到的结点,并在该结点的free table(buckets)中分配资源。在伸展树中查找时,伸展树也会进行变换,使根节点的flag等于本次查找的flag值,这是考虑到程序运行的局部性原理。
分配资源时选择buckets有如下两种策略,在创建RA(函数RA_Create)时可以指定采用哪种策略:
- best fit:选择接近请求大小且满足请求大小的空闲BT,但是性能没有assured fit好,花费的时间可能更长。具体算法:对请求大小size计算对数,对数的底为2,得到结果index_low(对数结果向下取整),在buckets[index_low]指向的链表中顺序查找,请求分配资源时还会有基地址对齐要求,当BT的基地址向后偏移到满足对齐要求后,且BT剩余的大小大于等于请求大小则该BT满足要求,返回该BT,并退出查找。因为有对齐要求,所以buckets[index_low]未必能找到满足要求的BT。当buckets[index_low]未找到时,则查找buckets[index_low+1]的链表,直到查找完buckets[39]。注意,buckets指向的链表可能为空。
- assured fit:RA默认策略。从能满足请求大小和对齐要求的空闲BT查找,相对于best fit,拥有更好的性能。对(请求大小size+对齐要求alignment-1)计算对数,对数的底为2,得到结果index_high(对数结果向下取整)。在buckets[index_high+1]指向的链表中顺序查找,当BT的基地址向后偏移到满足对齐要求后,且BT剩余的大小大于等于请求大小则该BT满足要求,返回该BT,并退出查找,这里从index_high+1开始查找是因为取对数是向下取整(如果对数不是整数),所以buckets[index_high]不一定能满足要求,而index_high+1就一定能满足要求,如果buckets[index_high+1]为NULL,则从buckets[index_high+2]查找,直到buckets[39];如果都为NULL,则buckets向下查找,先查找buckets[index_high],如果该buckets指向的BT链表没有满足要求的,则查找buckets[index_high-1],直到buckets[index_low],根据上面best fit策略的分析,buckets[index_low]里面可能会有满足要求的BT,但是buckets[index_low-1]指向的BT链表肯定都太小不满足要求。
优缺点对比:assured fit比best fit能更快找到满足要求的BT,拥有更好的时间性能,但是它比best fit更先将更大块的内存split,造成大块连续内存更少,更容易出现分配大块连续内存失败的情况。
找到空闲BT后,会将该BT起始地址开始到对齐地址结束的内存split并根据大小放入相应的buckets(如果起始地址已经满足对齐要求则不需要split),然后从对齐地址开始直到满足请求大小处结束split并分配出去,最后分配出去的地址段后面可能还会有一部分空闲内存,该内存也会被split并根据大小放入相应的buckets。
如下图3、图4所示,当从空闲BT A分配内存时,因为地址对齐前段被split出了空闲BT A0,然后中段被split出BT A1并被分配出去,最后剩余后段空闲BT A2。被分配前BT A的is_leftmost和is_rightmost都为1,BT A与相邻结点资源不连续(相邻结点为BT A指针pNextSegment、pPrevSegment指向的结点),如果BT A与相邻结点资源连续且都为空闲,则必然会合并为更大的空闲资源。BT A被split前其成员变量is_leftmost和is_rightmost都为1,都split后低地址段BT A0的成员变量is_leftmost为1、is_rightmost为0,被分配出去的中段BT A1的成员变量is_leftmost和is_rightmost都为0,高段BT A2成员变量is_leftmost为0、is_rightmost为1。同时,BT A从链表buckests[Z]中移除,BT A split后根据BT A0、BT A2的大小分别插入链表buckets[X]、buckets[Y],而被分配出去的BT A1因为不是空闲资源,所以不需要插入buckets链表。空闲资源插入buckets链表时有如下两种策略,在创建R(函数RA_Create)时可以指定采用哪种策略:
- fast:默认策略,直接插入链表首节点,如下图4所示。
- optimal:链表根据BT资源大小排序,头结点资源最小,尾结点资源最大,插入链表时需要根据BT资源大小插入合适位置,保持排序。
fast策略相对于optimal策略速度更快,但是当分配资源时,遍历buckets链表返回第一个满足条件的BT。如果buckets链表排序,则返回的是该链表中满足条件的BT当中资源大小最小的,这样可以降低碎片(资源大小更小的意味着更低概率被split),且减少更大的BT被分配,从而保证后面分配更大连续资源时能被满足。
最后,它们都需要按照低地址到高地址的顺序插入RA的成员变量指针pHeadSegment指向的链表,且以BT A1的基地址作为key、BT A1的结构体_BT_地址为value插入到RA的成员变量pSegmentHash指向的hash表,这样做的目的是释放资源函数RA_Free传入的参数为RA的结构体_RA_ARENA_地址和需要释放资源的基地址,有了该hash表就可以快速通过基地址找到结构体_BT_的地址,从而进行释放操作,只有已分配的资源才会加入该hash表。
图3 free BT A被split前
图4 free BT A被split后
向RA释放资源
向RA释放资源就是申请资源的逆向过程,主要步骤如下:
- 以要释放资源的基地址作为key,在RA的成员变量pSegmentHash指向的hash表中找到BT,并将其从该hash表中移除。
- 如果该BT的成员变量is_leftmost为0,且其前面相邻BT空闲(BT成员变量pPrevSegment指向的结点,简称left BT),根据上面分析它们资源肯定连续,则将两个BT合并为一个空闲BT。具体步骤是,将left BT从buckets链表中移除,RA成员变量pHeadSegment指向的链表中将两个相邻BT合并为一个新BT,新BT的基地址为left BT的资源的基地址,新BT的成员变量is_leftmost设置为left BT的成员变量is_leftmost值,新BT的资源大小为两个BT资源大小总和。如果该BT的成员变量is_leftmost为1,则表明该BT为连续资源的起始段,不能和前面相邻节点合并。
- 同理,如果该BT的成员变量is_rightmost为0,且其后面相邻BT空闲(BT成员变量pNextSegment指向的结点),则进行合并;否则不合并。
- 如果合并后的BT或待释放的BT的成员变量is_leftmost和is_rightmost都为1,且该BT是import的(BT成员变量free_import为真),则将该BT从RA成员变量pHeadSegment指向的链表中删除,并将import资源释放。import流程见下面详解。
- 如果上面的条件不满足,则不走import资源释放流程。根据合并后的BT或待释放的BT的资源大小插入当前RA的相应buckets链表,需要根据fast和optimal策略插入合适位置。
上面第4步和第5步是互斥的,只能根据条件走其中一个流程。
从其他RA import资源
前面分析了从RA申请资源,如果RA资源不足分配失败,则调用该RA结构体_RA_ARENA_成员变量pImportAlloc指向的函数,该函数指针在函数RA_Create创建RA时设置,如果调用RA_Create时对应传入的参数为NULL,则函数RA_Create会设置pImportAlloc为一个直接返回失败的函数。在系统中创建device memory、sync checkpoint和sync prim对应的RA时会设置自己的import函数,创建其他RA时不会设置。下面以device memory为例说明import流程。
应用启动时,应用层(非内核)会创建多个device memory heap,包括SVM heap、General heap、USC Code heap等,随后应用就可以从这些heap中分配内存。这个heap对应的数据结构如下:
struct DEVMEM_HEAP_TAG
{
/* Name of heap - for debug and lookup purposes. */
IMG_CHAR *pszName;
/* Number of live imports in the heap */
ATOMIC_T hImportCount;
/* Base address and size of heap, required by clients due to some
* requesters not being full range
*/
IMG_DEV_VIRTADDR sBaseAddress;
DEVMEM_SIZE_T uiSize;
DEVMEM_SIZE_T uiReservedRegionSize; /* uiReservedRegionLength in DEVMEM_HEAP_BLUEPRINT */
/* The heap manager, describing if the space is managed by the user, an RA,
* kernel or combination */
IMG_UINT32 ui32HeapManagerFlags;
/* This RA is for managing sub-allocations within the imports (PMRs)
* within the heap's virtual space. RA only used in DevmemSubAllocate()
* to track sub-allocated buffers.
*
* Resource Span - a PMR import added when the RA calls the
* imp_alloc CB (SubAllocImportAlloc) which returns the
* PMR import and size (span length).
* Resource - an allocation/buffer i.e. a MemDesc. Resource size represents
* the size of the sub-allocation.
*/
RA_ARENA *psSubAllocRA;
IMG_CHAR *pszSubAllocRAName;
/* The psQuantizedVMRA is for the coarse allocation (PMRs) of virtual
* space from the heap.
*
* Resource Span - the heap's VM space from base to base+length,
* only one is added at heap creation.
* Resource - a PMR import associated with the heap. Dynamic number
* as memory is allocated/freed from or mapped/unmapped to
* the heap. Resource size follows PMR logical size.
*/
RA_ARENA *psQuantizedVMRA;
IMG_CHAR *pszQuantizedVMRAName;
/* We also need to store a copy of the quantum size in order to feed
* this down to the server.
*/
IMG_UINT32 uiLog2Quantum;
/* Store a copy of the minimum import alignment */
IMG_UINT32 uiLog2ImportAlignment;
/* The parent memory context for this heap */
struct DEVMEM_CONTEXT_TAG *psCtx;
/* Lock to protect this structure */
POS_LOCK hLock;
/* Each "DEVMEM_HEAP" has a counterpart in the server, which is
* responsible for handling the mapping into device MMU.
* We have a handle to that here.
*/
IMG_HANDLE hDevMemServerHeap;
/* This heap is fully allocated and premapped into the device address space.
* Used in virtualisation for firmware heaps of Guest and optionally Host
* drivers. */
IMG_BOOL bPremapped;
};
typedef struct DEVMEM_HEAP_TAG DEVMEM_HEAP;
- 成员变量psSubAllocRA为一个RA,该RA设置pImportAlloc为SubAllocImportAlloc、设置pImportFree为SubAllocImportFree,但是并未向该RA中添加资源。
- 成员变量psQuantizedVMRA为一个RA,该RA未设置pImportAlloc、pImportFree(RA_Create设置直接返回失败的函数),创建RA后会调用RA_Add向该RA添加heap对应的设备虚拟地址段资源。
创建好后的device memory heap如下图5所示,可见device memory heap中并没有物理内存资源。
图5 import资源前
当应用从DEVMEM_HEAP分配内存时,GPU UMD驱动是从psSubAllocRA指向的RA中分配(对应函数RA_Alloc),而起始的时候psSubAllocRA中并没有资源,从psSubAllocRA分配失败后,走import流程如下:
- 分配内存时传下的参数flag有从哪个physical heap分配内存的信息,从该physical heap对应的RA中分配物理内存,分配后如下图6所示。physical heap可以是LMA即GDDR,也可以是UMA即host RAM。这里假设是LMA,如果physical heap是LMA,则结构体PHYS_HEAP的成员指针变量pvImplData指向结构体PHYSMEM_LMA_DATA,构体PHYSMEM_LMA_DATA中成员psRA指向RA;如果physical heap是UMA,则成员指针变量pvImplData为NULL。这里假设physical heap为LMA。如果flag中未指定physical heap则使用default heap(GPU_LOCAL,如果flag中指定CPU mappable且未指定GPU cache coherent)或GPU_PRIVATE(如果flag中未指定CPU mappable)或GPU_COHERENT(如果flag中指定CPU mappable并指定GPU cache coherent)。
- 从DEVMEM_HEAP结构体成员变量psQuantizedVMRA指向的RA中分配与物理内存同样大小的虚拟内存。
- 建立页表,将分配的物理内存和虚拟内存进行映射。
- 最后将import的资源添加到psSubAllocRA指向的RA中。具体步骤如下:
- 创建BT,将BT资源基地址设置为从psQuantizedVMRA中分配的虚拟内存基地址,BT资源大小等于物理内存/虚拟内存大小,BT成员变量uFlags为分配内存时传入的flag。
- 以BT成员uFlags为key创建splay tree结点,并将结点插入splay tree。后面再import资源的时候如果BT成员变量uFlags的值等于splay tree中某结点的key,则不需要再创建splay tree结点,直接将新创建的BT插入splay tree该结点的free table即可。
- 将BT插入psSubAllocRA成员变量pHeadSegment指向的链表,成为该链表首节点。
- 将BT插入创建的splay tree结点的free table(即buckets)。
- 设置BT的成员free_import为1。
注意,import后,分配的物理内存相关的BT还在PHYS_HEAP对应的RA的成员变量pHeadSegment链表中,分配的虚拟内存相关的BT还在psQuantizedVMRA对应的RA的成员变量pHeadSegment链表中,如下图6所示。import成功后,再次从psSubAllocRA分配资源,流程和前面讲的一样,应用就可以对分配的内存进行操作了;如果import失败,则应用分配内存失败。上面第一次从psSubAllocRA分配失败,然后import资源,最后再从psSubAllocRA分配成功只需要调用一次RA_Alloc函数完成。RA_Alloc如果涉及到import,则会产生递归调用,代码大致流程如下:
RA_Alloc(psSubAllocRA, ...) 从psSubAllocRA分配资源
->_AttemptAllocAligned 从psSubAllocRA的splay tree中分配资源,分配成功则返回,分配失败则继续向下运行
->_AttemptImportSpanAlloc
->pArena->pImportAlloc 即调用函数 SubAllocImportAlloc
.... 中间省略
-> RA_Alloc 从PHYS_HEAP对应的RA中分配物理
->DevmemImportStructDevMap
-> RA_Alloc(psQuantizedVMRA, ...) 从psQuantizedVMRA分配设备虚拟内存
-> BridgeDevmmeIntMapPMR 将设备虚拟内存映射到物理内存,因为是在应用层调用RA_Alloc(psSubAllocRA...),需要陷入到内核由KMD完成映射
->_InsertResourceSpan 将import的资源添加到psSubAllocRA
->_AttemptAllocAligned 再次从psSubAllocRA的splay tree中分配资源,如果再分配失败,则整个分配流程失败
图 6 import资源后
import的资源大小大于等于请求分配的内存大小。在调用RA的pImportAlloc指向的函数前,需要计算import资源大小,需要考虑如下即个点:
- 请求资源大小及资源基地址对其,它们都是由应用传下来的。
- 超分配倍数,也是由应用传下来的,根据第1步计算的资源大小乘以超分配倍数得到一个值。这样做的目的是可以分配更大的资源,下一次应用申请资源时就不用再import资源,提高分配资源性能。
- 如果第2步的值不是psSubAllocRA成员uQuantum的倍数,则对第2步的值向上取整成为uQuantum的倍数,uQuantum是调用函数RA_Create创建RA时设置的,该值不是由应用传下来的,是根据系统page size(一般为4KB)或其他方式设置的。所以当应用分配4字节内存时,很可能import 4KB内存。
DDK(UMD和KMD)调用RA_Create创建RA,并调用RA_Add向RA中添加资源时,传入的flag都为0,所以它们的splay tree都只有一个结点且key值为0。后面调用RA_Alloc分配资源时,只有向DEVMEM_HEAP的成员psSubAllocRA调用RA_Alloc分配资源时,传入的flag可以不为0,其他所有调用RA_Alloc的地方传入的flag都为0。当调用RA_Alloc向DEVMEM_HEAP成员变量psSubAllocRA分配资源时,传入的flag可以不为0,但是import时调用RA_Alloc从PHYS_HEAP和psQuantizedVMRA分配资源时,传入的flag都为0,不然分配会失败,因为PHYS_HEAP和psQuantizedVMRA的splay tree都只有一个key为0的结点,import成功后psSubAllocRA的splay tree的结点的key值就等于RA_Alloc从psSubAllocRA分配资源时传入的flag值。sync prim和sync checkpoint也有import流程,但是调用RA_Alloc从它们的psSubAllocRA分配资源时,传入的flag都为0,所以import后psSubAllocRA的splay tree的结点的key值也为0。
释放import的资源
对于import的资源,释放的步骤与章节《向RA释放资源》的前面3步一样,第4步如果合并后的BT或待释放的BT的成员变量is_leftmost和is_rightmost都为1,且该BT是import的(BT成员变量free_import为真),则进行如下处理:
- 将该BT从RA(psSubAllocRA)成员变量pHeadSegment指向的链表中删除。
- 解除之前import的设备虚拟内存和设备物理内存的映射。
- 释放import设备虚拟内存到psQuantizedVMRA对应的RA。
- 释放import的物理内存到PHYS_HEAP的RA。
完成以上步骤后,就回到了图5所示的import前的状态。