我们在驱动的代码当中,经常需要把硬件地址映射到虚拟内存上面去,这就是我们需要学习mmap的原因所在了!正所谓磨刀不误砍柴工,让我们静下心来学习一下这个十分有用的工具吧。
prototype : void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
parameter :
start : 映射区的开始地址。(一般建议为null,让内核帮我们自动寻找一个合适的地址)
length : 映射区的长度。
prot :期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起。
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags :指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。
//直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,
//只能使用其中一个。(还有更多可选参数,具体网上易得)
fd :有效的文件描述词。
offset :被映射对象内容的起点。
return :
返回所映射的虚拟内存首地址。
例程如下:
#include
#include
#include
#include
#include
#include
int main() { int fd; char *start; char buf[100]; /*打开文件*/ fd = open("testfile",O_RDWR); start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); /* 读出数据 */ strcpy(buf,start); printf("buf = %s/n",buf); /* 写入数据 */ strcpy(start,"Buf Is Not Null!"); munmap(start,100); /*解除映射*/ close(fd); return 0; }
1、 测试例程先打开测试文件testfile,取得文件描述符fd。并通过调用mmap函数把测试文件映射到虚拟内存当中去,
其首地址返回到指针start。
2、 读出数据时,直接strcpy,从虚拟内存的首地址开始复制字符串到buf,其实质就是把测试文件testfile的内容复制到buf
(事先已经做了映射)。
3、 写入数据时,同样通过strcpy即可,但是需要注意的事,写入数据的动作,不会改变测试文件的长度。亦即测试文件原来内容
的长度为10个字节,如果我们写入多于10个字节的内容进去,文件只会保存前10个。如果写入少于10个字节长度的内容,那后
面没有实际内容的空余字节会出现乱码。
4、munmap函数的作用是解除映射,一般与mmap配对使用,该函数相对简单,在这里不做详细介绍。
通过以上分析,我们采用了内存映射的方法来代替了传统的read、write等系统调用,可能相对于文件来说其便捷性还不能充分
得以展示,倘若运用在硬件物理地址映射到虚拟内存地址上,其效果就能很好地体现出来了。
这次我们来讲述mmap较为具体一点的实现细节。
mmap设备方法是file_operations结构的成员,在Mmap系统调用发出时被调用。在此之前,内核已经完成了很多工作。mmap设备方法所需要做的就是建立虚拟地址到物理地址的页表。
prototype :
int (*mmap)(struct file *, struct vm_area_struct *);
parameter:
struct file * : 需要操作的文件
struct vm_area_struct * : 内核自动帮我们找到的一个虚拟内存区域。
return :
虚拟内存区域起始地址
Linux内核使用结构vm_area_struct 来描述虚拟虚拟内存区域,其中几个主要成员如下:
unsigned long vm_start : 虚拟内存区域起始地址
unsinged long vm_end : 虚拟内存区域结束地址
unsigned long vm_flags : 该区域的标记(能否直接把信息通过虚拟地址存入物理地址等)
通过上面的介绍,其实mmap是如何完成页表的建立呢?方法有两种:
1.使用remap_pfn_range一次建立所有页表
2.使用nopage VMA方法每次建立一个页表
我们这里详细介绍方法一。
prototype :
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
parameter:
vma : 虚拟内存区域指针
addr : 虚拟地址的起始值
pfn : 要映射的物理地址的页帧号,即将物理地址右移PAGE_SHIFT(12位),至于为什
么是12位,敬请查看《深入理解LINUX内核》内存管理部分
size : 要映射的区域的大小。
prot : VMA的保护属性。
return : 返回虚拟内存起始地址。
操作实例:
int memdev_mmap(struct file * filp, struct vm_area_struct * vma)
{
vma -> vm_flags |= VM_IO;
vma -> vm_flags |= VM_RESERVED; //设置保护属性
if (remap_pfn_range(vma, vma -> vm_start, virt_to_phys(dev -> data) >> PAGE_SHIFT, size, vma -> vm_page_prot))
{
return -EAGAIN;
}
return 0;
}
注意这里的remap_pfn_range函数里的virt_to_phys(dev -> data),因为例子采用的“设备”其实就是内存,所以需要先转化成物理地址再移位。如果以后我们操作实际的硬件,这里就不用转化了,直接填入物理地址即可。