Android NDK——实战演练之从零到零点八真正详解存储映射mmap(一)

引言

或许对于很多Android 程序员来说,谈到I/O就以为只有一种Java 中提供的传统标准文件I/O,殊不知其实内核还提供了另一种在一定程度上更高效的方式——mmap,事实上内核中进程间的通信很多地方都是通过mmap在两个进程间提供共享存储区,当然不仅仅是在内核层面,很多互联网App中也有使用mmap替代传统I/O,比如说微信开源的MMKV框架,它比SharedPreference更高效的原因之一就是采用了mmap。

一、存储映射概述

存储映射 I/O 全称memory-mapped I/O能够将磁盘文件映射到内存中的一个缓冲区上(即真实的内存地址与文件数据一一对应)。那么经过存储映射之后,当从内存缓冲区中读取数据时,就相当于是读取文件中的对应的字节数据,而将数据写入缓冲区时,操作系统会自动帮你把对应的数据写到磁盘文件中,进而避免了传统的read和write方式执行I/O。此外存储映射I/O常被用在进程间通信时提供共享存储区。

在从内存缓冲区读取字节数据时,内核自动读输入文件;而在将数据写入缓冲区时,内核也自动将数据写到磁盘文件,而数据被写到文件的确切时间依赖于内核的页(Page)管理算法,因为有些内核设置了守护进程,在系统运行期间,它慢条斯理地将改写过的页写到磁盘上,如果想要确保数据安全地写到磁盘文件中,则需要在进程终止前以MS_SYNC标志调用msync
在这里插入图片描述

二、mmap相关的函数

上面所说的存储映射是一种机制,所有的遵循POSIX的系统都需要支持它。所以内部怎么实现我们暂不需要去考究,对于我们开发者来说mmap就是内核提供给应用层的一个开发接口,一个api,按照预定的步骤和规范使用这个api我们就可以使用mmap机制来为我们工作,完成特定的任务,需要注意的是Linux采用的是分页来管理内存,意味着在内存管理时,是以页为基本单位的(一般32位的系统一页的大小为4096个字节),即映射也是基于页的。

页大小(Page),Linux系统中是按照页来管理内存的,页是内核内存管理单元(MMU)的粒度单位,它也是内存中允许有不同权限和行为的最小单元,页是内存映射的基本块,因而也是进程地址空间的基本块。mmap系统调用的操作单元是以页为基础的,因此参数addr和__offset必须按页大小对齐即大小必须是整数倍,换言之,映射区域的大小是也大小的整数倍,若调用方提供的len参数没有按页对齐,映射区域会一直占满最后一个页,多出来的内存即最后一个有效字节到映射区域边界这部分的空间,会被0填充,对该区域的所有读操作都将返回0.

1、mmap函数创建映射

调用mmap函数可以告知内核将文件描述符fd对应的文件的__size个字节数据(起始位置是从__offset指定)映射到一个存储区域

#if defined(__USE_FILE_OFFSET64)
/*
 * mmap64 wasn't really around until L, but we added an inline for it since it
 * allows a lot more code to compile with _FILE_OFFSET_BITS=64.
 *
 * GCC removes the static inline unless it is explicitly used. We can get around
 * this with __attribute__((used)), but that needlessly adds a definition of
 * mmap64 to every translation unit that includes this header. Instead, just
 * preserve the old behavior for GCC and emit a useful diagnostic.
 */
void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset)
	#if !defined(__clang__) && __ANDROID_API__ < __ANDROID_API_L__
	    __attribute__((error("mmap is not available with _FILE_OFFSET_BITS=64 when using GCC until "
	                         "android-21. Either raise your minSdkVersion, disable "
	                         "_FILE_OFFSET_BITS=64, or switch to Clang.")));
	#else
	    __RENAME(mmap64);
	#endif  /* defined(__clang__) */
#else
	void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset);
#endif  /* defined(__USE_FILE_OFFSET64) */

#if __ANDROID_API__ >= __ANDROID_API_L__
	void* mmap64(void* __addr, size_t __size, int __prot, int __flags, int __fd, off64_t __offset) __INTRODUCED_IN(21);
#endif

其中各参数的含义为:

  • __addr——用于指定映射存储区的起始地址,通常设置为0,表示由系统决定该映射区的起始地址,若自己指定的话需要与页长对齐。

  • __size——用于指定要映射文件的__size字节长度,如果指定的size小于一个页长,那么内核也会自动映射一个页长,总之是内核是以页长对齐来映射的,但是内核没有那么智能会去主动判断你给的值是否合理,比如说你的数据长度只有512如果你给2048那么就会造成把其他原本不应该被映射的区域映射了,此时就会造成把多余的字节数据写入到磁盘中。

  • __fd——用于指定要映射的文件描述符

  • __offset——用于指定要映射文件里的起始位置的偏移量,0则表示从文件的第一个字节开始映射且必须与页长对齐

  • __prot——用于指定该存储区域的所请求的访问权限,固定的常量值

__prot说明
PROT_READ映射区可读
PROT_WRITE映射区可写
PROT_EXEC映射区可执行
PROT_NONE映射区不可访问

可取PROT_NONE,也可指定为PROT_READ、PROT_WRITE和PROT_EXEC的任意组合的按位与,但是对指定映射存储区域的权限不能超过原始文件的访问权限,比如说原始文件只是可读的那么映射区域不可指定为PROT_WRITE。

  • __flags——用于指定该存储区域的类型及一些其他属性,下表中是比较常用的标志取值,
__flags说明
MAP_SHARED映射区共享,和所有其他映射该文件的进程共享这一个区域,对内存的写操作相当于是对真实文件的操作,读该映射区域的操作会受到其他映射进程的影响。
MAP_PRIVATE映射区不共享,对映射区的存储操作会创建该文件的一个私有副本,所有后来对改映射区的引用都是引用该副本,进程对内存的任何改变都不影响真正的文件或其他进程的映射。
MAP_FIXED表示返回值必须等于__addr,若内核无法映射到对应的__addr位置则映射失败,而且如果地址和指定偏移量和已有的映射区域重叠,则会重叠区原有内容就会被丢弃,通过新的内容进行填充,尽量少用这个值。
注意MAP_SHARED和MAP_PRIVATE必须指定一个,但不能同时指定。当映射文件描述符时,文件的引用计数就会加1,因此,如果映射文件后再关闭文件,进程依然可以访问该文件,当你取消映射或进程终止时,对应的文件引用计数就会减1.

mmap映射成功之后则返回映射区的起始地址,反之则返回MAP_FAILED,并且设置相应的erro值(mmap函数永远不会返回0):
在这里插入图片描述
另外,与映射区相关的信号有SIGSEGV和SIGBUS,当进程试图访问对他不可用或写一块只读的映射区域会产生SIGSEGV信号;而又当进程识图访问一块已经失效的映射存储区域时会产生SIGBUS。

2、munmap函数解除映射

通常当进程终止时,内核会自动解除其相关的存储映射区域的映射,但是关闭映射存储区使用的文件描述符并不会自动解除映射,此时就可以通过调用munmap函数主动解除进程地址空间从__addr开始__size字长的内存中所有页面的映射,若解除成功则返回0,反之返回-1

int munmap(void* __addr, size_t __size);
  • __addr——用于指定要解除映射存储区的起始地址,通常设置为mmap的返回值
  • __size——用于指定要解除映射文件的__size字节长度,理论上来说应该和mmap中的一致,内核会自动进行页面对齐,但是实际测试中发现但是假如你32位系统映射的长度为4097,则映射的页长为2页,此时最大可以传8192,超过这个值就会导致 Fatal signal 11 (SIGSEGV), code 1, fault addr,而且我在测试的时候即使传递了小于1个页长的值,按道理应该只解除一个页长的映射,原来是2个页长的映射,剩下的一个页长的内存空间也不可用。

3、msync函数通过映射同步文件

通过调用msync函数可以将mmap生成的映射在内存中的任何修改写回到磁盘中,从实现映射与磁盘文件的同步(相当于是调用了fsync函数),具体表现为文件或其子集在内存的映射是从__addr开始的__size长度字节被写回到磁盘,参数__addr必须是页对齐的,一般可指定为上一次mmap的返回值。

int msync(void* __addr, size_t __size, int __flags);

如果不调用msync函数可能无法保证在映射被取消之前,修改过的映射会被写回到自盘中,因为与write不同(被write修改的缓冲区被保存在一个队列中等待被写回),存储映射是基于页操作的,当向存储映射写入数据时,进程会直接修改内核页缓存中的文件页而无需经过内核,因此内核不会立即同步页缓存到磁盘

  • __addr——用于指定要同步映射存储区的起始地址,通常设置为mmap的返回值
  • __size——用于指定要同步映射文件的__size字节长度,与munmap中的类似。
  • __flags——用于指定内核冲洗存储区的控制方式,一定要指定MS_SYNC或MS_ASYNC
__flags说明
MS_SYNC指定内核简单地调式要写的页
MS_ASYNC返回之前等待写操作完成
MS_INVALIDATE指定所有其他的该块的映射拷贝都失效,后期对该文件的所有区域映射的访问都将直接同步到磁盘

成功时则返回0,失败返回-1并把erro设置为对应的值:

erro说明
EINVALflags 同时设置了MS_SYNC和MS_ASYNC或者参数__addr没有页对齐
ENOMEM指定的内存区域(或其中一部分)没有被映射,需要注意的是按POSIX规定,Linux在处理请求同步一块部门被解除映射的内存时,虽然会返回ENOMEM,但它依然会同步该区域其他所有有效的映射。

在这里插入图片描述

4、mprotect函数更改映射权限

通过mprotect函数可以更改已有映射存储区域的权限,调用这个函数之后则可以改变**[__addr,__addr+__size]区域的权限**,

int mprotect(void* __addr, size_t __size, int __prot);
  • __addr——用于指定要更改映射存储区的起始地址,必须与系统页长对齐,通常设置为mmap的返回值
  • __size——用于指定要改变映射文件的__size字节长度,必须是系统页长的整数倍
  • __prot——用于指定该存储区域的所请求的访问权限,固定的常量值,与mmap类似。

更改成功会则返回0,反之返回-1,并把设置erro值:
在这里插入图片描述

5、mremap函数调整已有映射区域的大小

通过调用mremap函数可以扩大或者缩小已有映射区域的大小。调用这个函数之后则可以改变**[__addr,__addr+__old_size)区域的大小为new_size**。内核还可以同时移动映射区域,这取决于进程地址空间可用及flag参数值,其中

void* mremap(void* __old_addr, size_t __old_size, size_t __new_size, int __flags, ...);
  • __flags——取0或MREMAP_MAYMOVE表示可根据需求移动映射区域并设置为重新制定的大小,如果内核可以移动映射区域一个较大值的大小调整操作才更有可能会成功。

调整成功会则返回新的映射区域的首地址,反之返回MAP_FAILED,并把设置erro值:
在这里插入图片描述

三、mmap 的优点和不足

有的系统将普通文件复制到另一个普通文件中时,存储映射I/O可能会比较快,但是这种技术不能用在某些设备之间(如网络设备或终端设备)进行复制,并且在对复制文件进行映射之后,也需要注意该文件的长度是否改变。

1、mmap 的优点

在这里插入图片描述

2、mmap 的不足

某些系统处理的时间可能并没有在程序中计算,比如系统守护进程把页写到磁盘中的操作,由于需要为读和写分配页,系统的守护进程会帮助我们准备可用的页,如果页的写操作是随机的而非连续的,那么它们写入磁盘所需时间会更长,因此在页可以被用来复用之前所等待的时间也会更长。

在这里插入图片描述

四、Android中借助mmap实现I/O

预知后事如何,请参见下篇文章Android NDK——实战演练之从零到零点八Android中借助mmap实现高效I/O(二)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值