内存管理¶
BO 管理¶
TTM 管理(放置、驱逐等...)XE 中的所有 BO。
BO 创建¶
创建一个可供 GPU 使用的内存块。在创建时传入放置规则(系统内存或 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 或缺页。
私有 BO¶
私有 BO 是使用传递到创建 IOCTL 中的有效 VM 参数创建的用户 BO。如果 BO 是私有的,则无法通过 prime FD 导出,并且只能为与其绑定的 VM 内的 BO 创建映射。最后,BO dma-resv 插槽/锁定指向 VM 的 dma-resv 插槽/锁定(VM 的所有私有 BO 共享公共 dma-resv 插槽/锁定)。
外部 BO¶
外部 BO 是使用传递到创建 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 被交换到临时存储,则验证调用将触发 BO 移回有效位置(GPU 可以访问 BO 的位置)。BO 的验证可能会驱逐其他 BO,以便为正在验证的 BO 腾出空间。
BO 驱逐/移动¶
所有驱逐(换句话说,将 BO 从一个内存位置移动到另一个内存位置)都通过 TTM 进行路由,并回调到 XE。
运行时驱逐¶
运行时驱逐是指在正常操作期间,TTM 决定需要移动 BO。通常,这是因为 TTM 需要为另一个 BO 腾出空间,而被驱逐的 BO 是 LRU 列表中第一个未锁定的 BO。
一个示例是只能放置在 VRAM 中的新 BO,但 VRAM 中没有空间。可能存在多个具有系统内存和 VRAM 放置规则的 BO 当前位于 VRAM 中,TTM 将触发移动这些 BO 中的一个(或多个),直到 VRAM 中有足够的空间来放置新的 BO。被驱逐的 BO 是有效的,但在再次使用之前(执行或计算模式重新绑定工作程序)仍然需要新的绑定。
另一个示例是,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 时(执行或计算模式重新绑定工作程序),将检查驱逐的 VMA 列表并触发重新绑定。在出现故障 VM 的情况下,重新绑定在缺页处理程序中完成。
VRAM 的暂停/恢复驱逐¶
在设备暂停/恢复期间,VRAM 可能会失去电源,这意味着 VRAM 内存的内容会被清除。因此,必须将暂停时 VRAM 中存在的 BO 移动到系统内存,以便保存其内容。
一个简单的 TTM 调用 (ttm_resource_manager_evict_all) 可以将所有非固定(用户)BO 移动到系统内存。需要使用一个简单的循环 + xe_bo_evict 调用手动驱逐固定的外部 BO。内核 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 插槽被正确使用而不是固定,我们可以安全地根据需要驱逐 + 重新绑定这些 BO。
某些内核 BO 不需要在恢复时恢复(例如,GuC ADS,因为它在恢复时会被重新填充),添加标志以将此类对象标记为不保存/恢复。
GGTT¶
Xe GGTT 实现了对全局虚拟地址空间的支持,该空间用于特权(即内核模式)进程可以访问的资源,并且不绑定到特定的用户级进程。例如,图形微控制器 (GuC) 和显示引擎(如果存在)使用此全局地址空间。
全局 GTT (GGTT) 将全局虚拟地址转换为可由 HW 访问的物理地址。GGTT 是一个扁平的单级表。
Xe 实现了一个简化版的 GGTT,专门管理从“一次写入保护内容内存 (WOPCM)”布局到预定义的 GUC_GGTT_TOP 的特定范围。这种方法避免了与 GuC(图形微控制器)硬件限制相关的复杂性。GuC 的地址空间在 GGTT 的两端都受到限制,因为 GuC shim 硬件会将对这些地址的访问重定向到其他硬件区域,而不是通过 GGTT。在下端,GuC 不能访问低于 WOPCM 大小的偏移量,而在上端,限制固定为 GUC_GGTT_TOP。为了保持简单,我们没有检查每个对象来确定它们是否被 GuC 访问,而是直接将这些区域从分配器中排除。此外,为了简化驱动程序加载,我们在此逻辑中使用最大 WOPCM 大小,而不是编程的大小,因此我们不需要等到确定实际编程大小(这需要固件获取)之后再初始化 GGTT。这些简化可能会浪费 GGTT 中的空间(根据平台的不同,约为 20-25 MB),但我们可以接受。这样做的好处是,GuC 引导 ROM 不能访问 WOPCM 最大大小以下的任何内容,因此引导 ROM 需要访问的任何内容(例如 RSA 密钥)都需要放置在 WOPCM 最大大小以上的 GGTT 中。从 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 中转换表实际位置的 iomem 指针,以便于 PTE 操作
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
参数
struct xe_ggtt *ggtt
要初始化的
xe_ggtt
描述
它允许创建 GuC 可以使用的新映射。这些映射不能被硬件引擎使用,因为它还没有 scratch 也没有进行初始清除。这将在常规的非早期 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_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 区域
-
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 中分配
参数
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,失败时返回负错误代码。
参数
struct xe_ggtt *ggtt
将移除节点的
xe_ggtt
struct xe_bo *bo
要移除的
xe_bo
参数
struct xe_ggtt *ggtt
将被检查的
xe_ggtt
u64 alignment
最小对齐
u64 *spare
如果不是 NULL:in:期望的要保留的内存大小 / out:调整后的可能的保留大小
返回
最大的连续 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
描述
打印可用的 GGTT 范围并返回可用总大小。
返回
可用总大小。
页表构建¶
下文中,我们使用术语“页表”来指代页目录(包含指向更低层级页目录或页表的指针)以及 0 级页表(仅包含指向内存页的页表项)。
当向已存在的页表树中插入地址范围时,通常会有一组页表与其他地址范围共享,以及一组页表是该地址范围私有的。共享页表在每个层级最多有两个,并且不能立即更新,因为这些页表的条目可能仍在被 GPU 用于其他映射。因此,当向这些页表插入条目时,我们通过将插入数据添加到 `struct xe_vm_pgtable_update` 结构中来暂存这些插入操作。这些数据(CPU 的子树和 GPU 的页表项)然后在单独的提交步骤中添加。CPU 数据在仍然处于 VM 锁、对象锁,以及对于 userptr,在读取模式下的通知器锁保护下提交。GPU 异步数据在满足相关依赖关系后由 GPU 或 CPU 提交。对于非共享页表(实际上,对于在暂存时不存在的共享页表),我们直接添加数据,而无需特殊的更新结构。页表树的这个私有部分将在数据提交到 VM 树的共享页表中的提交阶段之前,与 VM 页表树保持断开连接。