DRM 驱动 mmap 详解:(一)预备知识

!!!声明!!!
本文章转自:何小龙
链接:https://blog.csdn.net/hexiaolong2009/article/details/87392266
转载只是为了学习备份。

视频:三种 mmap 驱动实现方法

前言

在上一篇《DRM GEM 驱动程序开发(dumb)》我们学习了如何编写一个最简单的 DRM GEM 驱动程序。该驱动程序只提供了 dumb buffer 的操作能力,允许应用程序对 dumb buffer 进行 create 和 mmap 操作。

dumb buffer 操作虽简单,但在 drm 驱动目录下,各个厂家的实现却是五花八门。因为 dumb buffer 只能通过 mmap 来访问,因此本系列文章将带着大家一起来深入学习下 DRM 中的 mmap 驱动开发。

在正式讲解 DRM mmap 之前,我觉得有必要先让大家了解一个普通的 mmap 驱动应该如何编写。本文并不打算讲解 mmap 系统调用的原理及其相关细节,因为这涉及到 linux 内存管理的诸多概念,大家只需要了解如何去写一个简单的 mmap 驱动程序就可以了,为后续 drm mmap 驱动的编写做准备。如果大家阅读完本文后,想再进一步学习 mmap 系统调用的相关知识,推荐大家阅读彭东林的博客《内存映射函数 remap_pfn_range 学习》和胡潇的博客《认真分析mmap:是什么 为什么 怎么用》

正文

在 kernel 驱动中,实现 mmap 系统调用离不开两个关键步骤:(1)内存分配 (2) 建立映射关系。这刚好也对应了 DRM 中的 dumb_createmmap 操作。

先说映射关系,在 linux 驱动中建立映射关系的方法主要有如下两种:

  • 一次性映射 —— 在 mmap 回调函数中,一次性建立好整块内存的映射关系,通常以 remap_pfn_range() 为代表 。
  • Page Fault —— mmap 先不建立映射关系,等上层触发缺页异常时,在 fault 中断处理函数中建立映射关系,缺哪块补哪块,通常以 vm_insert_page() 为代表。

而内存分配的时机也会影响驱动程序的设计,大致分为如下三种:

  • 在 mmap 系统调用之前分配
  • 在 mmap 系统调用过程中分配
  • 在 fault 中断处理函数中分配

因此不同的分配时机 + 不同的映射机制,就会得到不同的 mmap 的实现策略。这也是为什么在 DRM 驱动中,各家的 dumb_createmmap 实现代码差异很大的原因。

下面就以示例代码的形式为大家展示几种典型的 mmap 驱动实现方式。

示例一

mmap 之前分配 + 一次性映射:

在这里插入图片描述

描述:

  1. 驱动初始化时先分配好 3 个 PAGE。
  2. 上层执行 mmap 系统调用时,在底层 mmap 回调函数中通过 remap_pfn_range() 一次性建立好所有的映射关系,并将映射后的起始虚拟地址返回给应用程序。
  3. 应用程序使用返回的虚拟地址进行内存读写操作。

驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static void *kaddr;

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
    
    
	return remap_pfn_range(vma, vma->vm_start,
				(virt_to_phys(kaddr) >> PAGE_SHIFT) + vma->vm_pgoff,
				vma->vm_end - vma->vm_start, vma->vm_page_prot);
}

static struct file_operations my_fops = {
    
    
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
    
    
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
    
    
	kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);
	return misc_register(&mdev);
}
module_init(my_init);

示例二

mmap 之前分配 + Page Fault:

在这里插入图片描述

描述:

  1. 驱动初始化时预先分配好 3 个 PAGE。
  2. 上层执行 mmap 系统调用,底层驱动在 mmap 回调函数中不建立映射关系,而是将本地实现的 vm_ops 挂接到进程的 vma->vm_ops 指针上,然后函数返回。
  3. 上层获取到一个未经映射的进程地址空间,并对其进行内存读写操作,导致触发缺页异常。缺页异常最终会调用前面挂接的 vm_ops->fault() 回调接口,在该接回调中通过 vm_insert_page() 建立物理内存与用户地址空间的映射关系。
  4. 异常返回后,应用程序就可以继续之前被中断的读写操作了。

注意:这种情况每次 Page Fault 中断只能映射一个 Page。

驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static void *kaddr;

static int my_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	int offset, ret;

	offset = vmf->pgoff * PAGE_SIZE;
	ret = vm_insert_page(vma, vmf->address, virt_to_page(kaddr + offset));
	if (ret)
		return VM_FAULT_SIGBUS;

	return VM_FAULT_NOPAGE;
}

static const struct vm_operations_struct vm_ops = {
    
    
	.fault = my_fault,
};

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
    
    
	vma->vm_flags |= VM_MIXEDMAP;
	vma->vm_ops = &vm_ops;
	return 0;
}

static struct file_operations my_fops = {
    
    
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
    
    
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
    
    
	kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);
	return misc_register(&mdev);
}
module_init(my_init);

DRM 驱动典型代表: tegraudl

示例三

Page Fault 中分配 + 映射:

在这里插入图片描述

描述:
映射的过程和示例二完全一样,只是内存分配的时机是在 page fault 中断处理函数中进行的。

驱动代码:

这里为了简化代码,总共只分配一个 page,多个 page 可通过 vmf->pgoff 来进行区分。

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static struct page *page;

static int my_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	int ret;

	if (!page)
		page = alloc_page(GFP_KERNEL);

	ret = vm_insert_page(vma, vmf->address, page);
	if (ret)
		return VM_FAULT_SIGBUS;

	return VM_FAULT_NOPAGE;
}

static const struct vm_operations_struct vm_ops = {
    
    
	.fault = my_fault,
};

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
    
    
	vma->vm_flags |= VM_MIXEDMAP;
	vma->vm_ops = &vm_ops;
	return 0;
}

static struct file_operations my_fops = {
    
    
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
    
    
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
    
    
	return misc_register(&mdev);
}
module_init(my_init);

DRM 驱动典型代表: vkmsvgem

示例四

同示例二(mmap 之前分配 + Page Fault 映射),区别在于 fault 中断处理函数,不再使用 vm_insert_page() 来映射,而是直接将物理 page 返回给 vmf->page 指针。

驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static void *kaddr;

static int my_fault(struct vm_fault *vmf)
{
    
    
	vmf->page = virt_to_page(kaddr + vmf->pgoff * PAGE_SIZE);
	get_page(vmf->page);
	return 0;
}

static const struct vm_operations_struct vm_ops = {
    
    
	.fault = my_fault,
};

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
    
    
	vma->vm_flags |= VM_MIXEDMAP;
	vma->vm_ops = &vm_ops;
	return 0;
}

static struct file_operations my_fops = {
    
    
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
    
    
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
    
    
	kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);
	return misc_register(&mdev);
}
module_init(my_init);

注意:使用 vmf->page 方式映射时,fault 中断函数返回值应该是 0。而使用 vm_insert_page() 方式映射时,返回值应该是 VM_FAULT_NOPAGE

DRM 驱动典型代表: vkmsvgem

示例五

mmap 中分配 + Page Fault 映射: 代码其实和示例三(Page Fault 中分配 + 映射)差异不大,只是在 mmap 回调中进行内存分配,这里就不演示了。

DRM 驱动典型代表: shmem(linux 4.19)

测试程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    
    
	int fd;

	fd = open("/dev/my_dev", O_RDWR);

	char *str = mmap(NULL, 4096 * 3, PROT_WRITE, MAP_SHARED, fd, 0);
	strcpy(str, "hello world\n");
	munmap(str, 4096 * 3);

	str = mmap(NULL, 4096 * 3, PROT_READ, MAP_SHARED, fd, 0);
	printf("%s\n", str);
	munmap(str, 4096 * 3);

	close(fd);

	return 0;
}

描述:
先后执行 2 次 mmap/munmap 操作,第一次为写操作,第二次为读操作。

运行结果:

# ./a.out
hello world

结语

不管是做 DRM 驱动开发,还是其它设备驱动开发,上面的示例都值得参考。希望通过本文,能让初学者对 mmap 驱动的实现有个大致的认识,这样我们在后续的 DRM 驱动讲解中才能做到心中有数。

源码下载

Github: sample-code/mmap
测试平台:QEMU vexpress-a9

参考资料

  1. 《内存映射函数 remap_pfn_range 学习》
  2. 《认真分析mmap:是什么 为什么 怎么用》
  3. LWN: fault()

扩展阅读:《dma-buf 由浅入深(四) —— mmap》
文章汇总: DRM (Direct Rendering Manager) 学习简介

猜你喜欢

转载自blog.csdn.net/qq_38350702/article/details/114266189