⚡ 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
图 1 · 用户态到内核的调用收敛:drmIoctl 是唯一的咽喉点

3. drmIoctl — 一切的入口

先看最底层。整个 libdrm 里成百上千次内核交互,全部经过这 8 行。它唯一做的"聪明事",是把被信号打断的 ioctl 自动重试:

xf86drm.c · drmIoctl() 源码 ↗
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 号并转发。点开下面代码块查看(默认折叠,体现"长源码不打断阅读"):

高亮行: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(友好结构体)
            
图 2 · 两趟 ioctl(count → alloc → fill)+ 热插拔 retry
⚠️
易错点

如果只调用一趟、或在两趟之间不重新读取 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
            
图 3 · KMS 资源对象的引用关系(connector → encoder → crtc 是点亮一块屏的链路)
libdrm 函数底层 ioctl作用
drmModeGetResourcesMODE_GETRESOURCES枚举 crtc/connector/encoder
drmModeGetConnectorMODE_GETCONNECTOR查某个接口的连接状态与模式
drmModeSetCrtcMODE_SETCRTC把 framebuffer 扫描到屏幕
drmCommandWriteRead驱动私有amdgpu/i915 自定义命令

7. 小结

libdrm 的价值不在"复杂",而在"统一":它把用户态与内核 DRM 的所有交互收敛到 drmIoctl 一个咽喉点,并用一致的封装模式(两趟 ioctl、私有命令号构造、友好结构体)降低上层心智负担。看懂这一层,再回头读内核 drm_ioctl() 的分发逻辑就顺理成章了。

🔗
下一步阅读

本页只是展示形式样板。真正的内核侧分发在 《DRM 设备模型速览》 一节,可对照用户态与内核态两个视角。