一、简介:
mmap是一种内存映射文件的方法。即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享
二、BIO(常规文件操作或传统IO)和mmap区别:
总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝
。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。
三、优缺点
优点:(高性能,操作文件就像操作内存一下,适合对较大文件的读写)
①对文件的读写操作跨国也页缓存,减少数据的拷贝次数,用内存读写取代IO流读写,提高了文件读写效率(Andorid加载.dex文件也通过使用此技术);
②实现用户空间和内核空间的高效交互方式;
③提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
④实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助磁盘空间协助操作,补充内存的不足。但是进一步会照成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好解决,需要用磁盘空间替代内存的时候,mmap都可以发挥其功效
;
缺点:①文件如果很小,比如小于4K的,比如60bytes,由于在内存当中的组织都是按页组织的,将文件调入到内存当中是一个页4K,相当于4096-60=4036bytes的内存空间浪费掉了;②文件无法完成拓展,因为mmap到内存的时候,你所能操作的范围就已经确定了,无法增加文件长度。
使用场景:
①对同一块区域频繁读写操作;
②用户日志、数据上报等,微信开源mars框架中的xlog模块就是基于mmap特性实现;
③跨进程同步的时候,mmap是个不错的选择,Android跨进程通信有自己独有的Binder机制,内部使用mmap实现;
Java层面使用:MappedByteBuffer
已经封装好
C++代码实现:mmap
四、内存映射原理
- 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
- 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
- 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
五、mmap相关函数
①建立映射关系函数:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功执行时,mmap()返回被映射区的指针地址。失败时,mmap()返回MAP_FAILED[其值为(void *)-1]
②解除映射关系函数:
int munmap( void * addr, size_t len )
成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;
addr是调用mmap()时返回的地址,len是映射区的大小;
③实时同步写入:
int msync( void *addr, size_t len, int flags )
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。
可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
六、mmap使用细节
①mmap映射区域大小必须是物理页大小(page_size)的倍数(32位系统中通常是4k字节)
,原因是内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位;
②内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节
。
③映射建立之后,即使文件关闭,映射依然存在
。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。
七、使用:
①C++使用mmap读写文件:
//-------------------------------------------读取文件----------------------------------
// 打开文件
int fd = open("input.txt", O_RDONLY);
// 读取文件长度
int len = lseek(fd,0,SEEK_END);
// 建立内存映射
char *addr = (char *) mmap(NULL, len, PROT_READ, MAP_PRIVATE,fd, 0);
close(fd);
// data用于保存读取的数据
char* data;
// 复制过来
memcpy(data, addr, len);
// 解除映射
munmap(addr, len)
//-------------------------------------------写入文件----------------------------------
//假设写入的数据放在char* data中
int len = data.length();
// 打开文件
int fd=open("output.txt", O_RDWR|O_CREAT, 00777);
// lseek将文件指针往后移动file_size-1位
lseek(fd,len-1,SEEK_END);
// 从指针处写入一个空字符;mmap不能扩展文件长度,这里相当于预先给文件长度,准备一个空架子
write(fd, "", 1);
// 使用mmap函数建立内存映射
char* addr = (char*)mmap(NULL, len, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
// 内存映射建立好了,此时可以关闭文件了
close(fd);
// 把data复制到addr里
memcpy(addr, data, len);
// 解除映射
munmap(addr, len)
①Java使用NIO库中的MappedByteBuffer
实现mmap读写文件:
try {
byte[] originalByte = "你好奥,我是好人".getBytes();
RandomAccessFile raf = new RandomAccessFile(file, "rw");
////position映射文件的起始位置,size映射文件的大小
MappedByteBuffer map = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 1024);
//写入数据
map.put(originalByte);
byte[] newData= new byte[originalByte.length];
//获取数据
map.get(newData);
Log.d(TAG, "data:" + String.valueOf(newData));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}