Skip to content

内核与驱动:把应用意图交给设备 ​

系统栈总览 / 下一节:平台图形栈、引擎与应用 / 排障:跨层故障链

应用的绘图 API 不会直接写 GPU 寄存器,触摸屏也不会直接调用应用回调。内核和驱动负责把不可信、并发的进程请求转换为设备工作,同时隔离地址空间、约束设备访问范围、管理 buffer 所有权,并在完成或失败时通知上层。

这层要同时完成四件事:输入交付、CPU 调度、数据搬运、异步同步。客户端“已经改了状态但还没显示”“GPU 已画完却没赶上本次刷新”“画面卡住但 CPU 不高”,都要从这四类机制中区分。

两条路径与一个回路 ​

控制路径决定谁做什么;数据路径搬运像素和资源;完成回路让下游知道何时可以消费结果。

text
控制:应用/引擎 -> 图形 API -> 用户态驱动 -> 内核 DRM/GPU driver -> GPU queue
数据:CPU 内存/文件/相机 buffer --DMA--> RAM 或显存 --GPU--> render target
回路:GPU fence / display vblank / input IRQ -> 内核事件 -> compositor 或应用

Linux DRM/KMS 是本页的主例子。Windows 的 WDDM/DXGI、macOS 的 IOKit/Metal 与 WindowServer、Android 的 SurfaceFlinger 有不同对象和权限模型,但也必须解决同一组问题:谁拥有 buffer,谁可写它,何时可复用,何时真正显示。

1. 输入:IRQ 不是应用已经收到事件 ​

鼠标、键盘、触摸屏控制器产生报告后,通常以中断(IRQ)通知 CPU。内核驱动确认中断来源、读取设备数据,再经输入子系统输出按键、相对位移或触摸坐标。高频设备的复杂处理可能延后到软中断或工作队列,避免长时间占据硬中断上下文。

text
触摸控制器报告坐标
  -> IRQ
  -> 内核驱动读取与确认
  -> 输入子系统形成事件
  -> 窗口系统/浏览器读取并路由
  -> 坐标转换、命中测试、应用事件处理器

三个边界必须分开:

  • IRQ 只说明设备需要服务;目标进程仍要等其线程获得 CPU。
  • 原始设备坐标到应用坐标会经过屏幕旋转、缩放、多屏、窗口和 viewport 变换;“点位偏移”要逐段检查。
  • 事件送达不等于画面出现。事件处理、状态变更、渲染提交、合成与扫描至少还跨一次帧边界。

2. 调度:内核不理解“一帧” ​

调度器只在可运行线程间分配 CPU:依据调度策略、优先级、运行队列、CPU 亲和性和等待状态。它不理解 DOM、Widget 或动画。浏览器主线程、Flutter 的 UI isolate、栅格线程、GPU 服务线程和系统合成器都在竞争 CPU。

text
runnable: 已在 run queue,等待 CPU
running:  正在某核执行
blocked:  等锁、IPC、I/O、page fault 或 fence,暂不参与竞争

因此页面延迟并不总是“JS 慢”:主线程可能被长 task 占住,也可能阻塞在锁、进程间消息、缺页或 GPU fence;CPU 利用率高也不能推出 GPU 忙。诊断需要线程时间线和等待原因,而不是仅看总 CPU。

3. 虚拟内存、缺页与进程隔离 ​

进程使用虚拟地址。CPU 的 MMU 按页表翻译到物理页;内核据此隔离进程、延迟分配内存,并把共享 buffer 映射给协作进程或设备。渲染会涉及应用堆、纹理、图块、command buffer、共享内存和显示 buffer。

text
应用虚拟地址 -> TLB 或页表遍历 -> 物理 RAM
  -> 设备映射/IOMMU 翻译 -> DMA 或 GPU 读写

page fault 不必然是错误。首次触页时内核可能只建立映射;也可能要分配页、读文件或在内存压力下回收页。后几类若发生在输入到呈现的关键路径,会造成长尾卡顿。独立显存、统一内存和不同驱动的资源驻留策略差别很大,不能用应用 RSS 推断一张纹理是否已可用。

图形 API 调用通常先由用户态驱动编码命令,再在创建上下文、映射 buffer、提交队列等步骤进入内核。浏览器还会经 GPU 进程/服务使用 IPC 隔离不可信网页与复杂驱动。系统调用和 IPC 返回只说明请求被接受或排队,不能说明 GPU 已执行、更不能说明屏幕已显示。

4. DMA 与 IOMMU:设备如何安全地访问数据 ​

DMA 让设备直接在内存与设备间传输数据,避免 CPU 逐字节搬运。驱动将经过授权的 buffer 映射给设备,并处理生命周期与缓存一致性;IOMMU 可进一步限制设备能访问的地址范围。应用的普通指针不能直接作为 GPU 可访问地址。

“零拷贝”也不等于零成本。相机或视频 buffer 可被共享,省掉一次 CPU 拷贝,却仍可能发生格式转换、缓存同步、跨设备等待或带宽竞争。是否应优化,先从 trace 确认拷贝和等待是否在关键路径上。

5. GPU 命令:从 command buffer 到执行队列 ​

GPU 不理解应用状态,只执行已编码的图形或计算工作。引擎把 draw、dispatch、纹理上传、资源屏障等写进 command buffer,提交到 queue。典型职责如下:

  1. 用户态驱动编码命令、维护 API 资源状态。
  2. 内核/驱动校验权限和对象,建立内存映射,接收提交。
  3. GPU 调度器在多个 context/进程间安排可运行工作。
  4. GPU 读取资源、执行 shader、写入 render target。
  5. 完成或出错时信号 fence,交给等待的 queue、compositor 或 CPU。
text
display list / scene
  -> graphics backend 编码 command buffer
  -> submit(queue, waits, signal fence)
  -> driver 调度 -> GPU 执行 -> fence signaled

submit 很快返回是正常的,它只说明工作进入队列。CPU 提交、GPU 开始、GPU 完成、compositor 采用 buffer、面板扫描到像素是不同时间点。没有这些时间戳,就无法区分 CPU 编码慢、队列积压、GPU 饱和和 presentation 延迟。

6. Fence、buffer ownership 与 vblank ​

CPU、GPU、合成器和显示控制器并行工作。没有同步,消费者会读到半成品,生产者会覆盖正在显示的画面。同步对象把依赖关系变成可等待、可观察的契约。

机制表达的事实例子
fence一批异步工作完成GPU 写完 render target,compositor 才可读取
semaphore/event后继 queue 可开始依赖任务合成 pass 等待栅格化结果
acquire/releasebuffer 的生产者与消费者切换所有权相机 -> GPU -> 系统合成器
vblank显示器到刷新边界在安全时机 page flip,避免扫描中替换画面

三缓冲能让显示、合成、GPU 并行,提高吞吐;但过深的 buffer queue 会把最新输入排在旧帧后面,增加延迟。双缓冲队列浅,却更容易因生产来不及而重复旧帧。这里没有固定最优值,交互系统在吞吐、稳定性和延迟间取舍。

7. Linux DRM/KMS:从结果到 scanout ​

DRM 管理图形设备对象与权限,KMS 管理 connector、CRTC、plane、framebuffer 和 modeset。一个用户态 compositor 组合各个 surface 的 buffer,选择 GPU 合成或硬件 plane,然后以 atomic commit 请求内核在合适的 vblank 更新显示状态。显示控制器从可扫描 buffer 逐行读取,最终送到面板。

text
GPU render target 完成
  -> compositor 取得 buffer
  -> GPU 合成或分配 hardware planes
  -> DRM atomic commit
  -> next vblank -> scanout -> 屏幕

这解释三类常见现象:

  • 撕裂:扫描中切换内容,屏幕上下来自不同帧;同步到 vblank 可避免,但会增加等待。
  • jank:新 buffer 未在刷新截止前就绪,显示器复用旧帧或跳过中间状态;状态未必丢失,但运动不连续。
  • 黑屏/花屏:可能是 shader、纹理格式、buffer 生命周期、plane 限制、驱动复位或显示链路;先证实最终提交的 buffer 与 KMS/系统合成器事件。

macOS 的应用通常经 Metal/CAMetalLayer 与 WindowServer 合成,Windows 通常经 Direct3D、DXGI 和 DWM;它们不暴露 Linux 的 DRM/KMS 文件接口,也不应直接用 CRTC/plane 来解释问题。可迁移的仍是 command queue、surface/buffer、同步和 presentation 的因果关系。

8. 设备复位与可观测性 ​

GPU 可能因驱动 bug、非法/超时工作、硬件故障或系统电源事件而被复位。复位会丢失或使 GPU context、资源内容失效;上层可能收到 device lost/removed,浏览器或应用要重建资源、降级或重启相关进程。不能把“某次提交没返回”简单归为应用卡死。

排障从一条可复现的时间线开始,保留事件时间、线程运行/阻塞、command submit、fence 完成和 presentation:

观察到的证据优先怀疑下一步
输入处理器很晚才开始run queue、长 task、锁、缺页查主线程调度和系统 trace
handler 很快返回却等 fenceGPU queue、资源上传、shader、栅格查 GPU/driver 轨道与资源压力
fence 已完成但晚一帧显示compositor、vblank、buffer queue查 presentation 和系统合成器
device lost、GPU reset 或黑屏驱动/硬件/资源恢复查内核日志、应用错误路径和重建策略

浏览器先使用 DevTools Performance 看 input delay、主线程、raster、compositor/GPU 轨道;原生 Linux 再结合系统 trace、DRM 事件、内核日志和厂商 GPU 工具。工具会变,证据结构不变。

上层开发者应保留的边界 ​

  • DOM mutation、setState 或 draw call 都只说明上层描述变了,不说明已经上屏。
  • 不要在输入回调里同步解码大资源、触发大规模布局或等待 GPU;先提交可见反馈,再安排非关键工作。
  • 不要凭直觉堆图层、纹理和离屏 buffer;它们会成为显存、带宽、栅格和合成压力。
  • 不要用固定延时等待“渲染完成”。用框架或平台的帧、fence、presentation 回调表达依赖;用 trace 证明真实路径。
  • “本地已提交”“已展示”“远端已确认”必须是不同状态。内核和驱动能证明设备完成工作,不能替服务端确认业务事实。

资料与边界 ​

本文不承诺某个浏览器、手机或操作系统的线程名、buffer 数和驱动实现。它们随硬件、内核、后端和版本变化;跨平台稳定的是隔离、异步队列、资源所有权、同步与最终 presentation 这几个问题。

学习规划与研究资料库