4. 导出端驱动准备:实现一个简单的DRM驱动骨架、注册gem对象、实现gem_prime_export回调

在DMA-BUF导出与导入的完整链路中,导出端驱动是数据流的源头。它负责分配物理连续或IOMMU可寻址的内存,并将其包装为DRM GEM对象,最终通过gem_prime_export回调将GEM对象转换为DMA-BUF句柄,传递给其他驱动或用户空间。本章将带领你从零搭建一个最小化的DRM驱动骨架,注册GEM对象,并实现核心的导出回调。

4.1 驱动骨架:最小化DRM驱动结构

一个DRM驱动至少需要定义struct drm_driver和struct drm_device,并完成设备绑定与解除。以下是一个极简的导出端驱动骨架,仅保留DMA-BUF导出所需的最小回调。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <drm/drm_drv.h>
#include <drm/drm_device.h>
#include <drm/drm_gem.h>
#include <drm/drm_prime.h>

static struct drm_driver export_drm_driver = {
    .driver_features    = DRIVER_GEM | DRIVER_RENDER,
    .gem_prime_export   = drm_gem_prime_export,   // 使用通用实现
    .gem_prime_get_sg_table = export_gem_get_sg_table, // 自定义回调
    .gem_prime_pin      = export_gem_prime_pin,
    .gem_prime_unpin    = export_gem_prime_unpin,
    .name               = "export_drm",
    .desc               = "DMA-BUF Export Demo Driver",
    .date               = "202501",
    .major              = 1,
    .minor              = 0,
};

static int export_probe(struct platform_device *pdev)
{
    struct drm_device *ddev;
    int ret;

    ddev = drm_dev_alloc(&export_drm_driver, &pdev->dev);
    if (IS_ERR(ddev))
        return PTR_ERR(ddev);

    platform_set_drvdata(pdev, ddev);
    ret = drm_dev_register(ddev, 0);
    if (ret) {
        drm_dev_put(ddev);
        return ret;
    }
    return 0;
}

static int export_remove(struct platform_device *pdev)
{
    struct drm_device *ddev = platform_get_drvdata(pdev);
    drm_dev_unregister(ddev);
    drm_dev_put(ddev);
    return 0;
}

static struct platform_driver export_pdrv = {
    .probe  = export_probe,
    .remove = export_remove,
    .driver = {
        .name = "export_drm",
    },
};
module_platform_driver(export_pdrv);
MODULE_LICENSE("GPL");
关键点: DRIVER_GEM 标志必须设置,否则DRM核心不会初始化GEM子系统。DRIVER_RENDER 允许驱动以渲染节点方式工作,无需绑定显示硬件。

4.2 注册GEM对象:分配与初始化

导出端驱动需要实现GEM对象的分配函数。通常,我们使用drm_gem_cma_create(CMA连续内存)或drm_gem_shmem_create(页式内存)来简化工作。但为了展示完整控制流程,这里手动实现一个基于drm_gem_object的分配器,并关联一个私有结构体用于存储物理内存信息。

struct export_gem_object {
    struct drm_gem_object base;
    struct page **pages;        // 物理页指针数组
    unsigned int num_pages;
    struct sg_table *sg;        // 散列表,用于导出
};

static struct export_gem_object *export_gem_create(struct drm_device *ddev,
                                                   size_t size)
{
    struct export_gem_object *obj;
    int ret;

    obj = kzalloc(sizeof(*obj), GFP_KERNEL);
    if (!obj)
        return ERR_PTR(-ENOMEM);

    // 调用DRM核心初始化GEM对象
    ret = drm_gem_object_init(ddev, &obj->base, size);
    if (ret) {
        kfree(obj);
        return ERR_PTR(ret);
    }

    // 分配物理页(实际驱动应使用dma_alloc_coherent或类似API)
    obj->num_pages = PAGE_ALIGN(size) / PAGE_SIZE;
    obj->pages = kcalloc(obj->num_pages, sizeof(struct page *), GFP_KERNEL);
    if (!obj->pages) {
        drm_gem_object_release(&obj->base);
        kfree(obj);
        return ERR_PTR(-ENOMEM);
    }

    // 此处省略实际物理页分配逻辑,生产代码应调用alloc_pages等
    return obj;
}
注意: 上述代码仅为教学骨架,省略了物理页分配和释放的完整实现。实际驱动必须确保内存可被DMA访问,并正确处理drm_gem_object_put引用计数。

4.3 实现gem_prime_export回调

gem_prime_export是GEM对象到DMA-BUF导出的核心入口。DRM核心提供了通用实现drm_gem_prime_export,它会调用驱动注册的gem_prime_get_sg_table回调来获取散列表,然后创建DMA-BUF。因此,驱动只需实现gem_prime_get_sg_table即可。

下面展示完整的导出回调链:

回调函数 作用 必须实现?
gem_prime_export 将GEM对象导出为DMA-BUF fd 否(可用通用实现)
gem_prime_get_sg_table 从GEM对象构造sg_table,描述物理内存布局 是
gem_prime_pin 固定内存,防止导出期间被迁移或释放 否(默认空操作)
gem_prime_unpin 解除固定 否(默认空操作)
static struct sg_table *export_gem_get_sg_table(struct drm_gem_object *obj)
{
    struct export_gem_object *export_obj = to_export_gem_object(obj);
    struct sg_table *sg;
    int ret;

    sg = kzalloc(sizeof(*sg), GFP_KERNEL);
    if (!sg)
        return ERR_PTR(-ENOMEM);

    // 从物理页数组构建散列表
    ret = sg_alloc_table_from_pages(sg, export_obj->pages,
                                    export_obj->num_pages, 0,
                                    obj->size, GFP_KERNEL);
    if (ret) {
        kfree(sg);
        return ERR_PTR(ret);
    }
    return sg;
}

static int export_gem_prime_pin(struct drm_gem_object *obj)
{
    // 对于简单驱动,可以空实现或增加引用计数
    return 0;
}

static void export_gem_prime_unpin(struct drm_gem_object *obj)
{
    // 对应unpin操作
}
流程总结: 用户空间调用DRM_IOCTL_PRIME_HANDLE_TO_FD → DRM核心调用gem_prime_export → 通用实现调用gem_prime_get_sg_table → 驱动返回sg_table → 核心创建DMA-BUF并返回fd。

4.4 导出流程SVG架构图

下图展示了从GEM对象到DMA-BUF文件描述符的完整导出流程,以及驱动各回调的参与位置。

DMA-BUF导出流程架构图 DMA-BUF导出流程:从GEM对象到fd 用户空间 DRM核心 (drm_prime.c) 导出端驱动 IOCTL 回调 步骤1: DRM_IOCTL_PRIME_HANDLE_TO_FD 步骤2: drm_gem_prime_export() 调用 gem_prime_get_sg_table 步骤3: 驱动构建 sg_table,描述物理内存 步骤4: DRM核心创建 dma_buf 对象,关联 sg_table 步骤5: 返回 dma_buf fd 给用户空间 struct drm_gem_object struct sg_table struct dma_buf

4.5 驱动注册与模块加载

完成上述代码后,需要将驱动注册到内核。使用module_platform_driver宏可以简化操作。加载模块后,会在/dev/dri/下创建渲染节点(如renderD128),用户空间可通过该节点进行GEM分配和导出操作。

// 模块加载验证命令
# insmod export_drm.ko
# ls /dev/dri/
card0  renderD128
# cat /sys/class/drm/renderD128/device/driver
export_drm
调试建议: 使用drm_info工具(需安装)查看驱动注册的GEM回调是否生效:drm_info -d /dev/dri/renderD128。重点关注Prime一节中的export和get_sg_table字段。

4.6 常见问题与避坑

  • 未设置DRIVER_GEM标志: 导致GEM对象初始化失败,drm_dev_alloc返回错误。
  • sg_table构建错误: 确保物理页数组有效,且sg_alloc_table_from_pages的偏移和长度参数正确。
  • 内存类型不匹配: 如果使用CMA内存,应使用drm_gem_cma_create而非手动分配,否则导出时可能因非连续内存导致IOMMU映射失败。
  • 引用计数泄漏: 每次drm_gem_object_get必须对应drm_gem_object_put,否则GEM对象无法释放。