⚡ TL;DR — 三句话看懂 libdrm
- libdrm 是内核 DRM ioctl 的用户态薄封装:把
open()/ioctl()包成好用的 C 函数,屏蔽EINTR重试、ioctl 号计算等细节。 - 所有请求最终都走
drmIoctl():无论是通用 KMS 接口还是驱动私有命令(drmCommandWriteRead),底层都是同一个带重试的 ioctl 包装。 - KMS 资源查询是「两趟 ioctl」模式:第一趟拿数量、分配缓冲区,第二趟真正填充;中途热插拔会自动 retry。
1. libdrm 是什么
libdrm 是 DRM 子系统的用户态入口库。内核通过 /dev/dri/card0、/dev/dri/renderD128 暴露设备节点,用户态程序(X server、Mesa、compositor、你自己的测试程序)并不直接手写 ioctl(),而是调用 libdrm 提供的函数。它做的事情很"薄",但很关键:统一 ioctl 号的构造、处理被信号打断的重试、管理 fd 与 bus id 的映射、把内核裸结构体转成更友好的指针形式。
内核 DRM 提供"机制"(ioctl 接口),libdrm 提供"手感"(C API)。理解 libdrm,就是理解用户态如何把一次函数调用翻译成一次内核 ioctl。
2. 整体调用链路
下图是一次典型请求从应用到内核的路径。注意无论上层入口是谁,最终都收敛到 drmIoctl() 这一个点:
flowchart TD
A["应用 / Mesa / X server"] --> B["libdrm 高层 API
drmModeGetResources()
drmModeSetCrtc() ..."]
A --> C["驱动私有 API
drmCommandWriteRead()
drmCommandWrite()"]
B --> D["drmIoctl(fd, request, arg)"]
C --> D
D --> E["ioctl() 系统调用"]
E --> F["内核 DRM core
drm_ioctl()"]
F --> G["驱动 ioctl 表
amdgpu / i915 ..."]
style D fill:#3b82f6,color:#fff,stroke:#1d4ed8
style F fill:#8b5cf6,color:#fff,stroke:#6d28d9
3. drmIoctl — 一切的入口
先看最底层。整个 libdrm 里成百上千次内核交互,全部经过这 8 行。它唯一做的"聪明事",是把被信号打断的 ioctl 自动重试:
int drmIoctl(int fd, unsigned long request, void *arg){ int ret; do { ret = ioctl(fd, request, arg); } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); return ret;}
高亮行即核心:EINTR(被信号打断)或 EAGAIN 时循环重试,直到成功或遇到真正的错误。上层所有函数都因此不必各自处理信号中断。
4. 驱动私有命令封装
amdgpu、i915 等驱动都有自己的私有 ioctl。libdrm 用 drmCommandWrite / drmCommandWriteRead 统一构造 ioctl 号并转发。点开下面代码块查看(默认折叠,体现"长源码不打断阅读"):
int drmCommandWriteRead(int fd, unsigned long drmCommandIndex, void *data, unsigned long size){ unsigned long request; request = DRM_IOC(DRM_IOC_READ|DRM_IOC_WRITE, DRM_IOCTL_BASE, DRM_COMMAND_BASE + drmCommandIndex, size); if (drmIoctl(fd, request, data)) return -errno; return 0;}
高亮行:DRM_IOC(...) 用方向(读/写)、幻数基址、DRM_COMMAND_BASE + index 与结构体大小拼出唯一 ioctl 号,随后交给 drmIoctl。这就是"驱动私有命令"落到内核的方式。
5. KMS:两趟 ioctl 拿资源
drmModeGetResources() 是理解 KMS 的经典例子。它演示了内核"可变长度数组"接口的标准用法——第一趟只问数量,分配好缓冲区后第二趟才真正取数据,并处理中途热插拔:
sequenceDiagram
participant U as 用户程序
participant L as libdrm
participant K as 内核 DRM
U->>L: drmModeGetResources(fd)
L->>K: ioctl GETRESOURCES(count 全 0)
K-->>L: 返回各资源的数量
L->>L: 按数量 drmMalloc 缓冲区
L->>K: ioctl GETRESOURCES(带缓冲区指针)
K-->>L: 填充 crtc/connector/encoder id
Note over L,K: 若期间热插拔导致数量变大,goto retry 重来
L-->>U: drmModeResPtr(友好结构体)
如果只调用一趟、或在两趟之间不重新读取 count,遇到显示器热插拔就会读到不一致的资源列表。libdrm 用 counts 快照比对 + goto retry 来兜底。
6. 核心数据结构关系
libdrm 把内核裸结构(drm_mode_card_res 等)转换成对调用者友好的指针结构。它们的归属关系如下:
classDiagram
class drmModeRes {
+min/max width,height
+count_fbs
+count_crtcs
+count_connectors
+uint32_t* crtcs
+uint32_t* connectors
}
class drmModeConnector {
+connector_id
+connection
+count_modes
+drmModeModeInfo* modes
}
class drmModeCrtc {
+crtc_id
+buffer_id
+mode
}
class drmModeEncoder {
+encoder_id
+crtc_id
}
drmModeRes --> drmModeConnector : id 列表
drmModeRes --> drmModeCrtc : id 列表
drmModeRes --> drmModeEncoder : id 列表
drmModeConnector --> drmModeEncoder : 当前 encoder
drmModeEncoder --> drmModeCrtc : 绑定的 crtc
| libdrm 函数 | 底层 ioctl | 作用 |
|---|---|---|
| drmModeGetResources | MODE_GETRESOURCES | 枚举 crtc/connector/encoder |
| drmModeGetConnector | MODE_GETCONNECTOR | 查某个接口的连接状态与模式 |
| drmModeSetCrtc | MODE_SETCRTC | 把 framebuffer 扫描到屏幕 |
| drmCommandWriteRead | 驱动私有 | amdgpu/i915 自定义命令 |
7. 小结
libdrm 的价值不在"复杂",而在"统一":它把用户态与内核 DRM 的所有交互收敛到 drmIoctl 一个咽喉点,并用一致的封装模式(两趟 ioctl、私有命令号构造、友好结构体)降低上层心智负担。看懂这一层,再回头读内核 drm_ioctl() 的分发逻辑就顺理成章了。
本页只是展示形式样板。真正的内核侧分发在 《DRM 设备模型速览》 一节,可对照用户态与内核态两个视角。