本文主要讲述

  • mmap 函数的使用,与驱动中 mmap 函数的实现
  • mmap 怎样使用,怎样实现,为什么 mmap 可以减少额外的拷贝?

下面简单介绍。

一、 mmap 的使用

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off );
int munmap(void *addr, size_t length);

函数属于系统调用级别,负责内存映射。

描述

mmap

把文件或者设备映射到内存。这个函数在调用进程的虚拟地址空间中创建一块映射区域。

addr 指定映射区域的首地址。如果 addrNULL ,那么由内核来选择一个地址来创建映射的区域,否则创建的时候会尽可能地使用 addr 的地址。在linux系统中,创建映射的时候应该是在下一个页面的边界创建, addrNULL 的时候,程序的可移植性最好。

len 指定文件被映射的长度, 或者映射区域的长度。

offset 指定从文件的哪个偏移位置开始映射。=offset= 必须是页面大小的整数倍页面的大小可以由 sysconf(_SC_PAGE_SIZE) 来返回.

prot 指定内存的保护模式(具体参见 man ), flags 指定区域在不同进程之间的共享方式,以及区域是否同步到相应的文件等等(具体参见 man ).

这个函数返回新创建的页面的地址。

munmap

取消 address 指定地址范围的映射。以后再引用取消的映射的时候就会导致非法内存的访问。

这里 address 应该是页面的整数倍, length 指定取消映射的地址长度。

成功的时候这个函数返回0, 失败的时候,两者都返回-1.

举例

//hello hello hello
/*程序功能:
 * 1)主要测试mmap和munmap的2)简单的write
 * 具体为:
 * 在命令中分别指定文件名,映射的长度,映射的起始地址.
 * 将文件映射到内存中
 * 把映射到内存中的内容用write写到标准输出。
 * 注意,这里没有对越界进行检测。
 * */
#include <sys/mman.h>//mmap
#include <unistd.h>//sysconf
#include <fcntl.h>//file open
#include <stdio.h>//printf
int main(int argc, char *argv[])
{
    if(argc != 4)
    {
        write(STDOUT_FILENO,"hello\n",6);
        printf("usage:%s \n",argv[0]);
        return 1;
    }
    char *filename = argv[1];//1)指定文件
    printf("the file to be mapped is:%s\n",filename);
    int fd = open(filename,O_RDONLY);

    int offset = atoi(argv[2]);//2)指定映射起始地址(页面的整数倍)
    printf("start offset of file to be mapped is:%d\n",offset);
    printf("page size is:%ld\n",sysconf(_SC_PAGE_SIZE));
    int realOffset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);//转换成页面的整数倍
    printf("real start offset of file to be mapped is:%d\n",realOffset);

    int length = atoi(argv[3]);//3)指定映射长度
    printf("the length to be map is:%d\n",length);
    int realLen = length+offset-realOffset;//实际写入的字节数
    printf("the real length to be map is:%d\n",realLen);

    //mmap的参数分别是:
    //NULL,让内核自己选择映射的地址;realLen指定映射的长度;
    //PROT_READ只读;MAP_PRIVATE不和其他的进程之间共享映射区域,数据也不写入对应的文件中;
    //realOffset映射文件的起始地址(页面的整数倍)。
    char *addr = mmap(NULL, realLen,PROT_READ,MAP_PRIVATE,fd,realOffset);//4)开始映射

    //关闭打开的文件,实际程序退出的时候会自动关闭。
    //关闭文件之后,相应的映射内存仍旧存在,映射的内存用munmap关闭。
    close(fd);
    //write的参数分别是:
    //STDOUT_FILENO:文件描述符号(这里是标准输出)
    //addr,将要写入文件的内容的地址
    //realLen,写入的长度,长度以addr作为起始地址
    write(STDOUT_FILENO,addr,realLen);//将映射的内容写到标准输出
    munmap(addr,realLen);//5)关闭映射的内存
    //write(STDOUT_FILENO,addr,realLen);//不能使用了
    printf("\n");
}

二、 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 系统调用向相应设备文件写之后,最终会进入到这个函数进行真正的读取所需操作。

static ssize_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 可以将内核空间内的指针数据拷贝给用户空间的指针所指)再访问。除非直接将内存映射,否则一定要拷贝才能访问用户空间数据。

四、其它

参考: http://blog.chinaunix.net/uid-9525959-id-3063123.html