作者: baron
drm 子系统使用 GEM (Graphic Execution Manager) 负责显示显存的分配和释放,他的整体框架如下图所示(图片是高清的, 如果看不清可以下载下来放大). drm 的内存管理分为几个部分组成.
- 显存的跟踪和管理这一块由 drm_vma_offset_manager 中 的 drm_mm 完成, 也就是图中的绿色部分
- 显存的分配和创建, 也就是图中的蓝色部分
- 显存的使用, 显存的使用分为两个部分, 一个是用户空间读写显存也就是红色部分, 另一个则是紫色部分
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 对内存的管理可以分为三个部分:
- 分配显存时的显存描述符 drm_vma_offset_node
- 分配到的物理内存的地址信息
- 提供 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 主要实现了下面功能.
- 创建一个 cma_obj , 并且 gem_obj 是其成员变量. 初始化 gem_obj. 支持用户客制化, 用户需要自己实现这个接口
drm->driver->gem_create_object(drm, size);
- 在 dr_mm 中查找一段符合长度的内存空间, 并将 gem_obj->vma_node 插入到 drm_mm 并且占有这块空间. 即分配过程.
- 拿到这一段空间之后调用 dma_alloc_wc 申请实际的物理空间并将地址信息保存到 cma_obj.
- 在 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!