内存管理¶
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
参数
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_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:输入:期望保留的内存大小 / 输出:调整后的可能保留空间
返回
最大的连续 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 的读取模式下的 notifier 锁下提交。GPU 异步数据由 GPU 或 CPU 在满足相关依赖性后提交。对于非共享页表(实际上,对于暂存时不存在的共享页表),我们直接添加数据,而无需特殊的更新结构。页表树的这个私有部分将与 vm 页表树断开连接,直到数据在提交阶段提交到 vm 树的共享页表。