2.10字符设备驱动之内存映射(mmap实现)

为什么要将内核空间的内存映射到用户空间

有些驱动在使用时需要频繁的操作内核空间的某一片内存(如显示屏驱动,需要频繁的读写显存),若采用传统的read和write会存在大量的内存拷贝(因为用户空间无法直接访问内核空间的地址),这将降低程序效率,此时可以将内核空间虚拟地址所对应的物理内存映射到用户空间,以此减少内存拷贝。

内存映射时的应用层操作

在应用层可以通过函数 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) 将文件(包括设备文件)映射到应用层的虚拟地址空间,此时应用程序便可以通过映射后的虚拟地址访问文件,在使用完成后应用层还应调用 int munmap(void *addr, size_t length) 来取消 mmap 的映射。

内存映射时的驱动层实现

当应用层调用 mmap 时会调用到驱动层的 int (*mmap) (struct file *filp, struct vm_area_struct *vma) 函数,此函数将根据用户传递的参数调用 int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot) 完成内存映射,下面对着两个函数进行详细介绍:

/** 对应到应用层的mmap函数
 * filp 文件句柄
 * vma 用于描述一个独立的虚拟内存区域
 */
int (*mmap) (struct file *filp, struct vm_area_struct *vma)

/** 将物理地址映射到虚拟地址空间,对于物理上不连续的内存空间可以循环调用此函数将这些不连续的物理内存映射到连续的虚拟地址空间
 * vma 用于描述一个独立的虚拟内存区域
 * addr 起始虚拟地址
 * pfn 物理内存页框号
 * size 映射空间的大小,最小为一页内存
 * prot 访问权限,如果不想被cache可以采用prot = pgprot_noncached(prot)方式添加nocache标志
 */
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot)

如何获取物理内存页框号

采用 __get_free_pages、__get_free_page、kmalloc 分配的用 virt_to_phys 可将虚拟地址转换为物理地址,然后将物理地址左移 PAGE_SHIFT 位即可的到物理地址页框号
采用 vmalloc 分配的用 vmalloc_to_pfn 可将虚拟地址转换为物理地址页框号

驱动代码实现

驱动程序以一个共享内存实现,它在内核空间分配一片内存,应用层可以通过read读取内存中的内容,也可以通过mmap将内存映射到应用层虚拟地址空间,如下是相应的代码实现,需要注意的是如果内核空间的内存在物理地址上不连续(如采用vmalloc分配),而期望映射到应用层虚拟地址空间是一片连续的地址时可以通过多次remap_pfn_range将物理上不连续的内存空间映射到连续的虚拟地址空间

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//#define USING_VMALLOC

#define GMEM_ORDER		3
#define GMEM_PAGES		(2^(GMEM_ORDER))
#define GMEM_SIZE		(GMEM_PAGES*PAGE_SIZE)

//次设备号,为MISC_DYNAMIC_MINOR表示自动分配
#define GMEM_MINOR		MISC_DYNAMIC_MINOR
//设备文件名
#define GMEM_NAME		"gmem"

//全局内存地址
static uint8_t *gmem_buffer;

//打开设备
static int gmem_open(struct inode *inode, struct file *file)
{
	return 0;
}

//释放设备
int gmem_release(struct inode *inode, struct file *file)
{
	return 0;
}

//读数据
ssize_t gmem_read(struct file *file, char __user *buffer, size_t size, loff_t *pos)
{
	int ret;
	size_t length = (size > GMEM_SIZE) ? GMEM_SIZE : size;

	//拷贝数据到应用层
	ret = copy_to_user(buffer, gmem_buffer, length);

	return length - ret;
}


static int gmem_mmap(struct file *file, struct vm_area_struct *vma)
{
	int result = 0;

#ifdef USING_VMALLOC
	/* 采用vmalloc分配的内存在物理上不连续,所以进行映射时最好一页一页的进行映射 */
	//获取应用层传递的偏移
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
	//计算偏移后的内核空间虚拟地址
	unsigned long map_offset = (unsigned long)gmem_buffer + offset;
	//根据内核空间虚拟地址计算物理地址页框号
	unsigned long pfn_start = (unsigned long)vmalloc_to_pfn((void*)map_offset);
	//计算期望映射的大小
	unsigned long size = vma->vm_end - vma->vm_start;
	//用户空间虚拟地址起始值,可能是用户指定,可能是系统分配
	unsigned long vmstart = vma->vm_start;

	//映射大小必须是PAGE_SIZE的整倍数
	if((size % PAGE_SIZE) != 0)
		size = (size + PAGE_SIZE) / PAGE_SIZE;
	//最大不超过全局内存大小
	if(size > GMEM_SIZE)
		size = GMEM_SIZE;
	//增加no cache属性
	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

	printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\r\n", pfn_start << PAGE_SHIFT, offset, size);
	/* 对 vmalloc 分配的内存一页一页的进行映射 */
	while (1) 
	{
		//进行内存映射,每次映射一页,因为vmalloc申请的内存可能不连续
		result = remap_pfn_range(vma, vmstart, pfn_start, PAGE_SIZE, vma->vm_page_prot);
		if(result != 0)
		{
			printk("remap_pfn_range failed at \r\n");
			break;
		}

		//完成对所有内存页的映射
		if(size <= PAGE_SIZE)
			break;

		//剩余大小递减
		size -= PAGE_SIZE;

		//用户空间虚拟地址递加
		vmstart += PAGE_SIZE;

		//内核空间虚拟地址递加
		map_offset += PAGE_SIZE;
		//根据内存地址计算物理地址页框号
		pfn_start = vmalloc_to_pfn((void *)map_offset);
	}

	return result;
#else
	//获取应用层传递的偏移
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
	//计算偏移后的内核空间虚拟地址
	unsigned long map_offset = (unsigned long)gmem_buffer + offset;
	//根据内核空间虚拟地址计算物理地址页框号
	unsigned long pfn_start = virt_to_phys((void*)map_offset) >> PAGE_SHIFT;
	//计算期望映射的大小
	unsigned long size = vma->vm_end - vma->vm_start;
	//用户空间虚拟地址起始值,可能是用户指定,可能是系统分配
	unsigned long vmstart = vma->vm_start;

	//映射大小必须是PAGE_SIZE的整倍数
	if((size % PAGE_SIZE) != 0)
		size = (size + PAGE_SIZE) / PAGE_SIZE * PAGE_SIZE;;
	//最大不超过全局内存大小
	if(size > GMEM_SIZE)
		size = GMEM_SIZE;
	//增加no cache属性
	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

	//进行内存映射
	printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\r\n", pfn_start << PAGE_SHIFT, offset, size);
	result = remap_pfn_range(vma, vmstart, pfn_start , size, vma->vm_page_prot);
	
	return result;
#endif
}

struct file_operations fops = {
	.open = gmem_open,
	.release = gmem_release,
	.read = gmem_read,
	.mmap = gmem_mmap,
};

struct miscdevice gmem_misc = {
	.minor = GMEM_MINOR,
	.name = GMEM_NAME,
	.fops = &fops,
};
static int __init gmem_init(void)
{
	int err = 0;

	printk("global memory init\r\n");

#ifdef USING_VMALLOC
	//采用vmalloc分配内存
	gmem_buffer = vmalloc(GMEM_SIZE);
#else
	//采用__get_free_pages分配内存
	gmem_buffer = (uint8_t *)__get_free_pages(GFP_KERNEL, GMEM_ORDER);
#endif
	if(gmem_buffer == NULL)
	{
		printk("allocate mem failed\r\n");
		return -ENOMEM;
	}
	printk("gmem_buffer = 0x%lx\r\n", (unsigned long)gmem_buffer);

	//注册misc设备
	err = misc_register(&gmem_misc);
	if(err != 0)
	{
		printk("register misc failed\r\n");
		return err;
	}
	return 0;
}

static void __exit gmem_exit(void)
{
	printk("global memory exit\r\n");

#ifdef USING_VMALLOC
	//采用vmalloc分配的内存必须用vfree释放
	vfree(gmem_buffer);
#else
	//采用__get_free_pages分配的内存必须用free_pages释放
	free_pages((unsigned long)gmem_buffer, GMEM_ORDER);
#endif
	//注销misc设备
	misc_deregister(&gmem_misc);
}

module_init(gmem_init);
module_exit(gmem_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lf");
MODULE_DESCRIPTION("mmap test");
MODULE_ALIAS("gmem");

驱动测试程序实现

驱动测试程序主要验证mmap操作是否成功,它首先通过mmap将内核空间的内存映射到应用层虚拟地址空间,然后通过读写内存的方式向其中写入数据,最好在通过read函数将写入的数据读取出来

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char * argv[])
{
	int fd;
	char *start;
	int i;
	char buf[32];

	//打开设备
	fd = open("/dev/gmem", O_RDWR);
	if (fd == -1)
		goto fail;

	//映射内核空间虚拟地址到用户空间,虚拟地址系统自动分配,映射大小32B(实际映射了一页),支持读写,共享,便宜为0
	start = mmap(NULL, 32, PROT_READ|PROT_WRITE, MAP_SHARED, fd, sysconf(_SC_PAGESIZE)*0);
	if (start == MAP_FAILED)
		goto fail;

	//采用内存拷贝方式写入“abcdefghijklmnopqrstuvwxyz”
	for (i = 0; i < 26; i++)
		*(start + i) = 'a' + i;
	*(start + i) = '\0';

	//通过read函数读取内核空间共享内存的数据
	if (read(fd, buf, 27) == -1)
		goto fail;

	//输出读取的数据,这里应该是"abcdefghijklmnopqrstuvwxyz"
	puts(buf);

	//取消mmap的映射
	munmap(start, 32);

	//关闭设备
	close(fd);

	return 0;

fail:
	perror("mmap test");
	exit(EXIT_FAILURE);
}

上机实验

  1. 从这里下载代码,进行编译,并拷贝到目标板跟文件系统的root目录中
  2. 通过命令加载insmod mmap.ko驱动,然后再通过命令./app.out执行,可见应用程序输出字符串”abcdefghijklmnopqrstuvwxyz“,这个字符串首先通过mmap映射得到的地址写入内存,然后又通过read函数读出并输出
    2.10字符设备驱动之内存映射(mmap实现)_第1张图片

你可能感兴趣的:(linux,arm开发,驱动开发,c语言)