文章目录
用户空间下的物理地址映射
mmap系统调用函数
mmap系统调用的实现过程
对应的底层驱动的mmap接口
代码示例(使用mmap修改LED示例):
用户空间下的物理地址映射
前一篇讲述了利用imremap函数完成Linux内核空间下的物理地址映射到内核虚拟地址空间上。那么如何能够将外设的物理地址映射到用户空间下的虚拟地址呢,如果一旦完成将外设的物理地址映射到用户空间下的虚拟地址,那么用户就可以直接通过这种映射访问外设的物理地址。
- 利用mmap函数完成用户空间到物理地址的映射。
mmap系统调用函数
系统中的“文件”其实就是软件上抽象出来的东西,研究文件就是在研究其中额数据。文件最终保存在EMMC(磁盘),也就是文件中的数据保存在EMMC(磁盘),与其说是研究操作文件,不如说是操作文件中数据对应的磁盘存储位置,磁盘的存储位置同样有对应的物理地址(例如EMMC的512字节开始存储uboot数据,那么512就是EMMC磁盘的一个起始物理地址)
例如:
void *addr; int fd = open("a.txt", O_RDWR); addr = mmap(0, 0x100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); memcpy(addr, "hello", 5);//向映射的用户虚拟内存写入数据 //本质就是向文件a.txt写入数据
- 表面上看是将文件a.txt和当前进程3G的MMAP虚拟内存映射区中的某块空闲的虚拟内存区域做映射,一旦完成这种映射,将来访问映射的用户虚拟内存就是在访问文件。
- 本质就是将文件对应的磁盘的物理地址和当前进程的虚拟内存映射区中的某块空闲的虚拟内存区域做映射,一旦完成这种映射,应用程序访问映射的虚拟地址就是在访问对应的物理地址。mmap就是将虚拟地址和物理地址完成映射的函数。
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
参数:
- start:为0时是告诉内核来帮你在MMAP虚拟内存映射区找一块空闲的用户虚拟内存做映射。
- length:映射的用户虚拟内存的大小。
- prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起。
- PROT_EXEC //页内容可以被执行
- PROT_READ //页内容可以被读取
- PROT_WRITE //页可以被写入
- PROT_NONE //页不可访问
- flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
- MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
- MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
- MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
- MAP_DENYWRITE //这个标志被忽略。
- MAP_EXECUTABLE //同上
- MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
- MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
- MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
- MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
- MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
- MAP_FILE //兼容标志,被忽略。
- MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
- MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
- MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
- fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
- off_toffset:被映射对象内容的起点。
返回值:
- addr:就是映射的用户虚拟内存的起始地址。
mmap系统调用的实现过程
- 应用程序调用mmap,首先是调用C库的mmap函数,C库的mmap函数中做两件事:
- 保存mmap系统调用号到r7寄存器。
- 调用svc指定触发软中断异常。
- 至此,当前进程由用户空间“陷入”内核空间继续执行,首先跑到内核的异常向量表软中断的处理入口地址0x08。接着做以下三件事:
- 从r7寄存器中取出之前保存的mmap系统调用号。
- 以系统调用号为下标在系统调用表中找到对应的内核实现函数sys_mmap,然后调用sys_mmap。
- 内核的sys_mmap将会做如下:
- 首先在MMAP虚拟内存映射区中帮用户找到一块空闲的用户虚拟内存区域用来映射外设的物理地址。
- 内核一旦找到空闲的用户虚拟内存区域后,内核会用struct vm_area_struct数据结构定义初始化一个对象来描述这块空闲用户虚拟内存的属性:
- 内核的sys_mmap最后调用底层驱动的mmap接口并且将第二步创建的struct vm_area_struct对象的首地址传递给底层驱动的mmap接口,这样底层驱动的mmap接口可以通过首地址来访问空闲的用户虚拟内存的相关属性。
- 底层驱动的mmap接口调用完毕,最终返回到用户空间,
- 切记:底层驱动的mmap接口永远仅做一件事:就是将已知的物理地址和已知的空闲的用户虚拟地址做映射,一旦完成这种映射,将来应用程序在用户空间访问映射的用户虚拟地址就是在访问对应的物理地址。
struct vm_area_struct { unsigned long vm_start; //空闲的用户虚拟内存的首地址,就是mmap函数的返回值addr unsigned long vm_end; //空闲的用户虚拟内存的结束地址 pgprot_t vm_page_prot;//空闲的用户虚拟内存的访问权限,就是PROT_READ|PROT_WRITE ... };
对应的底层驱动的mmap接口
struct file_operations { int (*mmap) (struct file *file, struct vm_area_struct *vma); };
接口功能:永远仅做只做一件事将已知的用户虚拟地址(内核已经帮你找好了嘛),和已知的物理地址(看芯片手册吗)做映射,一旦完成映射,将来硬件的操作都是在用户空间完成。类似:红娘
参数:
- file:文件指针,指向内核创建的描述文件被打开以后的状态属性的一个对象。
- vma:指向内核创建的用于描述空闲的用户虚拟内存的属性的一个对象,将来驱动的mmap接口可以通过此指针来获取空闲的用户
- 虚拟内存的属性:
- vma->vm_start:获取用户虚拟内存的起始地址
- vma->vm_end:获取用户虚拟内存的结束地址
- vma->vm_page_prot:获取用户虚拟内存的访问权限
int remap_pfn_range( struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot )
函数功能:最终完成地址映射
参数:
- vma:指向内核创建的用于描述空闲的用户虚拟内存的属性的一个对象,将来驱动的mmap接口可以通过此指针来获取空闲的用户,虚拟内存的属性.
- addr:已知的空闲的用户虚拟内存的起始地址, 就是vma->vm_start。
- pfn:已知的物理地址>>12,例如:0xC001C000>>12,
- size:空闲用户虚拟内存的大小, 就是:vma->vm_end-vma->vm_start。
- prot:空闲用户虚拟内存的访问权限, 就是:vma->vm_page_prot。
代码示例(使用mmap修改LED示例):
使用mmap在用户空间下调用gpio管脚操作led灯的亮灭,修改后的具体实现在led_test.c中,而驱动成了一个桥梁作用。
- led_drv.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <linux/mm.h> //mmap //vma:指向内核创建的一个vm_area_struct对象 //此对象来描述内核帮你找的空闲的用户虚拟内存的属性 static int led_mmap(struct file *file, struct vm_area_struct *vma) { //1.切记:只做一件事:将物理地址映射到用户虚拟地址上 //映射关系:物理地址0xC001C000和vm_start做了映射 //将来硬件的操作都是在用户空间完成 remap_pfn_range(vma, vma->vm_start,//起始用户虚拟地址 0xC001C000>>12,//已知起始物理地址>>12 vma->vm_end-vma->vm_start,//大小 vma->vm_page_prot//权限 ); return 0; //执行成功返回0,执行失败返回负值 } //定义初始化硬件操作接口对象 static struct file_operations led_fops = { .owner = THIS_MODULE, .mmap = led_mmap //地址映射接口 }; //定义初始化混杂设备对象 static struct miscdevice led_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "myled", .fops = &led_fops }; static int led_init(void) { //1.注册混杂设备到内核 misc_register(&led_misc); return 0; } static void led_exit(void) { //1.卸载混杂设备 misc_deregister(&led_misc); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
- led_test.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <sys/mman.h> int main(int argc, char *argv[]) { int fd; void *gpiobase; //寄存器的用户虚拟地址 unsigned long *gpiocout; unsigned long *gpiocoutenb; unsigned long *gpiocaltfn0; if(argc != 2) { printf("用法:%s <on|off>\n", argv[0]); return -1; } //打开设备 fd = open("/dev/myled", O_RDWR); if (fd < 0) { printf("打开设备失败!\n"); return -1; } //利用mmap进行地址映射 //结果:gpiobase=vm_start对应的物理地址就是0xC001C000 gpiobase = mmap(0, 0x100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); gpiocout = (unsigned long *)(gpiobase + 0x00); gpiocoutenb = (unsigned long *)(gpiobase + 0x04); gpiocaltfn0 = (unsigned long *)(gpiobase + 0x20); //3.配置引脚为GPIO功能,配置为输出,输出1(省电) *gpiocaltfn0 &= ~(0x3 << 24); *gpiocaltfn0 |= (1 << 24); *gpiocoutenb |= (1 << 12); *gpiocout |= (1 << 12); //4.根据用户命令开关灯 if(!strcmp(argv[1], "on")) *gpiocout &= ~(1 << 12); else *gpiocout |= (1 << 12); //解除地址映射 munmap(gpiobase, 0x100); //关闭设备 close(fd); return 0; }
- Makefile
obj-m += led_drv.o all: make -C /opt/kernel SUBDIRS=$(PWD) modules clean: make -C /opt/kernel SUBDIRS=$(PWD) clean
- 执行结果: