上图说了,mmap是操做这些设备的一种方法,所谓操做设备,好比IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。node
可是,因为应用程序不能直接操做设备硬件地址,因此操做系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。linux
操做设备还有不少方法,如ioctl、ioremap数组
mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操做虚拟内存至关于直接操做设备了,省去了用户空间到内核空间的复制过程,相对IO操做来讲,增长了数据的吞吐量。缓存
既然mmap是实现内存映射的接口,那么内存映射是什么呢?看下图网络
每一个进程都有独立的进程地址空间,经过页表和MMU,可将虚拟地址转换为物理地址,每一个进程都有独立的页表数据,这可解释为何两个不一样进程相同的虚拟地址,却对应不一样的物理地址。数据结构
每一个进程都有4G的虚拟地址空间,其中3G用户空间,1G内核空间(linux),每一个进程共享内核空间,独立的用户空间,下图形象地表达了这点框架
驱动程序运行在内核空间,因此驱动程序是面向全部进程的。函数
用户空间切换到内核空间有两种方法:测试
(1)系统调用,即软中断ui
(2)硬件中断
了解了什么是虚拟地址空间,那么虚拟地址空间里面装的是什么?看下图
虚拟空间装的大概是上面那些数据了,内存映射大概就是把设备地址映射到上图的红色段了,暂且称其为“内存映射段”,至于映射到哪一个地址,是由操做系统分配的,操做系统会把进程空间划分为三个部分:
(1)未分配的,即进程还未使用的地址
(2)缓存的,缓存在ram中的页
(3)未缓存的,没有缓存在ram中
操做系统会在未分配的地址空间分配一段虚拟地址,用来和设备地址创建映射,至于怎么创建映射,后面再揭晓。
如今大概明白了“内存映射”是什么了,那么内核是怎么管理这些地址空间的呢?任何复杂的理论最终也是经过各类数据结构体现出来的,而这里这个数据结构就是进程描述符。从内核看,进程是分配系统资源(CPU、内存)的载体,为了管理进程,内核必须对每一个进程所作的事情进行清楚的描述,这就是进程描述符,内核用task_struct结构体来表示进程,而且维护一个该结构体链表来管理全部进程。该结构体包含一些进程状态、调度信息等上千个成员,咱们这里主要关注进程描述符里面的内存描述符(struct mm_struct mm)
具体的结构,请参考下图
如今已经知道了内存映射是把设备地址映射到进程空间地址(注意:并非全部内存映射都是映射到进程地址空间的,ioremap是映射到内核虚拟空间的,mmap是映射到进程虚拟地址的),实质上是分配了一个vm_area_struct结构体加入到进程的地址空间,也就是说,把设备地址映射到这个结构体,映射过程就是驱动程序要作的事了。
以字符设备驱动为例,通常对字符设备的操做都以下框图
而内存映射的主要任务就是实现内核空间中的mmap()函数,先来了解一下字符设备驱动程序的框架
如下是mmap_driver.c的源代码
下面是测试代码test_mmap.c
下面是makefile文件
下面命令演示一下驱动程序的编译、安装、测试过程(注:其余用户在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转换成物理地址,是模拟了一个硬件设备,即把虚拟设备映射到虚拟地址,在实际中能够直接使用物理地址。
总结
①从以上看到,内核各个模块错综复杂、相互交叉
②单纯一个小小驱动模块,就涉及了进程管理(进程地址空间)、内存管理(页表与页帧映射)、虚拟文件系统(structfile、structinode)
③并非全部设备驱动均可以使用mmap来映射,好比像串口和其余面向流的设备,而且必须按照页大小进行映射。