drm 驱动系列 - 第三章 gem 内存管理


作者: baron

drm 子系统使用 GEM (Graphic Execution Manager) 负责显示显存的分配和释放,他的整体框架如下图所示(图片是高清的, 如果看不清可以下载下来放大). drm 的内存管理分为几个部分组成.

  1. 显存的跟踪和管理这一块由 drm_vma_offset_manager 中 的 drm_mm 完成, 也就是图中的绿色部分
  2. 显存的分配和创建, 也就是图中的蓝色部分
  3. 显存的使用, 显存的使用分为两个部分, 一个是用户空间读写显存也就是红色部分, 另一个则是紫色部分

1、 drm_mm

drm 使用 drm_mm 来管理内存, 它使用 drm_mm_node 来对内存进行分区. 每一个 drm_mm_node 代表一块连续的内存区域. 这块内存可以被认为是被 “分配”或“占用”的,也可以是空闲的。

struct drm_mm {
 
    struct list_head hole_stack;
    struct drm_mm_node head_node; // 内存的分区
    ......
};

显卡驱动如果需要使用到 drm_mm 来管理内存则需要在一开始就调用 void drm_mm_init(struct drm_mm *mm, u64 start, u64 size);分配好需要管理的内存空间大小, 需要注意的是这里并不会实际分配真正的内存. 只是设置一个内存区域. 这个内存区域就我们要用到的总的显存大小, 如果你需要使用双 buffer , 则 size = buffer_size x 2 . 我们调用 drm_mm_init 初始化之后, 就会为我们创建一个 drm_mm 用来管理显存. 并且初始化一个默认的 drm_mm_node 用来表示整个空间的大小.

struct drm_mm_node {
    struct list_head node_list;
    struct list_head hole_stack;
    struct rb_node rb;
    ......
    u64 start;  // 保存地址的偏移量, 也就是我们申请的内存的 offset ==> drm_mode_map_dumb->offset
    u64 size;   // 这块内存的大小
    ......
    struct drm_mm *mm;
};

其中 start 成员变量表示 drm_mm_node 表示的显存的起始偏移地址.也就是我们调用 drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req);时返回的 map_req->offset. 它表示的是内存的相对偏移, 这个 node 对应 drm_mm 中的内存偏移. 他的作用就是索引该 node.

1) 给驱动设置内存

#include <linux/module.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <drm/drmP.h>
#include <drm/drm_mm.h>
#include <drm/drm_vma_manager.h>

#define WIDTH  1920
#define HEIGHT 1080

static struct drm_device *drm;

static const struct file_operations vkms_fops = {
    .owner = THIS_MODULE,
    .open = drm_open,
    .release = drm_release,
    .unlocked_ioctl = drm_ioctl,
    .poll = drm_poll,
    .read = drm_read,
};

static struct drm_driver vkms_driver = {
    .fops           = &vkms_fops,
    .name           = "vkms",
    .desc           = "Virtual Kernel Mode Setting",
    .date           = "20180514",
    .major          = 1,
    .minor          = 0,
};

static int vkms_drm_mm_init(struct drm_device *dev)
{
    struct drm_vma_offset_manager *mgr;

    mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);

    drm->vma_offset_manager = mgr;
    drm_mm_init(&mgr->vm_addr_space_mm, 0, WIDTH * HEIGHT * 2);

    return 0;
}

static void vkms_drm_mm_cleanup(struct drm_device *dev)
{
    kfree(dev->vma_offset_manager);
}

static int __init vkms_init(void)
{

    drm = drm_dev_alloc(&vkms_driver, NULL);
    vkms_drm_mm_init(drm); // 初始化 drm_mm
    drm_dev_register(drm, 0);

    return 0;
}

static void __exit vkms_exit(void)
{
    drm_dev_unregister(drm);
    vkms_drm_mm_cleanup(drm);
    drm_dev_unref(drm);
}

module_init(vkms_init);
module_exit(vkms_exit);

MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("drm mm test drv");
MODULE_LICENSE("GPL");

在这段代码中我们初始化了 drm_mm 并设置了我们要管理的显存大小 1920 x 1080 x 2. 这里只是初始化, 因此我们还不能做什么. 但好歹我们有了一块可以分配的显存区域.为我们后续实验做准备.

2、 显存的分配和创建

drm 子系统已经为我们提供了一个默认的函数 drm_gem_cma_dumb_create用来分配和创建显存, 从名字就可以知道他包含三个部分 drm_gem_object、drm_gem_cma_object、dumb buffer(物理显存), 这个函数遵循先分配在创建的规则. 申请和创建的显存由 drm_gem_object 进行统一管理.


struct drm_gem_cma_object {
    struct drm_gem_object base; // 显存管理接口
    dma_addr_t paddr;           // 显存的物理地址
    struct sg_table *sgt;       // sg_table 本质上是由一块块单个物理连续的 buffer 所组成的链表,可以描述高端内存上分配出的离散 buffer. 也可以用来描述从低端内存上分配出的物理连续 buffe
    void *vaddr;                // 显存的虚拟地址
};

struct drm_gem_object {
    ......
    struct drm_device *dev;
    struct file *filp; // 该 gem 
    struct drm_vma_offset_node vma_node; // 管理分配显存描述符
    ......
    struct dma_buf *dma_buf;             // dma_buf 用于直接将这块显存进行共享
    struct dma_buf_attachment *import_attach;
}

这两个结构体是一体的, 在创建的时候我们创建 drm_gem_cma_object 结构时就会包含 drm_gem_object 接口. 这个技巧可以节省一点内存.其中 drm_gem_cma_object 还包含了我们申请的显存的地址信息. 因此 drm_gem_object 对内存的管理可以分为三个部分:

  1. 分配显存时的显存描述符 drm_vma_offset_node
  2. 分配到的物理内存的地址信息
  3. 提供 dma_buf 接口让显存实现共享.

drm_gem_cma_dumb_create 首先在 drm_mm 中申请一片需要的空间描述符, 然后再创建实际的物理内存. 上图中的蓝色的 drm_mm_node 就是 user 使用 libdrm 分配显存时申请到的内存空间描述符.之后再调用 dma_alloc_wc 分配实际的物理内存到drm_gem_object->drm_gem_cma_object. 具体流程如下所示.

drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create); --> // 开放给上层的 libdrm 接口
    drm_gem_cma_dumb_create() -->
        drm_gem_cma_create_with_handle() -->
            drm_gem_cma_create() -->
                __drm_gem_cma_create() -->
                    if (drm->driver->gem_create_object) // 回调 gem_create_object 接口创建 gem_obj 
                        gem_obj = drm->driver->gem_create_object(drm, size); 
                    else
                        gem_obj = kzalloc(sizeof(*cma_obj), GFP_KERNEL); // 创建一个 gem obj
                    drm_gem_create_mmap_offset() -->    
                        drm_gem_create_mmap_offset_size() -->
                            drm_vma_offset_add() -->
                                drm_mm_insert_node() --> // 在 drm_mm 中查找到空闲可用的空间, 然后插入一个 drm_vma_offset_node 表示这个可用的空间.
                dma_alloc_wc() // 申请物理内存, 并将内存地址保存到 cma_obj

drm_gem_cma_dumb_create 主要实现了下面功能.

  1. 创建一个 cma_obj , 并且 gem_obj 是其成员变量. 初始化 gem_obj. 支持用户客制化, 用户需要自己实现这个接口 drm->driver->gem_create_object(drm, size);
  2. 在 dr_mm 中查找一段符合长度的内存空间, 并将 gem_obj->vma_node 插入到 drm_mm 并且占有这块空间. 即分配过程.
  3. 拿到这一段空间之后调用 dma_alloc_wc 申请实际的物理空间并将地址信息保存到 cma_obj.
  4. 在 drm_file->object_idr 中申请一个 handle, 用来关联 gem obj. 并返回给用户空间, 用户通过这个 handle 即可找到这个 gem.

1) 给驱动增加分配显存接口

给我们的驱动加上创建和分配的功能即 drm_gem_cma_dumb_create, 为了能找到对应的 node 因此提供 drm_gem_dumb_map_offset , 改接口用于返回 node 对应的 start 偏移.

#include <linux/module.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <drm/drmP.h>
#include <drm/drm_mm.h>
#include <drm/drm_vma_manager.h>

#define WIDTH  1920
#define HEIGHT 1080

static struct drm_device *drm;

static const struct file_operations vkms_fops = {
    .owner = THIS_MODULE,
    .open = drm_open,
    .release = drm_release,
    .unlocked_ioctl = drm_ioctl,
    .poll = drm_poll,
    .read = drm_read,
};

static struct drm_driver vkms_driver = {
    .fops           = &vkms_fops,
    .driver_features    =  DRIVER_GEM,
    .name           = "vkms",
    .desc           = "Virtual Kernel Mode Setting",
    .date           = "20180514",
    .major          = 1,
    .minor          = 0,
    .dumb_create    = drm_gem_cma_dumb_create, // 在 drm_mm 中申请一个 node , 并分配物理内存
    .dumb_map_offset = drm_gem_dumb_map_offset, // 返回 node 中的 start 内存偏移, 即该 node 的索引
};

static int vkms_drm_mm_init(struct drm_device *dev)
{
    struct drm_vma_offset_manager *mgr;

    mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);

    drm->vma_offset_manager = mgr;
    drm_mm_init(&mgr->vm_addr_space_mm, 0, WIDTH * HEIGHT * 2);

    return 0;
}

static void vkms_drm_mm_cleanup(struct drm_device *dev)
{
    kfree(dev->vma_offset_manager);
}

static int __init vkms_init(void)
{

    drm = drm_dev_alloc(&vkms_driver, NULL);

    vkms_drm_mm_init(drm); // 初始化 drm_mm

    drm_dev_register(drm, 0);

    return 0;
}

static void __exit vkms_exit(void)
{
    drm_dev_unregister(drm);
    vkms_drm_mm_cleanup(drm);
    drm_dev_unref(drm);
}

module_init(vkms_init);
module_exit(vkms_exit);

MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("drm mm test drv");
MODULE_LICENSE("GPL");

3、用户空间读写显存

经过前面的操作, 我们已经能够分配并且创建显存了, 现在我们需要在用户空间读写显存, 因此需要将显存映射到用户空间, 映射的方式有很多种. drm 子系统默认提供了 drm_gem_cma_mmap 接口来进行映射.

drm_gem_cma_mmap() --> 
    drm_gem_mmap(filp, vma); --> //  从 vma_offset_manager 中查找  vm_pgoff 偏移对应的 node, 通过 node 返回对应的 drm_gem_object, 对 vma 进行一初始化
        vma->vm_ops = dev->driver->gem_vm_ops; // drm_gem_mmap 中设置了 vm_ops 的回调, 在我们驱动中也要实现, 不然会 mmap 失败. 博主也不知道为啥, 有知道的同学可以告知一下.
    drm_gem_cma_mmap_obj(cma_obj, vma); --> 
        dma_mmap_wc(cma_obj->base.dev->dev, vma, cma_obj->vaddr, cma_obj->paddr, vma->vm_end - vma->vm_start); // 把 cma_obj 中描述的物理内存返映射到用户空间

1) 编程实验

一共增加了两个个接口 drm_gem_cma_mmap、drm_gem_cma_vm_ops

#include <linux/module.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <drm/drmP.h>
#include <drm/drm_mm.h>
#include <drm/drm_vma_manager.h>

#define WIDTH  1920
#define HEIGHT 1080

static struct drm_device *drm;

static const struct file_operations vkms_fops = {
    .owner = THIS_MODULE,
    .open = drm_open,
    .release = drm_release,
    .unlocked_ioctl = drm_ioctl,
    .poll = drm_poll,
    .read = drm_read,
    .mmap = drm_gem_cma_mmap, // 实现 mmap 操作
};

static struct drm_driver vkms_driver = {
    .fops           = &vkms_fops,
    .driver_features    =  DRIVER_GEM,
    .name           = "vkms",
    .desc           = "Virtual Kernel Mode Setting",
    .date           = "20180514",
    .major          = 1,
    .minor          = 0,
    // 在 drm_gem_cma_mmap 中会设置这个回调函数. 在我们驱动中也要实现, 
    // 不然会 mmap 失败. 博主也不知道为啥, 有知道的同学可以告知一下.
    .gem_vm_ops     = &drm_gem_cma_vm_ops,     
    .dumb_create    = drm_gem_cma_dumb_create, // 在 drm_mm 中申请一个 node , 并分配物理内存
    .dumb_map_offset = drm_gem_dumb_map_offset, // 返回 node 中的 start 内存偏移, 即该 node 的索引
};

static int vkms_drm_mm_init(struct drm_device *dev)
{
    struct drm_vma_offset_manager *mgr;

    mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);

    drm->vma_offset_manager = mgr;
    drm_mm_init(&mgr->vm_addr_space_mm, 0, WIDTH * HEIGHT * 2);

    return 0;
}

static void vkms_drm_mm_cleanup(struct drm_device *dev)
{
    kfree(dev->vma_offset_manager);
}

static int __init vkms_init(void)
{

    drm = drm_dev_alloc(&vkms_driver, NULL);

    vkms_drm_mm_init(drm); // 初始化 drm_mm

    drm_dev_register(drm, 0);

    return 0;
}

static void __exit vkms_exit(void)
{
    drm_dev_unregister(drm);
    vkms_drm_mm_cleanup(drm);
    drm_dev_unref(drm);
}

module_init(vkms_init);
module_exit(vkms_exit);

MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("drm mm test drv");
MODULE_LICENSE("GPL");

用户空间对显存进行读写. 这个程序就是照抄的龙哥的程序 https://blog.csdn.net/hexiaolong2009/article/details/106532966

#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

int main(int argc, char **argv)
{
        int fd;
        char *vaddr;
        struct drm_mode_create_dumb create_req = {};
        struct drm_mode_destroy_dumb destroy_req = {};
        struct drm_mode_map_dumb map_req = {};

        fd = open("/dev/dri/card0", O_RDWR);

        create_req.bpp = 32;
        create_req.width = 240;
        create_req.height = 320;


        //  在 drm_vma_offset_manager 中分配一个 240 x 320 的显存, 并返回对应 gem obj 的 handle.
        drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_req);
        printf("create dumb: handle = %u, pitch = %u, size = %llu\n",
                create_req.handle, create_req.pitch, create_req.size);

        // 通过 handle 找到 gem, 返回对应 node 的 start 偏移 == map_req.offset
        map_req.handle = create_req.handle;
        drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req);
        printf("get mmap offset 0x%llx\n", map_req.offset);

        // 使用这个 offeet 找到对应 node 并映射其描述的内存
        vaddr = mmap(0, create_req.size, PROT_WRITE, MAP_SHARED, fd, map_req.offset);
        strcpy(vaddr, "This is a dumb buffer!");
        munmap(vaddr, create_req.size);

        // 使用这个 offeet 找到对应 node 并映射其描述的内存
        vaddr = mmap(0, create_req.size, PROT_READ, MAP_SHARED, fd, map_req.offset);
        printf("read from mmap: %s\n", vaddr);
        munmap(vaddr, create_req.size);

        getchar();

        destroy_req.handle = create_req.handle;
        drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_req);
        close(fd);

        return 0;
}

运行结果:

[root@100ask:/dmabuf/2]# insmod drm_read_gpu.ko // 加载模块 ===> 不要纠结名字, 随便起的
[root@100ask:/dmabuf/2]#
[root@100ask:/dmabuf/2]#
[root@100ask:/dmabuf/2]# ./a.out
create dumb: handle = 1, pitch = 960, size = 307200
get mmap offset 0x0
read from mmap: This is a dumb buffer!
Linux DRM(Direct Rendering Manager)驱动Linux操作系统中用于显卡和显示设备管理的驱动程序。它提供了一套统一的接口,使得用户可以通过应用程序直接访问显卡硬件,实现图形渲染和硬件加速等功能。 Linux DRM驱动包括了核心驱动模块和设备驱动模块。核心驱动模块负责提供基本的显卡管理功能,如内存分配、模式设置和渲染命令队列等。设备驱动模块负责支持特定显卡和显示设备的硬件特性和功能。通过这种模块化的设计,Linux DRM驱动能够支持多种显卡和显示设备。 在分析Linux DRM驱动时,可以从以下几个方面入手: 1. 驱动架构:了解Linux DRM驱动的整体架构,包括核心驱动模块和设备驱动模块的功能和交互方式。 2. 设备支持:分析特定显卡和显示设备的驱动模块,了解其支持的硬件特性和功能。 3. 内存管理:了解Linux DRM驱动中的内存管理机制,包括内存分配、显存管理和页面交换等。 4. 渲染和硬件加速:分析Linux DRM驱动中的渲染命令队列和硬件加速功能的实现方式。 5. 显示管道:了解Linux DRM驱动中的显示管道管理机制,包括模式设置和输出控制等。 通过对Linux DRM驱动的分析,可以更深入地理解显卡和显示设备的工作原理,为开发应用程序和调优系统性能提供指导和参考。此外,还可以通过分析和改进Linux DRM驱动来实现新的显卡和显示设备的支持,提升系统的兼容性和性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值