Linux内存映射-mmap

1.mmap简介

mmap可以将文件或者其他对象映射到内存中,即将一个文件或者其它对象的地址空间映射到进程的地址空间,实现了文件磁盘地址和进程一段虚拟地址的对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写这一段内存,系统会自动将映射文件读取到映射的内存空间当中,同时将脏页回写到对应的文件磁盘上,这样就完成了文件的读写操作,而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也可直接反映到用户空间,从而可以实现不同进程间的文件共享。函数原型如下:

 	void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
	addr:映射虚拟地址空间的起始地址,如为NULL,则由内核自动分配。
	length:映射虚拟地址空间的大小,系统自动取为页的整数倍,不足一页,按一页映射。
	port:描述了映射内存的权限,可取PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE。
	flags:描述了映射空间更新后对映射了相同区域的进程是否可见及是否将文件写回。常用一下选项。
		MAP_SHARED:映射空间更新后对映射了相同区域的进程可见,可将脏页写回映射的文件
		MAP_PRIVATE:采用了私有copy-on-write的映射方式,写入时会拷贝一份副本,在副本上更新,
		             这些数据不会写回文件,映射空间更新后对映射相同区域的进程不可见。
	fd:映射对象的文件句柄,在映射之前,必须先打开对象,得到文件描述符。
	offset:映射对象地址空间的偏移值。
	返回值:映射成功,返回映射虚拟地址空间的首地址,失败返回(void *)-1

关于长度length和偏移值offset的说明:
length:传入的长度只要大于0即可,可以不是页的整数倍。但内核按最大页的整数倍进行映射,不足一页,按一页映射。测试发现,映射长度为100字节,读取的字节只要不超过4096字节,都可正常读取,映射长度为4096字节,读取4000-4500之间的数据,4000-4096之间的数据可正常读取,超过4096后显示为乱码且读取字节数不正确,说明内核按整页进行映射。
offset:经过测试发现,offset必须为页的整数倍,如不是页的整数倍,映射会失败,错误码为22,显示无效的参数。

2.mmap在内核中的执行流程

mmap为POSIX标准定义的系统调用,内核中调用的是CALL(OBSOLETE(sys_old_mmap)),属于比较老的系统调用。Linux内核从2.3.31版本开始,提供了进行内存映射的私有系统调用mmap2
两者之间的差别很小,区别在于mmap进入内核后会将用户空间的参数拷贝到内核空间,然后检查偏移值是否是页的整数倍,如不是,则返回错误,表示无效的参数,这和之前的测试结果保持一致,后续执行的函数就和mmap2一致了。mmapmmap2函数在内核中的执行流程如图1所示。主要流程如下:

  1. 根据传入的fd,找到打开文件对应的file结构体。
  2. 从进程的虚拟地址空间分配虚拟地址范围
  3. 检查分配的虚拟地址范围
  4. 如不能和已有的虚拟内存区域合并,则分配内存区域
  5. 调用具体设备中实现的mmap函数,这里就执行/dev/mem设备注册的mmap函数
  6. 将虚拟内存区域添加到管理虚拟内存的链表和红黑树中
  7. 设置内存页保护标志
    Linux内存映射-mmap_第1张图片
    每一个具体的(虚拟)设备,都要向系统注册一个操作函数的结构体,常见的openwriteread函数都在里面,mmap函数就包含在里面,若设备要支持内存映射,就必须实现mmap函数。
    Linux内存映射-mmap_第2张图片
    /dev/mem设备注册的结构体如下图所示。
    Linux内存映射-mmap_第3张图片
    对于/dev/mem设备来说,应用层调用mmap系统调用,最终要调用到/dev/mem设备在内核中注册的mmap函数,其执行流程如下图所示,主要流程如下:
  8. 检查映射的物理地址是否超过支持的物理地址范围
  9. 是否支持私有映射,CPU有MMU则默认支持
  10. 检查物理地址是否被允许映射,此选项由CONFIG_STRICT_DEVMEM控制,如定义则会对映射的地址进行严格的检查,如没有定义,则不检查(CONFIG_STRICT_DEVMEM的配置路径:Kernel hacking ---> [ ] Filter access to /dev/mem)。
  11. 如开启了检查选项,将会检查映射的物理地址是否是设备独占保留的IO映射区域、是否是系统内存、物理内存的访问权限是否符合要求。
  12. 通过后,则设置页的访问权限
  13. 将内核中的物理地址映射到用户空间。
    Linux内存映射-mmap_第4张图片

3.mmap在内核中的执行流程

mem设备节点位于dev目录下,/dev/mem是物理地址空间的全映像,可以用来访问CPU的物理地址空间,一般用法是open("/dev/mem",O_RDWR),得到mem设备的文件描述符,接着用mmap来映射物理内存或外设的IO资源,利用mmap返回的虚拟地址,可以操作映射的物理内存或外设的IO资源,这就是实现用户空间硬件驱动的方法。mem的驱动文件名为mem.c,位于内核源码drivers/char目录下。

4.mmap驱动源码

如果不使用/dev/mem设备,可自己实现mmap驱动,映射想要映射的地址空间。

	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	
	#define DEVICE_NAME1 	"mem_mmap1"
	#define DEVICE_NAME2 	"mem_mmap2"
	static const char drv_name1[] = "mem_mmap-program1";
	static const char drv_name2[] = "mem_mmap-program2";
	#define MEM_MMAP_ADDR1  (0x90000000) /* 映射内存的物理基地址 */
	#define MEM_MMAP_ADDR2  (MEM_MMAP_ADDR1 + MEM_MMAP_LEN)
	#define MEM_MMAP_LEN    (0x4000000) /* 映射内存的最大长度 */
	
	//#define DBG_INFO
	
	static int mem_mmap_open1(struct inode *inode, struct file *file)  
	{
	#ifdef INFO  
	    pr_info("mem_mmap_open1: memory mmap module open ok\n");
	#endif
	    return 0;  
	}  
	
	static int mem_mmap1(struct file *filp, struct vm_area_struct *vma)  
	{      
	    unsigned long page;  
	    // 偏移地址必须按PAGE_SIZE对齐,vm_pgoff为偏移的PAGE数量
	    // 右移PAGE_SHIFT,表示偏移的字节数
	    unsigned long offset = (unsigned long)vma->vm_pgoff << PAGE_SHIFT;   
	    unsigned long start = (unsigned long)vma->vm_start;   
	    unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);  
	
	#ifdef INFO    
	    pr_info("mem_mmap1: vm_pgoff %lx, offset %lx, start %lx, "
	            "end is %lx, size is %lx\n",
	            (unsigned long)vma->vm_pgoff, offset, start, vma->vm_end ,size);
	#endif
	
	    if(size > MEM_MMAP_LEN) {
	#ifdef INFO   
	        pr_info("mem_mmap1: mmap size is too big, size %lx, max size %lx\n", 
	                size, (unsigned long)MEM_MMAP_LEN);
	#endif
	        return -ENXIO;
	    }
	    if(offset + size > MEM_MMAP_LEN) {
	#ifdef INFO   
	        pr_info("mem_mmap1: offset is too big, size %lx, offset %lx, max size %lx\n", 
	                size, offset, (unsigned long)MEM_MMAP_LEN);
	#endif
	        return -ENXIO;
	    }
	    /* 映射的起始物理地址 */
	    page = MEM_MMAP_ADDR1 + offset;   
	    vma->vm_flags |= VM_IO | VM_SHARED;
	    // 关闭cache
	    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
	    /* mmap the request addr */  
	    if(remap_pfn_range(vma, start, page >> PAGE_SHIFT, size, vma->vm_page_prot)) {
	        pr_err("mem_mmap1: mem_mmap:mmap faild\n");
	        return -EAGAIN; 
	    } 
	#ifdef INFO   
	    pr_info("mem_mmap1: mmap ok\n");
	#endif
	    return 0;  
	}  
	
	static struct file_operations dev_fops_mem_mmap1 = {
	    .owner    = THIS_MODULE,
	    .open    = mem_mmap_open1,  
	    .mmap   = mem_mmap1,  
	};
	
	static struct miscdevice misc_mem_mmap1 = {
	    .minor = MISC_DYNAMIC_MINOR,
	    .name = DEVICE_NAME1,
	    .fops = &dev_fops_mem_mmap1,
	};
	
	
	static int mem_mmap_open2(struct inode *inode, struct file *file)  
	{  
	#ifdef INFO   
	    pr_info("mem_mmap_open2: memory mmap module open ok\n");
	#endif
	    return 0;  
	}  
	
	static int mem_mmap2(struct file *filp, struct vm_area_struct *vma)  
	{      
	    unsigned long page;  
	    // 偏移地址必须按PAGE_SIZE对齐,vm_pgoff为偏移的PAGE数量
	    // 右移PAGE_SHIFT,表示偏移的字节数
	    unsigned long offset = (unsigned long)vma->vm_pgoff << PAGE_SHIFT;   
	    unsigned long start = (unsigned long)vma->vm_start;   
	    unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);  
	#ifdef INFO   
	    pr_info("mem_mmap2: vm_pgoff %lx, offset %lx, start %lx, "
	            "end is %lx, size is %lx\n",
	            (unsigned long)vma->vm_pgoff, offset, start, vma->vm_end ,size);
	#endif
	
	    if(size > MEM_MMAP_LEN) {
	#ifdef INFO   
	        pr_info("mem_mmap2: mmap size is too big, size %lx, max size %lx\n", 
	                size, (unsigned long)MEM_MMAP_LEN);
	#endif
	        return -ENXIO;
	    }
	    if(offset + size > MEM_MMAP_LEN) {
	#ifdef INFO   
	        pr_info("mem_mmap2: offset is too big, size %lx, offset %lx, max size %lx\n", 
	                size, offset, (unsigned long)MEM_MMAP_LEN);
	#endif
	        return -ENXIO;
	    }
	    /* 映射的起始物理地址 */
	    page = MEM_MMAP_ADDR2 + offset;   
	    vma->vm_flags |= VM_IO | VM_SHARED;
	    // 关闭cache
	    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
	    /* mmap the request addr */  
	    if(remap_pfn_range(vma, start, page >> PAGE_SHIFT, size, vma->vm_page_prot)) {
	        pr_err("mem_mmap2: mem_mmap:mmap faild\n");
	        return -EAGAIN; 
	    } 
	#ifdef INFO   
	    pr_info("mem_mmap2: mmap ok\n");
	#endif
	    return 0;  
	} 
	
	static struct file_operations dev_fops_mem_mmap2 = {
	    .owner    = THIS_MODULE,
	    .open    = mem_mmap_open2,  
	    .mmap   = mem_mmap2,  
	};
	
	static struct miscdevice misc_mem_mmap2 = {
	    .minor = MISC_DYNAMIC_MINOR,
	    .name = DEVICE_NAME2,
	    .fops = &dev_fops_mem_mmap2,
	};
	
	
	static int __init mem_mmap_init(void)
	{
	    int ret = 0;
	    /*register misc device for sharemem*/
	    ret = misc_register(&misc_mem_mmap1);
	    if (ret != 0) {
	        pr_err("mem_mmap_init: misc_mem_mmap1 register error...\n");
	        ret = -ENOENT;
	
	        return ret;
	    }
	
	   ret = misc_register(&misc_mem_mmap2);
	    if (ret != 0) {
	        pr_err("mem_mmap_init: misc_mem_mmap2 register error...\n");
	        ret = -ENOENT;
	        misc_deregister(&misc_mem_mmap1);
	        return ret;
	    }
	#ifdef INFO   
	    pr_info("mem_mmap_init: mem_mmap_init ok\n" );
	#endif
	    return ret;
	    
	}
	
	static void __exit mem_mmap_exit(void)
	{
	    misc_deregister(&misc_mem_mmap1);
	    misc_deregister(&misc_mem_mmap2);
	}
	
	module_init(mem_mmap_init);
	module_exit(mem_mmap_exit);
	MODULE_DESCRIPTION("memory mmap module");
	MODULE_AUTHOR("[email protected]");
	MODULE_LICENSE("GPL");

你可能感兴趣的:(Linux驱动,Linux,内存映射,mmap,/dev/mem)