文章目录
- [1. 引言](#1. 引言)
- [2. 核心逻辑模块](#2. 核心逻辑模块)
- [3. 设备初始化](#3. 设备初始化)
- [4. 文件接口](#4. 文件接口)
- [5. 内存管理](#5. 内存管理)
- [6. Prime 与跨设备共享](#6. Prime 与跨设备共享)
- [7. 命令提交](#7. 命令提交)
- [8. 任务调度](#8. 任务调度)
- [9. 同步](#9. 同步)
- [10. 中断处理](#10. 中断处理)
- [11. 电源管理](#11. 电源管理)
- [12. 隔离、权限与虚拟化](#12. 隔离、权限与虚拟化)
- [13. 测试、调试与性能追踪](#13. 测试、调试与性能追踪)
- [14. 命令提交流程示例](#14. 命令提交流程示例)
- [15. 总结](#15. 总结)
1. 引言
在深入内核之前,我们先回顾一下 GPU 驱动在整个 Linux 图形栈中的位置。
在用户态,应用程序(如游戏、浏览器、桌面合成器)通过调用图形 API(如 OpenGL, Vulkan)来描述渲染任务。这些调用被用户态驱动(如 Mesa 3D, Vulkan ICD)接收,它们负责将 API 调用编译成硬件特定的命令缓冲(Command Buffers),并管理缓冲对象(Buffer Objects, BOs)。随后,这些命令和 BOs 通过 libdrm 库提交给内核。
在内核态,DRM(Direct Rendering Manager)子系统是所有 GPU 驱动的通用框架。特定硬件的 GPU 驱动(如 i915, amdgpu, panfrost 等)在 DRM 框架下运行。它们接收来自用户态的命令,管理 GPU 硬件资源(如显存、寄存器、命令队列),调度 GPU 执行任务,并通过中断和事件(如 dma_fence)将完成状态通知回用户态。
GPU 驱动架构图
硬件
内核空间
用户空间
应用(GL/VK)
Mesa3D
libdrm
DRM Core
GPU 驱动 (i915/amdgpu/panfrost/...)
内存管理器 (GEM/TTM)
调度/提交 (Scheduler/Submission)
同步 (dma_fence / timeline)
GPU 引擎 (Engines)
显存 / 系统内存 (VRAM / System Memory)
GPU 驱动的两面:KMS(显示)与 Render(渲染)
要对 GPU 驱动有全局认知,必须理解它包含两大独立但又协作的子系统:
- KMS (Kernel Mode Setting) :负责显示。它管理显示控制器、屏幕、接口(HDMI/DP)、平面(Planes)、CRTCs(时序控制器)等。KMS 的主要职责是"点亮屏幕"和"画面上屏"(Scanout)。它决定了哪个 Buffer Object (BO) 在哪个屏幕的哪个位置以何种分辨率和刷新率显示。
- Render (渲染) :负责计算。它管理 GPU 的 3D 引擎、计算引擎、命令提交、调度和内存(GEM/TTM)。它的职责是执行用户态提交的命令,生成最终的图像数据(即填充 BO)。
本文的重点是Render(渲染)侧的驱动框架。但我们必须记住,KMS 是渲染结果的最终消费者。渲染(Producer)和显示(Consumer)之间的同步,是 GPU 驱动中至关重要的环节。
2. 核心逻辑模块
一个现代 GPU 渲染驱动主要包含以下几个逻辑模块:
- 设备初始化 (Probe):在系统启动时被内核发现(通过 PCI, Platform Bus 等),申请硬件资源(MMIO BARs, IRQ, Clocks)。
- 文件接口 (File Operations) :提供
/dev/dri/cardX(KMS+Render)和/dev/dri/renderDN(仅 Render)设备节点,处理来自用户态的open,ioctl,mmap等系统调用。 - 内存管理 (GEM/TTM):核心模块。负责显存(VRAM)和系统内存(System Memory)中 BO 的分配、释放、映射(CPU 和 GPU 访问)、迁移和共享(dma-buf)。
- 命令提交 (Submission):接收来自用户态的命令流(Batch Buffers),进行验证、重定位(Relocations),并放入硬件队列。
- 调度 (Scheduler):管理多个 GPU 任务(Jobs)的执行顺序、优先级和抢占(Preemption)。
- 同步 (Synchronization) :使用
dma_fence和timeline确保操作的依赖顺序。例如,GPU 必须在渲染完成后,KMS 才能将该 BO 上屏显示。 - 中断处理 (IRQ):处理 GPU 的"命令完成"中断、"GPU Hang"错误中断、热插拔等事件。
- 电源管理 (Power Management):控制 GPU 的时钟频率、电压和运行时挂起/恢复(Runtime PM)。
- 调试 (Debugfs / Trace):提供 debugfs 接口和 tracepoints,用于观测驱动内部状态和性能分析。
模块交互流程图
异步完成与同步
命令执行路径
用户空间接口
驱动初始化阶段
probe() / 驱动初始化
drm_dev_register
/dev/dri/cardX 文件节点
ioctl 接口处理
GEM/TTM 内存管理
Execbuffer / 命令提交
DRM 调度器
GPU 硬件引擎
硬件中断 (IRQ) 处理
dma_fence 信号
用户事件 / 唤醒 libdrm
3. 设备初始化
驱动的生命周期始于被内核总线(PCI, Platform Bus 等)发现。
- PCI GPU(桌面/服务器) :驱动会注册一个
struct pci_driver。当内核发现匹配的 PCI ID 时,会调用驱动的.probe()回调。 - Platform GPU(SoC/嵌入式) :驱动会注册一个
struct platform_driver,通常通过设备树(Device Tree)或 ACPI 匹配。
probe() 函数是驱动的入口点,其核心职责是初始化 struct drm_device。
下面的伪代码展示了这个流程(以 PCI 为例):
c
// 驱动定义的一系列 DRM 回调
static struct drm_driver my_drm_driver = {
.name = "mygpu",
.driver_features = DRIVER_MODESET | DRIVER_RENDER | DRIVER_GEM | DRIVER_SYNCOBJ,
.open = my_open, // open 回调
.postclose = my_postclose, // close 回调
.ioctls = my_ioctls, // ioctl 列表
.fops = &my_fops, // file_operations
// ... 其它 KMS 和 GEM 回调
};
// PCI 驱动的 probe 入口
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
struct drm_device *dev;
int ret;
// 1. 分配 drm_device 结构,并关联 my_drm_driver
dev = drm_dev_alloc(&my_drm_driver, &pdev->dev);
if (IS_ERR(dev))
return PTR_ERR(dev);
// 2. 启用 PCI 设备
pci_enable_device_mem(pdev);
pci_request_regions(pdev);
pci_set_master(pdev); // 开启 DMA
// 3. 映射 MMIO 寄存器
// ... pci_iomap() ...
// 4. 初始化 KMS (显示)
drm_mode_config_init(dev);
// 5. 初始化 GEM / TTM (内存管理)
my_memory_manager_init(dev);
// 6. 初始化 Scheduler (命令调度)
my_scheduler_init(dev);
// 7. 请求 IRQ
// ... request_irq() ...
// 8. 注册 DRM 设备 (这里会创建 /dev/dri/cardX)
ret = drm_dev_register(dev, 0);
if (ret)
goto err_free_dev;
// ... 其它初始化,如 debugfs ...
return 0;
err_free_dev:
drm_dev_put(dev); // 失败时回滚
return ret;
}
// PCI 驱动结构体
static struct pci_driver my_pci_driver = {
.name = "mygpu_pci",
.id_table = my_pci_ids,
.probe = my_pci_probe,
.remove = my_pci_remove,
};
// 模块入口:注册 PCI 驱动
module_pci_driver(my_pci_driver);
PCI probe 简化流程图
DRM Core GPU 驱动 PCI Core 内核 DRM Core GPU 驱动 PCI Core 内核 驱动开始初始化 请求 I/O 或内存区域 映射硬件寄存器空间 分配并初始化 drm_device 结构 驱动自定义的内存分配与管理 初始化 KMS 接口 注册硬件中断处理函数 调用 device_add() 设备现在对用户空间可见 驱动探测流程结束 发现新的 PCI 设备 1 调用 probe(pci_dev, id_table) 2 pci_enable_device() 3 pci_request_regions() 4 pci_iomap() 5 获取 MMIO 基地址 (mmio_base) 6 drm_dev_alloc() 7 初始化内存管理器 (GEM/TTM) 8 drm_mode_config_init() 9 request_irq() 10 drm_dev_register() 11 通知创建设备节点 12 创建 /dev/dri/cardX 完成 13 返回 0 (成功) 14
4. 文件接口
当用户态 open() 设备文件 /dev/dri/cardX 或 /dev/dri/renderDN 时,内核会:
- 调用 DRM Core 的
drm_open()。 - 创建一个
struct drm_file实例,用于代表这个"打开的会话"。 drm_file用于追踪该进程所拥有的句柄(handles)和上下文(contexts)。
驱动的核心交互几乎都是通过 ioctl() 完成的。file_operations 结构中的 unlocked_ioctl 会指向 drm_ioctl()。
drm_ioctl():DRM Core 的主分发函数。它会检查ioctl编号,如果是 DRM 通用ioctl(如DRM_IOCTL_GEM_OPEN),则由 Core 处理;如果是驱动私有ioctl(如DRM_IOCTL_I915_GEM_EXECBUFFER2),则分发到drm_driver->ioctls列表中的对应回调。- render nodes (
/dev/dri/renderD128):这是一种特殊的设备节点,它只提供"渲染"功能(GEM, Submit),不提供"显示"功能(KMS)。这对于沙盒环境(如容器、浏览器 GPU 进程)非常重要,因为它允许进程使用 GPU 计算,但阻止它们修改显示设置。
open → ioctl → submit 流程图
GPU 硬件 GPU 驱动 DRM Core /dev/dri/cardX 用户进程 GPU 硬件 GPU 驱动 DRM Core /dev/dri/cardX 用户进程 获得 DRM 文件描述符 (fd) 关联到文件 fd,用于追踪进程状态 请求创建 GEM 缓冲对象 驱动创建 drm_gem_object 结构体 (BO) 为 BO 创建一个本地、进程可见的整数句柄 (handle) 用户后续操作 BO 使用此 handle 提交 Batch Buffer,包含 BO handles 驱动通过 handle 查找 BO,进行验证和重定位 硬件执行完毕 标记异步任务完成 如果进程正在等待 fence,则被唤醒 open("/dev/dri/cardX") 1 创建 drm_file 结构体 2 ioctl(DRM_IOCTL_GEM_CREATE) 3 DRM ioctl 分派 4 driver->gem_create_object() 5 返回 BO 对象 6 注册 GEM handle 7 返回 GEM handle (本地句柄) 8 ioctl(DRM_IOCTL_SUBMIT_CMDS / EXECBUFFER) 9 DRM ioctl 分派 10 driver->submit_cmds(handles) 11 提交命令到硬件 (Ring Buffer) 12 IRQ (中断) 完成 13 signal dma_fence 14 唤醒等待进程 15
5. 内存管理
GPU 内存管理是驱动最核心、最复杂的部分。DRM 提供了两个主要的内存管理框架:
- GEM (Graphics Execution Manager):一个轻量级的内存管理器。它只定义了 BO 的创建、映射和共享(dma-buf)的接口。GEM 本身不管理 VRAM 空间或 BO 迁移,它要求驱动自己实现物理内存的分配器,非常适合集成显卡(UMA)或内存管理相对简单的嵌入式 GPU。
- TTM (Translation Table Maps) :一个重量级、功能完备的内存管理器。它提供了复杂的内存类型管理(VRAM, GTT, System Memory)、BO 迁移(Eviction,当 VRAM 不足时将 BO 踢回系统内存)、VRAM 空间管理等,主要用于需要复杂内存管理的独立显卡(如
amdgpu)。
核心概念:
- BO (Buffer Object) :
drm_gem_object。这是内核中对一块内存(显存或系统内存)的抽象,用户态通过不透明的handle来引用它。 - IOVA (I/O Virtual Address) :GPU 访问内存使用的地址。如果系统有 IOMMU,这就是 IOMMU 提供的虚拟地址;如果没有,这通常就是物理 DMA 地址。驱动的核心职责之一是将 BO 的物理页(
sg_table)映射到 GPU 可见的 IOVA。 - Modifiers (布局修饰符) :一个 64 位值,用于描述 BO 数据的物理布局。为什么需要它?因为现代 GPU 为了节省带宽和提升性能,不会线性存储图像,而是使用平铺(Tiling)或压缩(Compression,如 AFBC, UBWC)格式。Modifier 告诉显示控制器(DPU)或其它设备(如 VPU)如何正确地读取这些被压缩/平铺的数据。
BO 生命周期图
物理资源
内核空间
用户空间
ioctl
ioctl
mmap
ioctl
绑定(Pin) / 释放(free)
导出(export)
导入(import)
GEM_CREATE
MAP_OFFSET
mmap
GEM_CLOSE
GEM/TTM 内存管理
drm_gem_object
dma-buf fd
VRAM / System RAM (物理页)
外部设备导入(Display/VPU)
6. Prime 与跨设备共享
DMA-BUF 是 Linux 内核中用于在不同设备驱动间零拷贝共享缓冲区的标准机制,在 DRM 语境下也叫 Prime。在 GPU 驱动中,它用于:
- GPU(渲染) -> Display(显示)
- VPU(视频解码) -> GPU(后处理)
- Camera -> GPU(AI 分析)
实现细节:
- 导出 (Export) :当用户请求导出 BO 时(
DRM_IOCTL_PRIME_HANDLE_TO_FD),驱动调用drm_gem_prime_export()。这会创建一个struct dma_buf,它封装了 BO 的物理页列表(sg_table)和元数据(如 Modifiers)。该dma_buf最终被包装成一个文件描述符(fd)返回给用户。 - 导入 (Import) :另一个驱动(或同一个驱动的另一个进程)拿到这个 fd 后,调用
DRM_IOCTL_PRIME_FD_TO_HANDLE。内核通过dma_buf_get()找到对应的dma_buf,导入方驱动(Importer)为其创建一个本地的drm_gem_object,该对象指向(而不是拷贝)原始的dma_buf物理页。 - 同步 (Synchronization):跨设备共享必须使用 Fences。导出时必须附带一个 Fence,表示"Producer 何时写完";导入方在使用前必须等待这个 Fence。
DMA-BUF 导出/导入流程图
DRM/dma-buf 核心 显示驱动 (PRIME 导入方) GPU 驱动 (PRIME 导出方) 用户空间 (App) DRM/dma-buf 核心 显示驱动 (PRIME 导入方) GPU 驱动 (PRIME 导出方) 用户空间 (App) 用户请求导出 GEM/BO (Buffer Object) 驱动将 GEM 对象转换为 dma_buf 结构 fd 可通过进程间通信共享 导入方(如 Display)获得 fd 导入方请求将 fd 转换为本地 handle 增加 dma_buf 引用计数 驱动创建新的、指向 dma_buf 的本地对象 导入方将自身设备 attach 到 dma_buf 用户现在可以使用此本地 handle 操作共享缓冲区 ioctl(PRIME_HANDLE_TO_FD) 1 drm_gem_prime_export() 2 dma_buf 结构体 3 返回 dma_buf_fd (文件描述符) 4 传递 dma_buf_fd 5 ioctl(PRIME_FD_TO_HANDLE) 6 dma_buf_get() 7 1. 创建本地 GEM handle 8 2. dma_buf_attach() 9 返回 local handle 10
7. 命令提交
GPU 不会逐条执行指令,而是执行打包好的"命令缓冲"(Batch Buffer 或 Command Stream)。
- Ring Buffer (命令环):这是 GPU 硬件上的一个 FIFO 队列(通常在 VRAM 或系统内存中)。驱动(Kernel)将命令写入 Ring Buffer 的尾部,GPU 硬件从头部读取并执行。
- Batch Buffer (用户命令):用户态驱动(Mesa)构建的命令包,它本身也是一个 BO。
- 提交 IOCTL :用户态调用一个私有的
ioctl(如DRM_IOCTL_I915_GEM_EXECBUFFER2)来提交渲染任务。这个ioctl会告诉内核:"请执行这个 Batch Buffer,它依赖这些 BOs"。 - Relocations (重定位) :Batch Buffer 中包含对其它 BOs(如纹理、顶点数据)的引用,但用户态不知道这些 BO 的虚拟地址(IOVA),它只使用本地句柄(handle)。当命令提交到内核时,驱动必须解析这个 Batch Buffer,找到所有这些引用,并将它们替换为 GPU 可见的真实 IOVA,这个过程称为重定位。
命令提交流程图
GPU 硬件 GPU 驱动 (KMD) DRM 核心框架 libdrm Mesa / OpenGL 驱动 (UMD) 应用程序 (App) GPU 硬件 GPU 驱动 (KMD) DRM 核心框架 libdrm Mesa / OpenGL 驱动 (UMD) 应用程序 (App) 用户调用 glDraw* 或 vkCmdDraw* 生成 GPU 可执行指令,并准备数据/纹理 BOs 包含 Batch Buffer 指针和 BOs 列表 libdrm 将请求发送到内核 DRM 核心将请求交给特定的 GPU 驱动处理 检查权限、大小和命令有效性 (安全性检查) 将 Batch Buffer 中的虚拟地址修正为 IOVA/物理地址 创建异步同步原语 将处理完的命令放入硬件队列,并通知 GPU GPU 执行完毕 标记任务完成,唤醒等待 f 的线程/设备 提交 Draw Calls (渲染命令) 1 构建 Batch Buffer & 缓冲对象 (BOs) 2 准备 execbuffer 数据包 3 ioctl(DRM_IOCTL_GEM_EXECBUFFER) 4 分派 execbuffer 请求 5 1. 验证 BOs 和命令 6 2. 重定位 (Relocation) 7 3. 创建 dma_fence (f) 8 写入 Ring Buffer / 触发调度 (Kick GPU) 9 返回 dma_fence (f) 10 返回 dma_fence_fd 11 返回 dma_fence_fd 12 返回完成通知 (取决于 API) 13 硬件中断 (IRQ) 14 dma_fence_signal(f) 15
8. 任务调度
如果多个应用程序同时提交命令,GPU 硬件(通常)只有一个或几个执行引擎。调度器(drm_sched)负责管理这些任务的执行。
- 软调度 (Software Queues):内核驱动维护多个软件运行队列(runqueues),通常按优先级划分。
- 作业 (Job) :一次
ioctl提交被封装成一个drm_sched_job。 - 硬件 (Ring):当 GPU 硬件引擎空闲时,调度器从软件队列中挑选一个最高优先级的 Job,将其提交到硬件 Ring Buffer。在命令提交阶段由驱动完成 Relocations 操作,这是 Job 加入调度队列前的预处理步骤。
- 抢占 (Preemption):现代 GPU 支持不同粒度的抢占(例如,在 draw call 之间切换,甚至像素级别中断)。如果一个高优先级任务(如桌面合成器)来了,调度器可以(尝试)抢占当前正在运行的低优先级任务(如游戏),驱动需要实现保存和恢复 GPU 上下文(Context)的复杂逻辑。
任务调度流程图
硬件接口与执行
内核调度和管理
用户空间
ioctl(SUBMIT)
加入
GPU 空闲时唤醒
写入 GPU 命令
硬件读取
执行完成
标记 Job 完成
用户提交 Job
调度器(DRM Scheduler)
软件运行队列(Run Queue)
Job 选择 (Picker)
重定位(Relocation)
命令传输通道(Ring Buffer)
GPU 执行 (Execute)
硬件中断(IRQ)
9. 同步
为什么需要同步? CPU、GPU 和显示引擎是高度异步的。当你提交一个命令时,CPU 立即返回,但 GPU 可能在几毫秒后才执行。我们必须有一种机制来管理操作顺序。
dma_fence:是 Linux 内核中异步操作的"完成回执"。它是一个轻量级对象,代表一个"未来会发生的事件"。- 创建与信令 :当驱动向 GPU 提交一个 Job 时,它会创建一个
dma_fencef并将其返回给用户态(通常包装在sync_filefd 中)。当 GPU 完成该 Job 时,驱动在中断处理程序中调用dma_fence_signal(f)。 - 等待 :其它任何需要等待这个 Job 完成的模块(如 KMS、VPU 或另一个 GPU Job)可以调用
dma_fence_wait(f),这个调用会休眠,直到f被 signal。
关键使用场景 (Render -> Scanout):
- 用户态(Compositor)提交渲染命令,获得
fence_fd_A。 - Compositor 调用 KMS 的
ioctl请求"上屏"这个新渲染的 BO,并将fence_fd_A作为参数传入(in_fence)。 - KMS 驱动在准备上屏时,会
dma_fence_wait()这个fence_A。 - 等待...,GPU 渲染完成,
fence_A被 signal。 - KMS 驱动被唤醒,安全地将 BO 上屏。
硬件 (GPU) 驱动 (Kernel Driver) 用户 (User Space) 硬件 (GPU) 驱动 (Kernel Driver) 用户 (User Space) 提交要执行的命令集 驱动程序创建同步原语 f 用户态获得描述符,可用于等待或导入 驱动通知 GPU 开始执行,并设置中断关联 f GPU 执行完毕,发出中断信号 唤醒所有等待 f 的线程/设备 ioctl(SUBMIT_JOB) 1 创建 dma_fence 对象 (f) 2 返回 fence_fd (f) 3 提交 Job 到 Ring Buffer 并关联 f 4 硬件完成 IRQ 5 dma_fence_signal(f) 6 ioctl(WAIT_FENCE) 7 返回 (任务完成) 8
10. 中断处理
中断(IRQ)用于通知驱动两种主要事件:
- Job Completion :任务完成,驱动需要 signal 对应的
dma_fence并调度下一个 Job。 - Error / Hang:GPU 挂起(例如,执行了非法指令或访问了无效内存)。
GPU Hang 是生产环境中必须处理的问题。如果 GPU 挂了,整个系统可能会冻结。驱动必须实现 Reset(重置)和 Recovery(恢复)流程。
Reset / Recovery 流程:
- 检测 (Detection):驱动通过"看门狗"定时器(Watchdog)检测 Job 是否超时未完成,或通过硬件错误中断检测到 Hang。
- 停止 (Quiesce):立即停止向 GPU 提交任何新任务。
- 重置 (Reset):执行硬件特定的重置序列(可能是引擎重置,或代价高昂的整个设备重置)。
- 恢复 (Restore):重新初始化 GPU 硬件状态、清空硬件 Ring、恢复驱动内部的软件队列状态。
- 上报 (Report) :通过
ioctl向导致 Hang 的进程返回错误(如-EIO),并 signal 所有受影响的 fences(标记为出错状态),这允许用户态(Mesa)知道任务失败了,并尝试重新提交。
用户进程 (User App) 驱动 (Kernel Driver) GPU 硬件 用户进程 (User App) 驱动 (Kernel Driver) GPU 硬件 驱动检测到 GPU 失去响应 (Hang) 阻止新的 Job 提交到 Ring Buffer 清空所有寄存器、Ring Buffer 及未完成的命令 确认硬件已恢复到初始状态 重新初始化 Ring Buffer,恢复调度器状态,标记失败 Job 通知用户进程 Job 失败,需要清理并重试 错误中断 / 看门狗超时 (Hang Detection) 1 1. 停止 Job 调度并锁定资源 2 2. 发起硬件重置 (Reset Command) 3 硬件重置完成信号 4 3. 恢复 HW/SW 状态 (State Restoration) 5 4. 向出错 Job 进程返回 EIO 错误 6
11. 电源管理
现代 GPU 驱动必须精细地管理电源,以在提供高性能的同时最大限度地延长电池寿命(移动/嵌入式设备)或降低数据中心能耗。这主要依赖于内核的 Runtime PM(运行时电源管理)框架和驱动自身的时钟/电源域控制。
1. Runtime PM (运行时电源管理)
- 核心机制 :驱动利用内核的 Runtime PM 框架(
pm_runtime_API)在 GPU 硬件空闲时自动将其置于低功耗状态(D-States,如D3cold),并在需要时(D0)快速唤醒。 - 引用计数 :Runtime PM 的核心是引用计数。驱动通过
pm_runtime_get_sync()增加引用计数(唤醒设备),并通过pm_runtime_put_autosuspend()减少引用计数(允许设备自动挂起)。 - 触发时机 :
open/close:当第一个drm_file被打开时,驱动调用pm_runtime_get_sync()唤醒 GPU。当最后一个drm_file关闭时,调用pm_runtime_put()。submit(命令提交) :在ioctl提交任务时,驱动必须pm_runtime_get_sync()确保 GPU 处于唤醒状态才能接收命令。irq_handler(中断处理) :中断处理程序本身通常在pm_runtime_get()保护下运行。当驱动处理完中断,特别是当它检测到 GPU 已经没有更多任务时,它会调用pm_runtime_put_autosuspend()。
- 自动挂起 (Autosuspend) :驱动会设置一个
autosuspend_delay(例如 50ms)。当引用计数降至 0 后,pm_runtime_put_autosuspend()并不会立即挂起设备,而是启动这个定时器。如果 50ms 内没有新的pm_runtime_get(),内核 PM Core 才会真正执行挂起操作(如切断电源)。
2. 空闲状态追踪
驱动如何知道"GPU 已空闲"并调用 pm_runtime_put_autosuspend()?这至关重要。
- 基于 Fence :最精确的方法是追踪
dma_fence。驱动在提交 Job 时pm_runtime_get(),并在该 Job 对应的dma_fence被 signal(即 Job 完成)时,在中断或dma_fence_signal()上下文中调用pm_runtime_put()。 - 基于 DRM Scheduler :如果使用了
drm_sched,调度器本身会追踪队列中是否有待处理的 Job。当调度器发现所有软件队列(runqueues)都为空,并且硬件 Ring Buffer 也已排空时,它就可以触发pm_runtime_put_autosuspend()。
3. Clock / Power Domains (时钟/电源域门控)
这是一种比 Runtime PM 更细粒度的电源管理。SoC 中的 GPU 通常包含许多独立的"块"(如 3D 引擎、视频编解码器、显示控制器、内存接口等),每个块都有自己的时钟和电源域。
动态门控:驱动程序不只是在整个 GPU 空闲时才省电。在 GPU 运行时,驱动也会根据当前的任务类型动态地开关这些时钟域。
示例 :如果用户只提交了一个 3D 渲染任务,驱动会在 run_job 时 clk_enable(3d_engine_clk),但保持 vpu_engine_clk(视频解码器时钟)处于 clk_disable() 状态。当 Job 完成后(IRQ 中),再 clk_disable(3d_engine_clk)。
协同工作:Runtime PM 通常控制的是整个 GPU 设备的"主电源/时钟",而 Clock Domains 控制的是 GPU 内部的精细子模块。两者协同工作以达到最佳能效。
12. 隔离、权限与虚拟化
1. 多进程隔离
drm_file 天然地提供了进程间隔离。一个 drm_file(代表一个 open 会话)维护着一张私有的 handle to GEM object 映射表。因此,一个进程的 GEM handle 在另一个进程中是无效的,无法被(恶意)访问。
2. 权限模型
- Master :DRM 核心定义了"Master"权限。通常,第一个打开
/dev/dri/cardX的进程(如 Xorg 或 Wayland Compositor)会成为 Master。只有 Master 才能执行 KMS 操作(如设置模式、改变分辨率、上屏)或设置特权寄存器。 - Auth :非 Master 进程(如游戏、浏览器)可以通过
DRM_IOCTL_GET_AUTH获得一个唯一的magiccookie,并将其传递给 Master。Master 通过DRM_IOCTL_AUTH_MAGIC验证这个 cookie,从而"认证"该进程。被认证的进程可以执行渲染、创建 BO 等操作,但不能执行 KMS 操作。 - Render Nodes :
/dev/dri/renderDN节点彻底改变了这一点,它不提供任何 KMS 接口,因此不需要 Master 权限,任何进程都可以打开它并安全地执行渲染,极大地简化了沙盒和容器环境。
3. 虚拟化 (Virtualization)
- VFIO (Passthrough):最简单直接的方式。通过 IOMMU,将整个物理 GPU 设备(PCIe 设备)完全"穿透"并分配给一个虚拟机(VM)。宿主机(Host)驱动被解绑,VM 内的驱动(可以是标准驱动)直接控制硬件。优点:性能接近原生。缺点:GPU 独占,无法共享。
- SR-IOV (Single Root I/O Virtualization):一种基于 PCI 标准的硬件虚拟化。硬件(GPU)本身支持被划分为一个物理功能(PF)和多个虚拟功能(VF)。每个 VF 都是一个轻量级的 PCIe 设备,可以像 VFIO 一样被"穿透"给不同的 VM。Host 驱动(运行在 PF 上)负责管理 VFs 的资源分配。
- vGPU (Mediated Device) :一种更灵活的软件与硬件协同的虚拟化方案(如 Intel GVT-g, NVIDIA vGPU)。它不依赖 SR-IOV,而是使用 Linux 内核的
mdev(Mediated Device) 框架。mdev框架 :Host 驱动(如i915)向mdev框架注册,声明它可以创建"虚拟 GPU"。管理员可以配置创建多个mdev实例,这些实例随后被分配给 VMM (QEMU),并作为虚拟设备呈现给 Guest VM。- 优势 :vGPU 方案利用了驱动中已有的调度器(
drm_sched)和内存管理框架,实现了在多个 VM 之间安全、公平地共享同一个物理 GPU,且粒度非常灵活。 - vGPU 工作流 :
- Guest (客户机) :VM 内的(半虚拟化)DRM 驱动提交
ioctl命令。 - VMM (宿主机) :QEMU 截获这个
ioctl调用(通常是通过 VFIO-mdev 接口)。 - Host (宿主机) :VMM 将其转发给 Host 上的
mdev驱动(即物理 GPU 驱动,如i915)。 - Host 驱动 :Host 驱动接收到来自 VM 的命令,将其视为一个"外部"任务。它进行安全性验证、上下文隔离、命令重定位,然后将这个 Job 封装成一个标准的
drm_sched_job,推入到物理 GPU 的drm_sched队列中。
- Guest (客户机) :VM 内的(半虚拟化)DRM 驱动提交
13. 测试、调试与性能追踪
- 测试套件 :
- modetest:libdrm 自带工具,用于测试 KMS(显示模式、平面)。
- piglit / VK-CTS:OpenGL 和 Vulkan 的一致性测试套件,用于验证驱动的 3D/Compute 实现是否符合规范。
- igt-gpu-tools:(原 i915-tools) 强大的 GPU 驱动测试集合,现已支持多款驱动。
- 调试 (debugfs) :驱动应在
/sys/kernel/debug/dri/0/目录下暴露丰富的内部状态文件(如:当前 BO 列表、Ring Buffer 状态、GPU 频率、Hang 统计等)。 - 追踪 (tracepoints / ftrace) :驱动应提供关键路径的 tracepoints(如:job submit, irq handler, fence signal)。开发者可以使用
trace-cmd或perf抓取轨迹,并使用gpuvis等工具进行可视化分析,直观地看到 CPU 和 GPU 的并行执行时间线。
14. 命令提交流程示例
下面是一个简化的端到端流程伪代码实例,展示了用户态和内核态的交互。
用户态
c
/* 用户态(Mesa)构建 BOs 和命令缓冲 */
int fd = open("/dev/dri/renderD128", O_RDWR);
// 1. 创建 BO (用于存储命令)
uint32_t cmd_bo_handle = drm_gem_create(fd, 4096);
void *cpu_ptr = drm_gem_mmap(fd, cmd_bo_handle, 4096);
// 2. 写入 GPU 命令
write_gpu_commands(cpu_ptr);
munmap(cpu_ptr, 4096);
// 3. 准备提交
struct my_submit_args args = {
.cmd_bo_handle = cmd_bo_handle,
.flags = NEED_FENCE_FD,
};
// 4. 提交命令,并请求一个 fence_fd
int out_fence_fd = drmIoctl(fd, DRM_IOCTL_MY_SUBMIT, &args);
// 5. (可选) 等待 GPU 完成
poll({.fd = out_fence_fd, .events = POLLIN}, 1, -1);
close(out_fence_fd);
内核态
c
/* 驱动的 IOCTL 回调 */
int my_submit_ioctl(struct drm_device *dev, void *data, struct drm_file *file)
{
struct my_submit_args *args = data;
struct drm_gem_object *bo;
struct my_job *job;
struct dma_fence *fence;
int fence_fd = -1;
// 1. 根据 handle 查找 BO
bo = drm_gem_object_lookup(file, args->cmd_bo_handle);
// 2. 分配一个 Job
job = kzalloc(sizeof(*job), GFP_KERNEL);
job->bo = bo; // 引用 BO (增加 refcount)
// 3. (核心) 使用 DRM Scheduler 提交
// my_gpu_sched 是驱动的调度器实例
fence = drm_sched_job_arm(&job->base, my_gpu_sched);
drm_sched_job_push(&job->base); // 推入队列
// 4. 如果用户需要,将 dma_fence 导出为 fd
if (args->flags & NEED_FENCE_FD) {
fence_fd = dma_fence_get_stub_fd(fence);
}
dma_fence_put(fence); // job 持有 fence 引用,这里释放本地引用
return fence_fd; // 返回 fd (或 0)
}
/*
* 当调度器(drm_sched)决定运行此 Job 时,
* 它会调用驱动注册的 .run_job() 回调
*/
struct dma_fence *my_run_job(struct drm_sched_job *sched_job)
{
struct my_job *job = container_of(sched_job, struct my_job, base);
// 1. (核心) 获取 dma_fence,Job 完成时必须 signal 它
struct dma_fence *f = &job->hw_fence;
// 2. (硬件操作) 锁定 BO 内存,获取 IOVA
// ... pin_bo(job->bo) ...
// ... do_relocations(job->bo) ...
// 3. 将命令写入硬件 Ring Buffer
// ... write_to_hw_ring(job->bo->iova, f) ...
// 4. "敲钟" (Kick) 告诉 GPU 开始执行
// ... mmio_write(GPU_DOORBELL, ...) ...
// 5. 返回 fence,调度器会将其返回给提交者
return f;
}
/* GPU 完成中断处理(IRQ Handler)*/
irqreturn_t my_irq_handler(int irq, void *arg)
{
struct my_driver *drv = arg;
if (/* 是 Job 完成中断 */) {
// 1. 找到是哪个 Job 完成了
struct my_job *finished_job = find_finished_job(drv);
// 2. (核心) Signal 对应的 fence
dma_fence_signal(&finished_job->hw_fence);
// 3. 释放 Job
drm_sched_job_cleanup(&finished_job->base);
kfree(finished_job);
return IRQ_HANDLED;
}
// ... 处理其它中断 ...
return IRQ_NONE;
}
15. 总结
本文档详细拆解了 Linux 内核 GPU 驱动(渲染侧)的框架,理解这个框架的关键在于掌握几个核心概念的交互:
- 入口 :驱动通过
pci_driver或platform_driver的probe函数启动,并注册drm_device。 - 接口 :通过
file_operations和ioctl列表响应来自libdrm的用户态请求。 - 内存 (GEM/TTM) :一切操作的对象都是
drm_gem_object(BO)。驱动负责管理它们的分配、GPU 映射 (IOVA) 和跨设备共享 (dma-buf)。 - 执行 (Submit & Scheduler) :用户态提交的命令被打包成
drm_sched_job,由调度器排队,经"重定位"后发往硬件 Ring Buffer 执行。 - 同步 (dma_fence):这是串联起所有异步操作(CPU, GPU, Display)的粘合剂,确保了正确的执行顺序。
- 稳定 (Reset/Recovery):驱动必须有能力从 GPU Hang 中检测、重置并恢复,以保证系统稳定。
掌握了这些模块如何协同工作,是深入理解和开发 Linux GPU 驱动的基础。