温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何解析Linux驱动中mmap内存映射

发布时间:2021-10-22 11:45:57 来源:亿速云 阅读:127 作者:柒染 栏目:互联网科技

今天就跟大家聊聊有关如何解析Linux驱动中mmap内存映射,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

mmap在linux哪里?

如何解析Linux驱动中mmap内存映射

什么是mmap?

上图说了,mmap是操作这些设备的一种方法,所谓操作设备,比如IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。

但是,由于应用程序不能直接操作设备硬件地址,所以操作系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。

操作设备还有很多方法,如ioctl、ioremap

mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操作虚拟内存相当于直接操作设备了,省去了用户空间到内核空间的复制过程,相对IO操作来说,增加了数据的吞吐量。

什么是内存映射?

既然mmap是实现内存映射的接口,那么内存映射是什么呢?看下图

如何解析Linux驱动中mmap内存映射

驱动程序运行在内核空间,所以驱动程序是面向所有进程的。

用户空间切换到内核空间有两种方法:

(1)系统调用,即软中断

(2)硬件中断

虚拟地址空间里面是什么?

了解了什么是虚拟地址空间,那么虚拟地址空间里面装的是什么?看下图

如何解析Linux驱动中mmap内存映射

现在已经知道了内存映射是把设备地址映射到进程空间地址(注意:并不是所有内存映射都是映射到进程地址空间的,ioremap是映射到内核虚拟空间的,mmap是映射到进程虚拟地址的),实质上是分配了一个vm_area_struct结构体加入到进程的地址空间,也就是说,把设备地址映射到这个结构体,映射过程就是驱动程序要做的事了。

内存映射的实现

以字符设备驱动为例,一般对字符设备的操作都如下框图

如何解析Linux驱动中mmap内存映射

以下是mmap_driver.c的源代码

[cpp] view plain copy

  1. //所有的模块代码都包含下面两个头文件  

  2. #include <linux/module.h>  

  3. #include <linux/init.h>  

  4.   

  5. #include <linux/types.h> //定义dev_t类型  

  6. #include <linux/cdev.h> //定义struct cdev结构体及相关操作  

  7. #include <linux/slab.h> //定义kmalloc接口  

  8. #include <asm/io.h>//定义virt_to_phys接口  

  9. #include <linux/mm.h>//remap_pfn_range  

  10. #include <linux/fs.h>  

  11.   

  12. #define MAJOR_NUM 990  

  13. #define MM_SIZE 4096  

  14.   

  15. static char driver_name[] = "mmap_driver1";//驱动模块名字  

  16. static int dev_major = MAJOR_NUM;  

  17. static int dev_minor = 0;  

  18. char *buf = NULL;  

  19. struct cdev *cdev = NULL;  

  20.   

  21. static int device_open(struct inode *inode, struct file *file)  

  22. {  

  23.     printk(KERN_ALERT"device open\n");  

  24.     buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//内核申请内存只能按页申请,申请该内存以便后面把它当作虚拟设备  

  25.     return 0;  

  26. }  

  27.   

  28. static int device_close(struct inode *indoe, struct file *file)  

  29. {  

  30.     printk("device close\n");  

  31.     if(buf)  

  32.     {  

  33.         kfree(buf);  

  34.     }  

  35.     return 0;  

  36. }  

  37.   

  38. static int device_mmap(struct file *file, struct vm_area_struct *vma)  

  39. {  

  40.     vma->vm_flags |= VM_IO;//表示对设备IO空间的映射  

  41.     vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出  

  42.     if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里  

  43.                        vma->vm_start,//虚拟空间的起始地址  

  44.                        virt_to_phys(buf)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移12位  

  45.                        vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍  

  46.                        vma->vm_page_prot))//保护属性,  

  47.     {  

  48.         return -EAGAIN;  

  49.     }  

  50.     return 0;  

  51. }  

  52.   

  53. static struct file_operations device_fops =  

  54. {  

  55.     .owner = THIS_MODULE,  

  56.     .open  = device_open,  

  57.     .release = device_close,  

  58.     .mmap = device_mmap,  

  59. };  

  60.   

  61. static int __init char_device_init( void )  

  62. {  

  63.     int result;  

  64.     dev_t dev;//高12位表示主设备号,低20位表示次设备号  

  65.     printk(KERN_ALERT"module init2323\n");  

  66.     printk("dev=%d", dev);  

  67.     dev = MKDEV(dev_major, dev_minor);  

  68.     cdev = cdev_alloc();//为字符设备cdev分配空间  

  69.     printk(KERN_ALERT"module init\n");  

  70.     if(dev_major)  

  71.     {  

  72.         result = register_chrdev_region(dev, 1, driver_name);//静态分配设备号  

  73.         printk("result = %d\n", result);  

  74.     }  

  75.     else  

  76.     {  

  77.         result = alloc_chrdev_region(&dev, 0, 1, driver_name);//动态分配设备号  

  78.         dev_major = MAJOR(dev);  

  79.     }  

  80.       

  81.     if(result < 0)  

  82.     {  

  83.         printk(KERN_WARNING"Cant't get major %d\n", dev_major);  

  84.         return result;  

  85.     }  

  86.       

  87.       

  88.     cdev_init(cdev, &device_fops);//初始化字符设备cdev  

  89.     cdev->ops = &device_fops;  

  90.     cdev->owner = THIS_MODULE;  

  91.       

  92.     result = cdev_add(cdev, dev, 1);//向内核注册字符设备  

  93.     printk("dffd = %d\n", result);  

  94.     return 0;  

  95. }  

  96.   

  97. static void __exit char_device_exit( void )  

  98. {  

  99.     printk(KERN_ALERT"module exit\n");  

  100.     cdev_del(cdev);  

  101.     unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);  

  102. }  

  103.   

  104. module_init(char_device_init);//模块加载  

  105. module_exit(char_device_exit);//模块退出  

  106.   

  107. MODULE_LICENSE("GPL");  

  108. MODULE_AUTHOR("ChenShengfa");  

下面是测试代码test_mmap.c

[cpp] view plain copy

  1. #include <stdio.h>  

  2. #include <fcntl.h>  

  3. #include <sys/mman.h>  

  4. #include <stdlib.h>  

  5. #include <string.h>  

  6.   

  7. int main( void )  

  8. {  

  9.     int fd;  

  10.     char *buffer;  

  11.     char *mapBuf;  

  12.     fd = open("/dev/mmap_driver", O_RDWR);//打开设备文件,内核就能获取设备文件的索引节点,填充inode结构  

  13.     if(fd<0)  

  14.     {  

  15.         printf("open device is error,fd = %d\n",fd);  

  16.         return -1;  

  17.     }  

  18.     /*测试一:查看内存映射段*/  

  19.     printf("before mmap\n");  

  20.     sleep(15);//睡眠15秒,查看映射前的内存图cat /proc/pid/maps  

  21.     buffer = (char *)malloc(1024);  

  22.     memset(buffer, 0, 1024);  

  23.     mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//内存映射,会调用驱动的mmap函数  

  24.     printf("after mmap\n");  

  25.     sleep(15);//睡眠15秒,在命令行查看映射后的内存图,如果多出了映射段,说明映射成功  

  26.       

  27.     /*测试二:往映射段读写数据,看是否成功*/  

  28.     strcpy(mapBuf, "Driver Test");//向映射段写数据  

  29.     memset(buffer, 0, 1024);  

  30.     strcpy(buffer, mapBuf);//从映射段读取数据  

  31.     printf("buf = %s\n", buffer);//如果读取出来的数据和写入的数据一致,说明映射段的确成功了  

  32.       

  33.       

  34.     munmap(mapBuf, 1024);//去除映射  

  35.     free(buffer);  

  36.     close(fd);//关闭文件,最终调用驱动的close  

  37.     return 0;  

  38. }  

下面是makefile文件

[plain] view plain copy

  1. ifneq ($(KERNELRELEASE),)  

  2.   

  3. obj-m := mmap_driver.o  

  4.   

  5. else  

  6. KDIR := /lib/modules/3.2.0-52-generic/build  

  7.   

  8. all:  

  9.     make -C $(KDIR) M=$(PWD) modules  

  10. clean:  

  11.     rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.order  

  12.   

  13. endif  

下面命令演示一下驱动程序的编译、安装、测试过程(注:其他用户在mknod之后还需要chmod改变权限)

# make    //编译驱动

# insmod mmap_driver.ko    //安装驱动

# mknod /dev/mmap_driver c 999 0    //创建设备文件

# gcc test_mmap.c -o test.o    //编译应用程序

# ./test.o    //运行应用程序来测试驱动程序


拓展:

关于这个过程,涉及一些术语

(1)设备文件:linux中对硬件虚拟成设备文件,对普通文件的各种操作均适用于设备文件

(2)索引节点:linux使用索引节点来记录文件信息(如文件长度、创建修改时间),它存储在磁盘中,读入内存后就是一个inode结构体,文件系统维护了一个索引节点的数组,每个元素都和文件或者目录一一对应。

(3)主设备号:如上面的999,表示设备的类型,比如该设备是lcd还是usb等

(4)次设备号:如上面的0,表示该类设备上的不同设备

(5)文件(普通文件或设备文件)的三个结构

        ①文件操作:struct file_operations

        ②文件对象:struct file

        ③文件索引节点:struct inode

关于驱动程序中内存映射的实现,先了解一下open和close的流程

(1)设备驱动open流程

如何解析Linux驱动中mmap内存映射

(3)设备驱动mmap流程

由open和close得知,同理,应用程序调用mmap最终也会调用到驱动程序中mmap方法

①应用程序test.mmap.c中mmap函数

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:映射后虚拟地址的起始地址,通常为NULL,内核自动分配

length:映射区的大小

prot:页面访问权限(PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE)

flags:参考网络资料

fd:文件描述符

offset:文件映射开始偏移量

②驱动程序的mmap_driver.c中mmap函数

上面说了,mmap的主要工作是把设备地址映射到进程虚拟地址,也即是一个vm_area_struct的结构体,这里说的映射,是一个很悬的东西,那它在程序中的表现是什么呢?——页表,没错,就是页表,映射就是要建立页表。进程地址空间就可以通过页表(软件)和MMU(硬件)映射到设备地址上了

如何解析Linux驱动中mmap内存映射

virt_to_phys(buf),buf是在open时申请的地址,这里使用virt_to_phys把buf转换成物理地址,是模拟了一个硬件设备,即把虚拟设备映射到虚拟地址,在实际中可以直接使用物理地址。

总结

①从以上看到,内核各个模块错综复杂、相互交叉

②单纯一个小小驱动模块,就涉及了进程管理(进程地址空间)、内存管理(页表与页帧映射)、虚拟文件系统(structfile、structinode)

③并不是所有设备驱动都可以使用mmap来映射,比如像串口和其他面向流的设备,并且必须按照页大小进行映射。

看完上述内容,你们对如何解析Linux驱动中mmap内存映射有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI