zoukankan      html  css  js  c++  java
  • 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的源代码

    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 ");  
    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 ");  
    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 ");  
    66.     printk("dev=%d", dev);  
    67.     dev = MKDEV(dev_major, dev_minor);  
    68.     cdev = cdev_alloc();//为字符设备cdev分配空间  
    69.     printk(KERN_ALERT"module init ");  
    70.     if(dev_major)  
    71.     {  
    72.         result = register_chrdev_region(dev, 1, driver_name);//静态分配设备号  
    73.         printk("result = %d ", 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 ", 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 ", result);  
    94.     return 0;  
    95. }  
    96.   
    97. static void __exit char_device_exit( void )  
    98. {  
    99.     printk(KERN_ALERT"module exit ");  
    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

    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 ",fd);  
    16.         return -1;  
    17.     }  
    18.     /*测试一:查看内存映射段*/  
    19.     printf("before mmap ");  
    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 ");  
    25.     sleep(15);//睡眠15秒,在命令行查看映射后的内存图,如果多出了映射段,说明映射成功  
    26.       
    27.     /*测试二:往映射段读写数据,看是否成功*/  
    28.     strcpy(mapBuf, "Driver Test");//向映射段写数据  
    29.     memset(buffer, 0, 1024);  
    30.     strcpy(buffer, mapBuf);//从映射段读取数据  
    31.     printf("buf = %s ", buffer);//如果读取出来的数据和写入的数据一致,说明映射段的确成功了  
    32.       
    33.       
    34.     munmap(mapBuf, 1024);//去除映射  
    35.     free(buffer);  
    36.     close(fd);//关闭文件,最终调用驱动的close  
    37.     return 0;  
    38. }  

    下面是makefile文件

    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流程

    ①应用程序调用open("/dev/mmap_driver", O_RDWR);

    ②Open就会通过VFS找到该设备的索引节点(inode),mknod的时候会根据设备号把驱动程序的file_operations结构填充到索引节点中(关于mknod /dev/mmap_driver c 999 0,这条指令创建了设备文件,在安装驱动(insmod)的时候,会运行驱动程序的初始化程序(module_init),在初始化程序中,会注册它的主设备号到系统中(cdev_add),如果mknod时的主设备号999在系统中不存在,即和注册的主设备号不同,则上面的指令会执行失败,就创建不了设备文件)

    ③然后根据设备文件的索引节点中的file_operations中的open指针,就调用驱动的open方法了。

    ④生成一个文件对象files_struct结构,系统维护一个files_struct的链表,表示系统中所有打开的文件

    ⑤返回文件描述符fd,把fd加入到进程的文件描述符表中


    (2)设备驱动close流程

    应用程序调用close(fd),最终可调用驱动的close,为什么根据一个简单的int型fd就可以找到驱动的close函数?这就和上面说的三个结构(struct file_operations、struct file、struct inode)息息相关了,假如fd = 3


    (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(硬件)映射到设备地址上了

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


    总结

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

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

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

  • 相关阅读:
    Django系列6:Model简介,ORM,字段类型,约束
    Django系列4:数据常规操作级联数据
    Django系列5:脑图总结
    Sorted Adjacent Differences
    C
    E. Sleeping Schedule
    D. Ehab the Xorcist
    C. Game with Chips
    D. Walk on Matrixv
    Circle of Monsters
  • 原文地址:https://www.cnblogs.com/wanghuaijun/p/7624564.html
Copyright © 2011-2022 走看看