Linux驱动mmap内存映射 | 您所在的位置:网站首页 › mmap内存映射 › Linux驱动mmap内存映射 |
mmap在linux哪里?
什么是mmap?
上图说了,mmap是操作这些设备的一种方法,所谓操作设备,比如IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。 但是,由于应用程序不能直接操作设备硬件地址,所以操作系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。 操作设备还有很多方法,如ioctl、ioremap mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操作虚拟内存相当于直接操作设备了,省去了用户空间到内核空间的复制过程,相对IO操作来说,增加了数据的吞吐量。 什么是内存映射? 既然mmap是实现内存映射的接口,那么内存映射是什么呢?看下图 每个进程都有独立的进程地址空间,通过页表和MMU,可将虚拟地址转换为物理地址,每个进程都有独立的页表数据,这可解释为什么两个不同进程相同的虚拟地址,却对应不同的物理地址。 什么是虚拟地址空间? 每个进程都有4G的虚拟地址空间,其中3G用户空间,1G内核空间(linux),每个进程共享内核空间,独立的用户空间,下图形象地表达了这点 驱动程序运行在内核空间,所以驱动程序是面向所有进程的。 用户空间切换到内核空间有两种方法: (1)系统调用,即软中断 (2)硬件中断 虚拟地址空间里面是什么? 了解了什么是虚拟地址空间,那么虚拟地址空间里面装的是什么?看下图 虚拟空间装的大概是上面那些数据了,内存映射大概就是把设备地址映射到上图的红色段了,暂且称其为“内存映射段”,至于映射到哪个地址,是由操作系统分配的,操作系统会把进程空间划分为三个部分: (1)未分配的,即进程还未使用的地址 (2)缓存的,缓存在ram中的页 (3)未缓存的,没有缓存在ram中 操作系统会在未分配的地址空间分配一段虚拟地址,用来和设备地址建立映射,至于怎么建立映射,后面再揭晓。 现在大概明白了“内存映射”是什么了,那么内核是怎么管理这些地址空间的呢?任何复杂的理论最终也是通过各种数据结构体现出来的,而这里这个数据结构就是进程描述符。从内核看,进程是分配系统资源(CPU、内存)的载体,为了管理进程,内核必须对每个进程所做的事情进行清楚的描述,这就是进程描述符,内核用task_struct结构体来表示进程,并且维护一个该结构体链表来管理所有进程。该结构体包含一些进程状态、调度信息等上千个成员,我们这里主要关注进程描述符里面的内存描述符(struct mm_struct mm) 内存描述符 具体的结构,请参考下图 现在已经知道了内存映射是把设备地址映射到进程空间地址(注意:并不是所有内存映射都是映射到进程地址空间的,ioremap是映射到内核虚拟空间的,mmap是映射到进程虚拟地址的),实质上是分配了一个vm_area_struct结构体加入到进程的地址空间,也就是说,把设备地址映射到这个结构体,映射过程就是驱动程序要做的事了。 内存映射的实现 以字符设备驱动为例,一般对字符设备的操作都如下框图 而内存映射的主要任务就是实现内核空间中的mmap()函数,先来了解一下字符设备驱动程序的框架 以下是mmap_driver.c的源代码 [cpp] view plain copy //所有的模块代码都包含下面两个头文件 #include #include #include //定义dev_t类型 #include //定义struct cdev结构体及相关操作 #include //定义kmalloc接口 #include //定义virt_to_phys接口 #include //remap_pfn_range #include #define MAJOR_NUM 990 #define MM_SIZE 4096 static char driver_name[] = "mmap_driver1";//驱动模块名字 static int dev_major = MAJOR_NUM; static int dev_minor = 0; char *buf = NULL; struct cdev *cdev = NULL; static int device_open(struct inode *inode, struct file *file) { printk(KERN_ALERT"device open\n"); buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//内核申请内存只能按页申请,申请该内存以便后面把它当作虚拟设备 return 0; } static int device_close(struct inode *indoe, struct file *file) { printk("device close\n"); if(buf) { kfree(buf); } return 0; } static int device_mmap(struct file *file, struct vm_area_struct *vma) { vma->vm_flags |= VM_IO;//表示对设备IO空间的映射 vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出 if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里 vma->vm_start,//虚拟空间的起始地址 virt_to_phys(buf)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移12位 vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍 vma->vm_page_prot))//保护属性, { return -EAGAIN; } return 0; } static struct file_operations device_fops = { .owner = THIS_MODULE, .open = device_open, .release = device_close, .mmap = device_mmap, }; static int __init char_device_init( void ) { int result; dev_t dev;//高12位表示主设备号,低20位表示次设备号 printk(KERN_ALERT"module init2323\n"); printk("dev=%d", dev); dev = MKDEV(dev_major, dev_minor); cdev = cdev_alloc();//为字符设备cdev分配空间 printk(KERN_ALERT"module init\n"); if(dev_major) { result = register_chrdev_region(dev, 1, driver_name);//静态分配设备号 printk("result = %d\n", result); } else { result = alloc_chrdev_region(&dev, 0, 1, driver_name);//动态分配设备号 dev_major = MAJOR(dev); } if(result ops = &device_fops; cdev->owner = THIS_MODULE; result = cdev_add(cdev, dev, 1);//向内核注册字符设备 printk("dffd = %d\n", result); return 0; } static void __exit char_device_exit( void ) { printk(KERN_ALERT"module exit\n"); cdev_del(cdev); unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1); } module_init(char_device_init);//模块加载 module_exit(char_device_exit);//模块退出 MODULE_LICENSE("GPL"); MODULE_AUTHOR("ChenShengfa");
下面是测试代码test_mmap.c [cpp] view plain copy #include #include #include #include #include int main( void ) { int fd; char *buffer; char *mapBuf; fd = open("/dev/mmap_driver", O_RDWR);//打开设备文件,内核就能获取设备文件的索引节点,填充inode结构 if(fd |
CopyRight 2018-2019 实验室设备网 版权所有 |