!!!声明!!!
本文章转自:何小龙
链接:https://blog.csdn.net/hexiaolong2009/article/details/87392266
转载只是为了学习备份。
没有 mmap 的 dumb buffer 是没有灵魂的!
前言
在之前的《DRM GEM 驱动程序开发(dumb)》中,我们学习了如何编写一个最简单的 GEM 驱动程序,该驱动程序直接使用了 DRM 现成的 CMA Helper 函数来实现 mmap
和 dumb_create
回调接口。
在上一篇《DRM 驱动 mmap 详解:(一)预备知识》中,我们学习了 Linux 驱动常用的 mmap 实现方法,即 一次性映射(remap_pfn_range) 和 按需映射(page fault)。在 DRM 驱动中,CMA Helper 是一次性映射的典型代表,因此本篇我们将仿照 CMA Helper 的具体实现,自己动手写一个 GEM CMA 驱动,以此来展示一次性映射在 DRM 驱动中的典型应用。
CMA Helper
CMA 是 Contiguous Memory Allocator 的缩写,它本身指代的是一种内存分配器(或内存分配策略),专用于分配物理连续的大块内存,以满足大内存需求的设备(如 Display、Camera)。CMA 除了具有内存分配的功能外,还具有内存迁移的功能,使得同一块 CMA 区域既可以被系统使用也可以被专用的 DMA 设备占用,从而大大提高了内存的使用率。要想使用 CMA 内存,需要在内核配置中开启 CONFIG_CMA
配置宏。
CMA helper 则是 DRM 驱动中一组通用的 GEM API,专用于分配、访问物理连续的系统内存,对于那些不带 IOMMU 或不具有专用显存的 Display 硬件而言,极大的方便了它们的 DRM 驱动开发,其中最典型的例子就是 tinydrm 驱动。CMA helper 最早由 Sascha Hauer(来自德国 Pengutronix 公司)基于三星 Exynos 平台开发,并于 2012 年 9 月合入 linux-3.7 主线。
需要注意的是,DRM 中的 CMA helper 和 CMA 本身没有直接关系, 即使当前内核没有使能 CONFIG_CMA
,也不影响 DRM CMA helper 的使用。CMA helper 的内存分配接口使用的是 dma_alloc_wc()
,而内核 CMA 分配器本身是和 DMA 子系统无缝衔接的,当内核开启 CONFIG_CMA 时,dma_alloc_wc() 后端可以采用 CMA 来分配一致性内存;当内核关闭 CONFIG_CMA 配置时,dma_alloc_wc() 则采用系统默认的页分配器来分配连续的物理内存。至于为何要用“CMA”关键字来给 DRM helper 函数命名,这里直接引用作者原话:
The code technically does not depend on CMA as the backend allocator, the name has been chosen because cma makes for a nice, short but still descriptive function prefix.
—— 摘自:[PATCH v4] DRM: add drm gem CMA helper
本来 Sascha 想用 “dma_alloc” 这个关键字,但后来发现这会使函数名变得特别啰嗦,因此最终决定使用 “CMA” 关键字代替。
DRM CMA helper 为我们提供了如下常用的 GEM API:
drm_gem_cma_mmap()
drm_gem_cma_dumb_create()
drm_gem_cma_free_object()
在上一篇我们了解到,一次性映射的典型做法是在 mmap 回调函数中通过调用 remap_pfn_range()
这类映射函数,来直接将已分配好的物理内存一次性映射到用户空间。因此我们不难猜测,drm_gem_cma_mmap() 内部一定调用了 remap_pfn_range() 这类映射接口,而内存分配动作一定是在 drm_gem_cma_dumb_create() 中完成的,事实也确实如此。
接下来我们就自己动手写一个 DRM CMA 驱动。
驱动程序
本源码以《DRM GEM 驱动程序开发(dumb)》为模板,结合《DRM 驱动 mmap 详解:(一)预备知识》中的示例一,参照 drm_gem_cma_helper.c
文件修改而成。
请注意,本源码并没有引用
drm_gem_cma_helper.h
头文件
#include <drm/drmP.h>
#include <drm/drm_gem.h>
struct drm_gem_cma_object {
struct drm_gem_object base;
dma_addr_t paddr;
void *vaddr;
};
static struct drm_device drm;
static int drm_gem_cma_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct drm_gem_cma_object *cma_obj;
drm_gem_mmap(filp, vma);
cma_obj = vma->vm_private_data;
return remap_pfn_range(vma, vma->vm_start, cma_obj->paddr >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot);
}
static int drm_gem_cma_dumb_create(struct drm_file *file_priv,
struct drm_device *drm,
struct drm_mode_create_dumb *args)
{
struct drm_gem_cma_object *cma_obj;
args->pitch = args->width * args->bpp / 8;
args->size = args->pitch * args->height;
cma_obj = kzalloc(sizeof(*cma_obj), GFP_KERNEL);
drm_gem_object_init(drm, &cma_obj->base, args->size);
drm_gem_handle_create(file_priv, &cma_obj->base, &args->handle);
cma_obj->vaddr = dma_alloc_wc(drm->dev, args->size, &cma_obj->paddr,
GFP_KERNEL | __GFP_NOWARN);
return 0;
}
static const struct vm_operations_struct drm_gem_cma_vm_ops = {};
static const struct file_operations mygem_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.mmap = drm_gem_cma_mmap,
};
static struct drm_driver mygem_driver = {
.driver_features = DRIVER_GEM,
.fops = &mygem_fops,
.dumb_create = drm_gem_cma_dumb_create,
.gem_vm_ops = &drm_gem_cma_vm_ops,
.name = "my-gem-cma",
.desc = "My GEM CMA Driver",
.date = "20200901",
.major = 1,
.minor = 0,
};
static int __init mygem_init(void)
{
drm_dev_init(&drm, &mygem_driver, NULL);
drm_dev_register(&drm, 0);
return 0;
}
module_init(mygem_init);
重点
mmap 主要完成如下两步操作:
drm_gem_mmap()
remap_pfn_range()
drm_gem_mmap() 我们后面再讲,大家只需要记住所有的 drm mmap 都需要调用这个函数就可以了。而 remap_pfn_range() 在如今的 CMA Helper 中已看不到它的身影,其实它隐藏在了 dma_mmap_wc()
中。
dumb_create 主要完成如下三步操作:
- 创建 gem-object
- 创建 gem-handle
- 分配 buffer
第1、第2 步是所有 drm dumb_create 都必须实现的操作,而第3步则是可选的。由于一次性映射需要事先分配好所有的 buffer,因此该步骤放在这里是最合适的。
警告: 以上源码为了简单起见,没有实现
gem_free_object_unlocked
回调接口,因此该驱动具有内存泄露的风险。读者在实际项目中还是要把它加上的,具体可以参考原生的drm_gem_cma_free_object()
函数实现,这里就不啰嗦了。
测试程序
这里直接使用《DRM GEM 驱动程序开发(dumb)》中的测试代码:
#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;
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);
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);
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);
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@ubuntu:~# ./dumb
create dumb: handle = 1, pitch = 960, size = 307200
get mmap offset 0x10000000
read from mmap: This is a dumb buffer!
源码下载
Github: cma/dumb.c
测试平台:QEMU vexpress-a9
结语
通过对 CMA Helper 的简化处理,希望本文能帮助大家更好的理解一次性映射在 DRM 驱动中的应用。下一篇,我们将以 vgem 驱动为例,为大家展示按需映射在 DRM 驱动中的具体实现,敬请期待!
参考资料
- [PATCH v4] DRM: add drm gem CMA helper
- LoyenWang:(十六)Linux内存管理之CMA
- 术道经纬:Linux中的Memory Compaction[二] - CMA
- 宋宝华:论Linux的页迁移(Page Migration)完整版
上一篇:DRM 驱动 mmap 详解:(一)预备知识
文章汇总: DRM (Direct Rendering Manager) 学习简介