内存管理

BO 管理

TTM 管理(放置、驱逐等)XE 中的所有 BO。

BO 创建

创建可供 GPU 使用的内存块。创建时传入放置规则(sysmem 或 vram 区域)。TTM 处理 BO 的放置,并可以触发其他 BO 的驱逐,以便为新 BO 腾出空间。

内核 BO

内核 BO 作为驱动程序加载的一部分创建(例如,uC 固件镜像、GuC ADS 等),或者作为需要内核 BO 的用户操作的一部分创建(例如,引擎状态、页表的内存等)。这些 BO 通常映射在 GGTT 中(除了页表的内存之外的任何内核 BO 都在 GGTT 中),被锁定(运行时无法移动或驱逐),具有 vmap(XE 可以通过 xe_map 层访问内存)并且具有连续的物理内存。

以下是内核 BO 被锁定和连续的原因的更多详细信息。

用户 BO

用户 BO 通过 DRM_IOCTL_XE_GEM_CREATE IOCTL 创建。创建后,可以 mmap BO(通过 DRM_IOCTL_XE_GEM_MMAP_OFFSET)以供用户访问,并且可以绑定 BO 以供 GPU 访问(通过 DRM_IOCTL_XE_VM_BIND)。所有用户 BO 都是可驱逐的,并且用户 BO 永远不会被 XE 锁定。后备存储的分配可以从创建时延迟到首次使用时,即 mmap、bind 或 pagefault。

私有 BO

私有 BO 是使用传递到 create IOCTL 的有效 VM 参数创建的用户 BO。如果 BO 是私有的,则无法通过 prime FD 导出,并且只能在绑定到的 VM 中为 BO 创建映射。最后,BO dma-resv 插槽/锁指向 VM 的 dma-resv 插槽/锁(VM 的所有私有 BO 共享通用的 dma-resv 插槽/锁)。

外部 BO

外部 BO 是使用传递到 create IOCTL 的 NULL VM 参数创建的用户 BO。外部 BO 可以通过 prime FD 与不同的 UMD/设备共享,并且 BO 可以映射到多个 VM 中。外部 BO 具有自己唯一的 dma-resv 插槽/锁。外部 BO 将位于具有 BO 映射的所有 VM 的数组中。这允许 VM 查找和锁定 VM 中映射的所有外部 BO(如果需要)。

BO 放置

创建用户 BO 时,会传递有效放置的掩码,指示哪些内存区域被认为是有效的。

内存区域信息可通过查询 uAPI 获得(TODO:添加链接)。

BO 验证

BO 验证 (ttm_bo_validate) 是指确保 BO 具有有效的放置。如果 BO 被交换到临时存储,则验证调用将触发移动回到有效的(GPU 可以访问 BO 的位置)放置。BO 的验证可能会驱逐其他 BO,以便为要验证的 BO 腾出空间。

BO 驱逐 / 移动

所有驱逐(或者换句话说,将 BO 从一个内存位置移动到另一个内存位置)都通过 TTM 进行路由,并回调到 XE。

运行时驱逐

运行时驱逐是指在正常操作期间,TTM 决定需要移动 BO 的情况。通常,这是因为 TTM 需要为另一个 BO 腾出空间,并且被驱逐的 BO 是 LRU 列表中第一个未锁定的 BO。

一个例子是只能放置在 VRAM 中的新 BO,但 VRAM 中没有空间。可能有多个具有 sysmem 和 VRAM 放置规则的 BO 当前驻留在 VRAM 中,TTM 将触发移动这些 BO 中的一个(或多个),直到 VRAM 中有空间放置新的 BO。被驱逐的 BO 有效,但在再次使用 BO 之前仍然需要新的绑定(执行或计算模式重新绑定 worker)。

另一个例子是,TTM 找不到另一个具有有效放置的 BO 来驱逐。在这种情况下,TTM 会将一个(或多个)未锁定的 BO 驱逐到临时不可达(无效)的放置。被驱逐的 BO 无效,并且在下次使用之前需要移动到有效的放置并重新绑定。

在这两种情况下,这些 BO 的移动都安排在 BO 的 dma-resv 插槽中的栅栏之后。

WW 锁定尝试确保如果 2 个 VM 使用 51% 的内存,则两个 VM 都会取得进展。

运行时驱逐使用每个 GT 迁移引擎(TODO:链接到迁移引擎文档)来执行从一个位置到另一个位置的 GPU memcpy。

运行时驱逐后重新绑定

当 BO 被移动时,BO 的每个映射 (VMA) 都需要在再次使用 BO 之前重新绑定。当 BO 被移动时,每个 VMA 都会添加到其 VM 的已驱逐列表中。这是安全的,因为 VM 锁定结构(TODO:链接到 VM 锁定文档)。在下次使用 VM 时(执行或计算模式重新绑定 worker),会检查已驱逐的 VMA 列表并触发重新绑定。在发生错误的 VM 的情况下,重新绑定在页面错误处理程序中完成。

VRAM 的挂起/恢复驱逐

在设备挂起/恢复期间,VRAM 可能会失去电源,这意味着 VRAM 内存的内容会被清除。因此,为了保存 VRAM 中存在的 BO 的内容,必须在挂起时将它们移动到 sysmem。

一个简单的 TTM 调用 (ttm_resource_manager_evict_all) 可以将所有非锁定(用户)BO 移动到 sysmem。需要手动驱逐锁定的外部 BO,使用一个简单的循环 + xe_bo_evict 调用。内核 BO 会稍微复杂一些。

一些内核 BO 被 GT 迁移引擎用于执行移动,因此我们无法通过 GT 迁移引擎移动所有 BO。为了简化起见,在挂起或恢复时,使用 TTM memcpy (CPU) 移动任何内核(锁定)BO。

一些内核 BO 需要恢复到完全相同的物理位置。TTM 使这非常容易,但需要注意的是,内存必须是连续的。同样为了简化起见,我们强制所有内核(锁定)BO 都是连续的,并恢复到相同的物理位置。

VRAM 中锁定的外部 BO 在恢复时通过 GPU 恢复。

挂起/恢复后重新绑定

大多数内核 BO 具有 GGTT 映射,必须在恢复过程中恢复。所有用户 BO 在验证后在下次使用时重新绑定。

未来工作

精简通过 TTM memcpy 在挂起/恢复时保存/恢复的 BO 列表。我们真正需要通过 TTM memcpy 保存/恢复的是 GuC 加载所需的内存以及 GT 迁移引擎运行所需的内存。

不要要求内核 BO 在物理内存中是连续的/在恢复时恢复到相同的物理地址。在所有可能性中,唯一需要恢复到相同物理地址的内存是用于页表的内存。所有这些内存一次分配 1 页,因此不需要连续要求。如果内核 BO 不是连续的,则需要在 vmap 代码上进行一些工作。

使一些内核 BO 可驱逐而不是锁定。一个例子是引擎状态,在所有可能性中,如果这些 BO 的 dma-slots 被正确使用而不是锁定,我们可以根据需要安全地驱逐 + 重新绑定这些 BO。

某些内核 BO 不需要在恢复时恢复(例如,GuC ADS,因为它在恢复时重新填充),添加一个标志来标记此类对象为不保存/恢复。

GGTT

Xe GGTT 实现了对全局虚拟地址空间的支持,该空间用于特权(即内核模式)进程可访问的资源,并且不绑定到特定的用户级进程。例如,图形微控制器 (GuC) 和显示引擎(如果存在)利用此全局地址空间。

全局 GTT (GGTT) 将全局虚拟地址转换为可由 HW 访问的物理地址。GGTT 是一个扁平的单级表。

Xe 实现了 GGTT 的简化版本,专门管理从 Write Once Protected Content Memory (WOPCM) 布局到预定义的 GUC_GGTT_TOP 的特定范围。这种方法避免了与 GuC(图形微控制器)硬件限制相关的复杂性。GuC 地址空间在 GGTT 的两端都受到限制,因为 GuC shim HW 将对这些地址的访问重定向到其他 HW 区域,而不是通过 GGTT。在底部,GuC 无法访问 WOPCM 大小以下的偏移量,而在顶部,限制固定为 GUC_GGTT_TOP。为了保持简单,我们不检查每个对象是否被 GuC 访问,而是直接从分配器中排除这些区域。此外,为了简化驱动程序加载,我们在此逻辑中使用最大 WOPCM 大小而不是编程的大小,因此我们不需要等到实际要编程的大小确定(这需要 FW 获取)之后才能初始化 GGTT。这些简化可能会浪费 GGTT 中的空间(取决于平台,约为 20-25 MB),但我们可以忍受这一点。另一个好处是 GuC 引导 ROM 无法访问 WOPCM 最大大小以下的任何内容,因此引导 ROM 需要访问的任何内容(例如 RSA 密钥)都需要放置在 GGTT 中 WOPCM 最大大小之上。在 WOPCM 最大大小之上启动 GGTT 分配可以免费为我们提供正确的放置。

GGTT 内部 API

struct xe_ggtt

主 GGTT 结构

定义:

struct xe_ggtt {
    struct xe_tile *tile;
    u64 size;
#define XE_GGTT_FLAGS_64K BIT(0);
    unsigned int flags;
    struct xe_bo *scratch;
    struct mutex lock;
    u64 __iomem *gsm;
    const struct xe_ggtt_pt_ops *pt_ops;
    struct drm_mm mm;
    unsigned int access_count;
    struct workqueue_struct *wq;
};

成员

tile

指向此 GGTT 所属的 tile 的后向指针

size

此 GGTT 的总大小

flags

此 GGTT 的标志 可接受的标志:- XE_GGTT_FLAGS_64K - 如果 PTE 大小为 64K。否则,常规大小为 4K。

scratch

用作暂存页面的内部对象分配

lock

用于保护 GGTT 数据的互斥锁

gsm

指向位于 GSM 中用于轻松 PTE 操作的转换表实际位置的 iomem 指针

pt_ops

每个平台的页表操作

mm

用于管理各个 GGTT 分配的内存管理器

access_count

计算 GGTT 写入次数

wq

用于处理节点删除的专用无序工作队列

描述

一般来说,每个 tile 可以包含其自己的全局图形转换表 (GGTT) 实例。

struct xe_ggtt_node

GGTT 中的一个节点。

定义:

struct xe_ggtt_node {
    struct xe_ggtt *ggtt;
    struct drm_mm_node base;
    struct work_struct delayed_removal_work;
    bool invalidate_on_remove;
};

成员

ggtt

指向将插入此区域的 xe_ggtt 的后向指针

base

一个 drm_mm_node

delayed_removal_work

延迟删除的工作结构

invalidate_on_remove

如果需要在删除时失效

描述

此结构需要在使用 xe_ggtt_node_init()(仅一次)初始化后,才能进行任何节点插入、保留或“气球化”。然后,它将由 xe_ggtt_node_remove() 或 xe_ggtt_node_deballoon() 最终确定。

struct xe_ggtt_pt_ops

GGTT 页表操作 这可能会因平台而异。

定义:

struct xe_ggtt_pt_ops {
    u64 (*pte_encode_bo)(struct xe_bo *bo, u64 bo_offset, u16 pat_index);
    void (*ggtt_set_pte)(struct xe_ggtt *ggtt, u64 addr, u64 pte);
};

成员

pte_encode_bo

对给定 BO 的 PTE 地址进行编码

ggtt_set_pte

直接写入 GGTT 的 PTE

int xe_ggtt_init_early(struct xe_ggtt *ggtt)

早期 GGTT 初始化

参数

struct xe_ggtt *ggtt

要初始化的 xe_ggtt

描述

它允许创建 GuC 可用的新映射。HW 引擎不可使用映射,因为它还没有完成暂存和初始清除。这将在常规的非早期 GGTT 初始化中发生。

返回

成功时返回 0,失败时返回负错误代码。

void xe_ggtt_node_remove(struct xe_ggtt_node *node, bool invalidate)

从 GGTT 中删除 xe_ggtt_node

参数

struct xe_ggtt_node *node

要删除的 xe_ggtt_node

bool invalidate

如果节点需要在删除时失效

int xe_ggtt_init(struct xe_ggtt *ggtt)

常规的非早期 GGTT 初始化

参数

struct xe_ggtt *ggtt

要初始化的 xe_ggtt

返回

成功时返回 0,失败时返回负错误代码。

int xe_ggtt_node_insert_balloon(struct xe_ggtt_node *node, u64 start, u64 end)

阻止分配指定的 GGTT 地址

参数

struct xe_ggtt_node *node

用于保存保留 GGTT 节点的 xe_ggtt_node

u64 start

保留区域的起始 GGTT 地址

u64 end

保留区域的结束 GGTT 地址

描述

使用 xe_ggtt_node_remove_balloon() 释放保留的 GGTT 节点。

返回

成功时返回 0,失败时返回负错误代码。

void xe_ggtt_node_remove_balloon(struct xe_ggtt_node *node)

释放保留的 GGTT 区域

参数

struct xe_ggtt_node *node

带有保留 GGTT 区域的 xe_ggtt_node

描述

有关详细信息,请参见 xe_ggtt_node_insert_balloon()

int xe_ggtt_node_insert_locked(struct xe_ggtt_node *node, u32 size, u32 align, u32 mm_flags)

锁定的版本,用于将 xe_ggtt_node 插入到 GGTT 中

参数

struct xe_ggtt_node *node

要插入的 xe_ggtt_node

u32 size

节点的大小

u32 align

节点的对齐约束

u32 mm_flags

用于控制节点行为的标志

描述

不能在没有首先调用 xe_ggtt_init() 的情况下调用它一次。用于已经获取 ggtt->lock 的情况。

返回

成功时返回 0,失败时返回负错误代码。

int xe_ggtt_node_insert(struct xe_ggtt_node *node, u32 size, u32 align)

xe_ggtt_node 插入到 GGTT 中

参数

struct xe_ggtt_node *node

要插入的 xe_ggtt_node

u32 size

节点的大小

u32 align

节点的对齐约束

描述

不能在没有首先调用 xe_ggtt_init() 的情况下调用它一次。

返回

成功时返回 0,失败时返回负错误代码。

struct xe_ggtt_node *xe_ggtt_node_init(struct xe_ggtt *ggtt)

初始化 xe_ggtt_node 结构体

参数

struct xe_ggtt *ggtt

新节点将要插入/保留的 xe_ggtt

描述

此函数将分配 xe_ggtt_node 结构体并返回其指针。该结构体将在通过 xe_ggtt_node_remove()xe_ggtt_node_remove_balloon() 移除节点后释放。 分配了 xe_ggtt_node 结构体并不意味着该节点已在 GGTT 中分配。 只有 xe_ggtt_node_insert(), xe_ggtt_node_insert_locked(), xe_ggtt_node_insert_balloon() 才能确保节点插入或保留在 GGTT 中。

返回

成功时返回指向 xe_ggtt_node 结构体的指针。 否则返回 ERR_PTR。

void xe_ggtt_node_fini(struct xe_ggtt_node *node)

强制结束 xe_ggtt_node 结构体

参数

struct xe_ggtt_node *node

要释放的 xe_ggtt_node

描述

如果 xe_ggtt_node_insert(), xe_ggtt_node_insert_locked(), 或 xe_ggtt_node_insert_balloon() 中的任何一个出现问题; 并且这个 **node** 不会被重用,那么需要调用此函数来释放 xe_ggtt_node 结构体

bool xe_ggtt_node_allocated(const struct xe_ggtt_node *node)

检查节点是否已在 GGTT 中分配

参数

const struct xe_ggtt_node *node

要检查的 xe_ggtt_node

返回

如果已分配则为 True,否则为 False。

void xe_ggtt_map_bo(struct xe_ggtt *ggtt, struct xe_bo *bo)

将 BO 映射到 GGTT 中

参数

struct xe_ggtt *ggtt

节点将被映射到的 xe_ggtt

struct xe_bo *bo

要映射的 xe_bo

int xe_ggtt_insert_bo_at(struct xe_ggtt *ggtt, struct xe_bo *bo, u64 start, u64 end)

在特定的 GGTT 空间插入 BO

参数

struct xe_ggtt *ggtt

bo 将被插入到的 xe_ggtt

struct xe_bo *bo

要插入的 xe_bo

u64 start

插入的起始地址

u64 end

插入范围的结束地址

返回

成功时返回 0,失败时返回负错误代码。

int xe_ggtt_insert_bo(struct xe_ggtt *ggtt, struct xe_bo *bo)

将 BO 插入到 GGTT 中

参数

struct xe_ggtt *ggtt

bo 将被插入到的 xe_ggtt

struct xe_bo *bo

要插入的 xe_bo

返回

成功时返回 0,失败时返回负错误代码。

void xe_ggtt_remove_bo(struct xe_ggtt *ggtt, struct xe_bo *bo)

从 GGTT 中移除 BO

参数

struct xe_ggtt *ggtt

节点将被移除的 xe_ggtt

struct xe_bo *bo

要移除的 xe_bo

u64 xe_ggtt_largest_hole(struct xe_ggtt *ggtt, u64 alignment, u64 *spare)

最大的 GGTT 空洞

参数

struct xe_ggtt *ggtt

要检查的 xe_ggtt

u64 alignment

最小对齐

u64 *spare

如果非 NULL:输入:期望保留的内存大小 / 输出:调整后的可能保留空间

返回

最大的连续 GGTT 区域的大小

void xe_ggtt_assign(const struct xe_ggtt_node *node, u16 vfid)

将 GGTT 区域分配给 VF

参数

const struct xe_ggtt_node *node

要更新的 xe_ggtt_node

u16 vfid

VF 标识符

描述

此函数由 PF 驱动程序使用,将 GGTT 区域分配给 VF。除了 PTE 的 VFID 位 11:2 之外,PRESENT 位 0 也被设置为,因为在某些平台上 VF 也无法修改它。

int xe_ggtt_dump(struct xe_ggtt *ggtt, struct drm_printer *p)

转储 GGTT 以进行调试

参数

struct xe_ggtt *ggtt

要转储的 xe_ggtt

struct drm_printer *p

用于转储信息的 drm_mm_printer 辅助句柄

返回

成功时返回 0,失败时返回负错误代码。

u64 xe_ggtt_print_holes(struct xe_ggtt *ggtt, u64 alignment, struct drm_printer *p)

打印空洞

参数

struct xe_ggtt *ggtt

要检查的 xe_ggtt

u64 alignment

最小对齐

struct drm_printer *p

drm_printer

描述

打印可用的 GGTT 范围,并返回总可用大小。

返回

总可用大小。

页表构建

下面,我们使用“页表”一词来表示页目录(包含指向较低级别页目录或页表的指针)以及仅包含指向内存页面的页表条目的第 0 级页表。

当在一个已存在的页表树中插入地址范围时,通常会有一组与其它地址范围共享的页表,以及一组此地址范围私有的页表。每层最多可以有两个共享页表,并且这些页表不能立即更新,因为这些页表的条目可能仍在被 GPU 用于其它映射。因此,当将条目插入到这些页表中时,我们改为通过将插入数据添加到 struct xe_vm_pgtable_update 结构体中来暂存这些插入。然后,在单独的提交步骤中添加此数据(CPU 的子树和 GPU 的页表条目)。CPU 数据在 vm 锁、对象锁以及 userptr 的读取模式下的 notifier 锁下提交。GPU 异步数据由 GPU 或 CPU 在满足相关依赖性后提交。对于非共享页表(实际上,对于暂存时不存在的共享页表),我们直接添加数据,而无需特殊的更新结构。页表树的这个私有部分将与 vm 页表树断开连接,直到数据在提交阶段提交到 vm 树的共享页表。