现在的位置: 首页 > 综合 > 正文

mmap函数:用法&实现

2017年05月19日 ⁄ 综合 ⁄ 共 6162字 ⁄ 字号 评论关闭

mmap函数的使用,与驱动中mmap函数的实现


mmap
怎样使用,怎样实现,为什么mmap可以减少额外的拷贝?
下面简单介绍。

一、mmap的使用


*内存映射:
#include<sys/mman.h>
void *mmap(void *addr, //
映射去首地址

size_tlength,  //指定映射区域的长度

int prot, int flags, //指定进程共享方式
       
int fd,

off_t offset); //开始映射的文件位置

int munmap(void *addr, size_t length);

[描述]

mmap

把文件或者设备映射到内存

。这个函数在调用进程的虚拟地址空间中创建一块映射区域。映射区域的首地址在addr中指定,length指定映射区域的长度。如果addr是NULL,那么由内核来选择一个地址来创建映射的区域,否则创建的时候会尽可能地使用addr的地址。在linux系统中,创建映射的时候应该是在下一个页面的边界创建,addr是NULL的时候,程序的可移植性最好。
length指定文件被映射的长度,offset指定从文件的哪个偏移位置开始映射,offset必须是页面大小的整数倍页面的大小可以由sysconf(_SC_PAGE_SIZE)来返回.
prot指定内存的保护模式(具体参见man)
.
flags指定区域在不同进程之间的共享方式,以及区域是否同步到相应的文件等等(具体参见man)
.
这个函数返回新创建的页面的地址。

munmap

取消address指定地址范围的映射。以后再引用取消的映射的时候就会导致非法内存的访问。这里address应该是页面的整数倍。
成功的时候这个函数返回0。


失败的时候,两者都返回-1.
[举例
]

1        //hellohello hello

2        /*程序功能:

3        * 1)主要测试mmap和munmap的2)简单的write

4        *具体为:

5        *在命令中分别指定文件名,映射的长度,映射的起始地址.

6        *将文件映射到内存中

7        *把映射到内存中的内容用write写到标准输出。

8        *注意,这里没有对越界进行检测

9       
*
*/

10    
#include <sys/mman.h>//mmap

11    
#include <unistd.h>//sysconf

12    
#include <fcntl.h>//fileopen

13    
#include <stdio.h>//printf

14     intmain(int
argc, char *argv[])

15     {

16    
   
if(argc!= 4)

17    
   
{

18    
       
write(STDOUT_FILENO,"hello\n",6);

19    
       
printf("usage:%s <filename> <offset><length>\n",argv[0]);

20    
       
return 1;

21    
   
}

22        char
*filename= argv[1];//1)指定文件

23    
  
printf("the file to be mapped is:%s\n",filename);

24    
   
int fd= open(filename,O_RDONLY);

25    

26        int offset
= atoi(argv[2]);//2)指定映射起始地址(页面的整数倍)

27    
   
printf("start offset of file to be mapped is:%d\n",offset);

28    
   
printf("page size is:%ld\n",sysconf(_SC_PAGE_SIZE));

29        intrealOffset
= offset&~(sysconf(_SC_PAGE_SIZE)- 1);//转换成页面的整数倍

30    
   
printf("real start offset of file to be mappedis:%d\n",realOffset);

31    

32        int length
= atoi(argv[3]);//3)指定映射长度

33    
   
printf("the length to be map is:%d\n",length);

34        int realLen
= length+offset-realOffset;//实际写入的字节数

35    
   
printf("the real length to be map is:%d\n",realLen);

36    

37        //mmap的参数分别是:

38        //NULL,让内核自己选择映射的地址;realLen指定映射的长度;

39        //PROT_READ只读;MAP_PRIVATE不和其他的进程之间共享映射区域,数据也不写入对应的文件中;

40        //realOffset映射文件的起始地址(页面的整数倍)

41        Char*addr=mmap(NULL,realLen,PROT_READ,MAP_PRIVATE,fd,realOffset);//4)开始映射

42    

43        //关闭打开的文件,实际程序退出的时候会自动关闭。

44        //关闭文件之后,相应的映射内存仍旧存在,映射的内存用munmap关闭。

45    
   
close(fd);

46        //write的参数分别是:

47        //STDOUT_FILENO:文件描述符号(这里是标准输出)

48        //addr,将要写入文件的内容的地址

49        //realLen,写入的长度,长度以addr作为起始地址

50        write(STDOUT_FILENO,addr,realLen);//将映射的内容写到标准输出

51        munmap(addr,realLen);//5)关闭映射的内存

52        //write(STDOUT_FILENO,addr,realLen);//不能使用了

53    
   
printf("\n");

54     }

二、mmap实现

驱动中有对mmap的具体实现。用户调用mmap系统调用函数之后,最终会调用到驱动中的mmap函数接口。下面是一个例子:
static int commdrv_mmap(struct file* file, struct vm_area_struct* vma)
{
    long phy_addr;
    unsigned long offset;
    unsigned long size;
    vma->vm_page_prot =pgprot_noncached(vma->vm_page_prot);
    vma->vm_flags |= VM_LOCKED;
    offset = vma->vm_pgoff <<PAGE_SHIFT;/*XXX assume is 12*/
    size = vma->vm_end -vma->vm_start;
    if(BUF0_OFF == offset) {
        phy_addr = PHYS_BASE0;
    } else if(BUF1_OFF == offset) {
        phy_addr = PHYS_BASE1;
    } else if(START_OFF == offset) {
        phy_addr = PHYS_BASE;
    } else {
        return -ENXIO;
    }
    /*phy_addr must be 4k *n*/
    if(remap_pfn_range(vma,vma->vm_start, phy_addr >> PAGE_SHIFT, size, vma->vm_page_prot)) {
        return -ENXIO;
    }
    return 0;
}

对于以上代码,
"vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);"表示要映射的内存是非cached的,这样不会存在缓存中的数据和实际数据不一致的情况,但是速度会比cached的要慢。
offset表示要映射的数据的偏移,这个偏移量来自用户空间的mmap调用,用户空间传入的偏移在这里进行判断,虽然一般的文件就将这个偏移量做为文件偏移了,其实这个offset的含义,由驱动自己解释,不一定就是字节偏移,驱动根据这个偏移量来决定映射哪块内存,
size表示要映射的内存的大小。
"remap_pfn_range(vma, vma->vm_start, phy_addr >> PAGE_SHIFT,size, vma->vm_page_prot)"表示将根据被映射的物理地址,以及虚拟起始地址,和大小等信息,将相应的部分映射到用户空间。其中参数vma直接来自commdrv_mmap函数的参数,phy_addr是要映射的设备的物理地址(必须是页对齐的),只有少量的信息自己设置,大多来自外部。最后映射的地址,通过用户调用的mmap函数返回,用户可以直接操作。


三、mmap优点


mmap实现了将设备驱动在内核空间的部分地址直接映射到用户空间,使得用户程序可以直接访问和操作相应的内容。减少了额外的拷贝,而一般的readwrite函数虽然表面上直接向设备写入,其实还需要进行一次拷贝。
例如,下面是某个设备驱动中的的write实现,当外面用户程序调用write系统调用向相应设备文件写之后,最终会进入到这个函数进行真正的读取所需操作。
staticssize_t commdrv_write(struct file* filp, char __user* buf,
       
size_t count, loff_t* ppos)
{
    char* wbuf;
    wbuf = (char*)vmalloc(count);
    if(!wbuf) {
        return 0;
    }
    ret = copy_from_user
(wbuf, (char __user*)buf, count);
   
if(0 != ret) {
        vfree(wbuf);
        return 0;
    }
    .....do others things with wbuf......
    vfree(wbuf);
    return count;
}
由上面的代码可知,用户传入的数据指针buf,在驱动中(也就是内核空间)不能直接访问,必须使用copy_from_user将其拷贝到内核空间的一块内存,然后才能进行后续的操作(内核中不能不经过copy_from_user直接访问用户传下来的指针buf的地址的内容)。

mmap,使得将内核空间直接映射到了用户空间,让用户空间通过返回的指针直接访问,这样内核和用户空间直接操作同样的内存。也就是说,如果不使用mmap,那么由于在内核空间的代码,和外面用户空间的代码对应的地址空间不同,这样内核空间和用户空间不能互相访问其指针;如果想要访问,对方指针的内容,那么只能通过copy_from_user之类的函数先将其数据拷贝到内核空间(相应的read一般使用copy_to_user可以将内核空间内的指针数据拷贝给用户空间的指针所指)再访问。除非直接将内存映射,否则一定要拷贝才能访问用户空间数据。


////////////////////////////////////////////////////////////////////////////////////////////  补充  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


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

负责把文件内容映射到进程的虚拟内存空间,通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。

addr: 映射的起始地址,通常为NULL,由系统指定

length:映射文件的长度

prot:映射区的保护方式 PROT_EXEC:映射区可被执行,PROT_READ映射区可被读取,PROT_WRITE映射区可被写入

flags:映射区的特性MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享
MAP_PRIVATE:
对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域的修改不会写回源文件。

fd:由open返回的文件描述符,代表要映射的文件

offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0,表示从文件头开始映射。

注意mmap不能映像原有文件的长度

 

int munmap(void *start, size_t length)

成功返回0,失败返回-1. start的取值一般是mmap返回的地址

 

虚拟内存区域:是虚拟地址空间的一个同质区间,即有同样特性的连续地址范围。一个进程的内存映像由以下几部分组成:程序代码数据BSS和栈区域,以及内存映射区域

 

一个进程的内存区域可以通过查看 /proc/pid/maps

每一行的域为:start_endperm offset major:minor inode

 

linux内核使用结构vm_area_struct来描述虚拟内存区域,其中主要成员如下:

unsigned longvm_start 虚拟内存区域起始地址

unsigned longvm_end 虚拟内存区域结束地址

unsigned longvm_flags

 

映射一个设备是指把用户空间的一段地址关联到设备内存上。

mmap做了三件事

1.找到用户空间地址(内核完成)

 2.找到设备的物理地址(原理图) 3.关联(通过页式管理)\

 

mmap设备方法需要做的就是建立虚拟地址到物理地址的页表。

int(*mmap)(structfile *, struct vm_area_struct *)

建立页表有两种方法:

1.使用remap_pfn_range一次建立所有页表;

2.使用nopage VMA方法每次建立一个也表。

int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot)

vma:虚拟内存区域指针 virt_addr:虚拟地址的起始值 pfn:要映射的物理地址所在的物理页祯号,可将物理地址>>PAGE_SHIFT得到(PAGE_SHIFT为12,相当于除以4KB)

size:要映射的区域的大小  prot:VMA的保护属性

抱歉!评论已关闭.