mmap内存映射

来源:互联网 发布:淘宝游戏王正版 编辑:程序博客网 时间:2024/06/06 04:14
参考文章:
Linux 内存映射函数 mmap()函数详解
linux内存映射mmap原理分析

mmap和普通文件读写的区别和比较 & mmap的注意点


mmap函数原型:
 void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)参数说明:start:要映射到的内存区域的起始地址,通常都是用NULL(NULL即为0)。NULL表示由内核来指定该内存地址length:要映射的内存区域的大小 prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起     -> PROT_EXEC //页内容可以被执行     -> PROT_READ  //页内容可以被读取     -> PROT_WRITE //页可以被写入     -> PROT_NONE  //页不可访问  flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体    MAP_FIXED :使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。     MAP_SHARED :对映射区域的写入数据会复制回文件内, 而且允许其他映射该文件的进程共享。     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函数返回)offset:表示被映射对象(即文件)从那里开始对映,通常都是用0。 该值应该为大小为PAGE_SIZE的整数倍
 返回值:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值     EACCES:访问出错      EAGAIN:文件已被锁定,或者太多的内存已被锁定      EBADF:fd不是有效的文件描述词      EINVAL:一个或者多个参数无效      ENFILE:已达到系统对打开文件的限制      ENODEV:指定文件所在的文件系统不支持内存映射      ENOMEM:内存不足,或者进程已超出最大内存映射数量      EPERM:权能不足,操作不允许      ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志      SIGSEGV:试着向只读区写入      SIGBUS:试着访问不属于进程的内存区  

函数munmap
 函数原型: int munmap(void *start, size_t length)  参数:start:要取消映射的内存区域的起始地址  length:要取消映射的内存区域的大小。 返回值:成功执行时munmap()返回0。失败时munmap返回-1. 
函数msync:
函数原型;int msync(const void *start, size_t length, int flags); 参数:start:要进行同步的映射的内存区域的起始地址。  length:要同步的内存区域的大小  flag:flags可以为以下三个值之一:           MS_ASYNC : 请Kernel快将资料写入。           MS_SYNC : 在msync结束返回前,将资料写入。           MS_INVALIDATE : 让核心自行决定是否写入,仅在特殊状况下使用 返回值:成功则返回0;失败则返回-1;

拷贝一些资料:

总结来说,常规文件操作为了提高读写效率和保护磁盘使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存再写回磁盘中(延迟写回),也是需要两次数据拷贝。

 

而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。



mmap使用细节

1、使用mmap需要注意的一个关键点是,mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。

2、内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。具体情形参见“情形三”。

3、映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。


针对第一点:
#define PAGE_SIZE 4096
然后后面的:

情形一:一个文件的大小是5000字节,mmap函数从一个文件的起始位置开始,映射5000字节到虚拟内存中。

分析:因为单位物理页面的大小是4096字节,虽然被映射的文件只有5000字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域8192个 字节,5000~8191的字节部分用零填充。

 

情形二:一个文件的大小是5000字节,mmap函数从一个文件的起始位置开始,映射15000字节到虚拟内存中,即映射大小超过了原始文件的大小。

分析:由于文件的大小是5000字节,和情形一一样,其对应的两个物理页。那么这两个物理页都是合法可以读写的,只是超出5000的部分不会体现在原文件中。由于程序要求映射15000字节,而文件只占两个物理页,因此8192字节~15000字节都不能读写,操作时会返回异常

情形三:一个文件初始大小为0,使用mmap操作映射了1000*4K的大小,即1000个物理页大约4M字节空间,mmap返回指针ptr。

分析:如果在映射建立之初,就对文件进行读写操作,由于文件大小为0,并没有合法的物理页对应,如同情形二一样,会返回SIGBUS错误。

但是如果,每次操作ptr读写前,先增加文件的大小,那么ptr在文件大小内部的操作就是合法的。例如,文件扩充4096字节,ptr就能操作ptr ~ [ (char)ptr + 4095]的空间。只要文件扩充的范围在1000个物理页(映射范围)内,ptr都可以对应操作相同的大小。

这样,方便随时扩充文件空间,随时写入文件,不造成空间浪费。

------------------以上拷贝自文章:https://www.cnblogs.com/charlesblc/p/6263665.html------------------------------------
原理什么的大概就是这样了,实现起来却遇到了一些问题,这里记录下来。顺便提几句,任何新的东西都要亲手去实践,光看原理,得到的就是假象。面试笔试的时候,这些都是损失的工资。
.....................................
看代码:
#include <unistd.h>#include <sys/time.h>#include <sys/types.h>#include <cstdlib>#include <cstring>#include <iostream>#include <sys/mman.h>#include <cstdio>#include <fcntl.h>using namespace std;#define PAGE_SIZE 4096int main(){    int fd;    timeval tv1,tv2;    memset(&tv1,0x00,sizeof(tv1));    memset(&tv2,0x00,sizeof(tv2));    //sleep(100);    fd=open("/usr/wang/linux/data",O_RDWR);    if(fd == -1)        puts("open file failed!");    int* mptr=(int*)mmap(0,PAGE_SIZE,PROT_WRITE | PROT_READ,MAP_SHARED,fd,0);    if(*mptr == -1)    {        cout << "file open failed!" << endl;        sleep(5);    }    cout << "fd=:" << fd << endl;//int nn=msync(mptr,PAGE_SIZE,MS_ASYNC);//if(!nn)//puts("sync success!");    //int sBuf[100]={0};    //int *array=sBuf;    gettimeofday(&tv1,NULL);    //int nLen=read(fd,(void*)mptr,sizeof(int)*100);//if(nLen <= 0)//{//cout << "read failed,nLen="  << nLen <<endl;//sleep(5);//}    //char sBuf[256]={0};    //memcpy(sBuf,mptr,sizeof(sBuf));    for(int i=0; i < 50; i++)    {        cout << (int)mptr[i]<< endl;        //printf("%s",sBuf);        ++mptr[i];        cout << mptr[i] << endl;    }//if(nLen != write(fd,(void*)mptr,nLen))//{//puts("write failed!");//}    gettimeofday(&tv2,NULL);    printf("spend time:%dmicroseconds!\n",(int)(tv2.tv_usec-tv1.tv_usec));    //free(array);    int n=munmap(mptr,PAGE_SIZE);    if(n == -1)    {        cout << "munmap failed!" << endl;        sleep(5);    }}

上面的注释,都是经验啊。可怜自己一开始学的windows,现在是真的难受。
-----------------------------------------------------
1> linux下文件不分二进制和文本文件,都是stream. 但是stream只是个说法,文件的存储格式其实还是有区别的。只是open函数打开文件时,不用也不能区分二进制和文本。
如果是普通的文本文件,比如自己之前vi一个文件输入几个数字。通过mmap访问得到的时ascii码,也是恶心了自己一手
2> mmap返回的时void*,如果打开的是二进制文件,这个文件是用比如fwrite逐个int字节写的。那么这里可以:
int * mptr=(int*)mmap(...);
后面通过mptr[i]来进行数组的操作
如果是文本文件的话,则要切割字符串,然后atoi.
3> mmamp没有read和write操作,自己一开始很不适应。如果只是看了别人的文章(里面有提到这一点),自己根本不会在意。所以啊,绝知此事要躬行。
4> open函数是unix/linux下特有的,返回的int的fd文件描述符,对应close.这里可以联系之前的socket编程里面的。
先写这么多,后续再补充吧...
原创粉丝点击