6

iOS的文件内存映射——mmap

 3 years ago
source link: https://www.jianshu.com/p/516e7ff6f251
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iOS的文件内存映射——mmap

12019.08.02 13:15:57字数 1,546阅读 4,099

mmap在日常开发中偶尔会遇到的一个关键词,最常用到的场景是MMKV,其次用到的是日志打印。虽然都已经被封装好,但也需要了解下mmap的基本原理和过程。

进程是App运行的基本单位,进程之间相对独立。iOS系统中App运行的内存空间地址是虚拟空间地址,存储数据是在各自的沙盒。
当我们在App中去读写沙盒中的文件时,我们会使用NSFileManager去查找文件,然后可以使用NSData去加载二进制数据。文件操作的更底层实现过程,是使用linux的read()write()函数直接操作文件句柄(也叫文件描述符、fd)。
在操作系统层面,当App读取一个文件时,实际是有两步:先将文件从磁盘读取到物理内存,再从系统空间拷贝到用户空间(可以认为是复制到系统给App统一分配的内存)。
iOS系统使用页缓存机制,通过MMU(Memory Management Unit)将虚拟内存地址和物理地址进行映射,并且由于进程的地址空间和系统的地址空间不一样,所以还需要多一次拷贝。

而mmap将磁盘上文件的地址信息与进程用的虚拟逻辑地址进行映射,建立映射的过程与普通的内存读取不同:正常的是将文件拷贝到内存,mmap只是建立映射而不会将文件加载到内存中。
这样做的注意事项:

  • 1、牺牲较大的虚拟内存,映射区域有多大就需要虚拟内存有多大;(故而太大的文件不适合映射整个文件,32位虚拟内存最大是4GB,可以只映射部分)
  • 2、因为映射有额外的性能消耗,所以适用于频繁读操作的场景;(单次使用的场景不建议使用)
  • 3、因为每次操作内存会同步到磁盘,所以不适用于移动磁盘或者网络磁盘上的文件;
  • 4、变长文件不适用;

iOS中的mmap

官网的demo为例,其他的代码很简明直接,核心就在于mmap函数。

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

*outDataPtr = mmap(NULL,
                   size,
                   PROT_READ|PROT_WRITE,
                   MAP_FILE|MAP_SHARED,
                   fileDescriptor,
                   0);

start:映射开始地址,设置NULL则让系统决定映射开始地址;
length:映射区域的长度,单位是Byte;
prot:映射内存的保护标志,主要是读写相关,是位运算标志;(记得与下面fd对应句柄打开的设置一致)
flags:映射类型,通常是文件和共享类型;
fd:文件句柄;
off_toffset:被映射对象的起点偏移;

用官网的代码做参考,写了一个读写的例子:

#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>

int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize)
{
    int outError;
    int fileDescriptor;
    struct stat statInfo;
    
    // Return safe values on error.
    outError = 0;
    *outDataPtr = NULL;
    *outDataLength = 0;
    
    // Open the file.
    fileDescriptor = open( inPathName, O_RDWR, 0 );
    if( fileDescriptor < 0 )
    {
        outError = errno;
    }
    else
    {
        // We now know the file exists. Retrieve the file size.
        if( fstat( fileDescriptor, &statInfo ) != 0 )
        {
            outError = errno;
        }
        else
        {
            ftruncate(fileDescriptor, statInfo.st_size + appendSize);
            fsync(fileDescriptor);
            *outDataPtr = mmap(NULL,
                               statInfo.st_size + appendSize,
                               PROT_READ|PROT_WRITE,
                               MAP_FILE|MAP_SHARED,
                               fileDescriptor,
                               0);
            if( *outDataPtr == MAP_FAILED )
            {
                outError = errno;
            }
            else
            {
                // On success, return the size of the mapped file.
                *outDataLength = statInfo.st_size;
            }
        }
        
        // Now close the file. The kernel doesn’t use our file descriptor.
        close( fileDescriptor );
    }
    
    return outError;
}


void ProcessFile(const char * inPathName)
{
    size_t dataLength;
    void * dataPtr;
    char *appendStr = " append_key";
    int appendSize = (int)strlen(appendStr);
    if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
        dataPtr = dataPtr + dataLength;
        memcpy(dataPtr, appendStr, appendSize);
        // Unmap files
        munmap(dataPtr, appendSize + dataLength);
    }
}

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
    NSLog(@"path: %@", path);
    NSString *str = @"test str";
    [str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
    ProcessFile(path.UTF8String);
    NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"result:%@", result);
}

MMKV和mmap

NSUserDefault是常见的缓存工具,但是数据的同步有时会不及时,比如说在crash前保存的值很容易出现保存失败的情况,在App重新启动之后读取不到保存的值。
MMKV很好的解决了NSUserDefault的局限,具体的好处可以见官网
但是同样由于其独特设计,在数据量较大、操作频繁的场景下,会产生性能问题。
这里的使用给出两个建议:
1、不要全部用defaultMMKV,根据业务大的类型做聚合,避免某一个MMKV数据过大,特别是对于某些只会出现一次的新手引导、红点之类的逻辑,尽可能按业务聚合,使用多个MMKV的对象;
2、对于需要频繁读写的数据,可以在内存持有一份数据缓存,必要时再更新到MMKV;

NSData与mmap

NSData是我们常用类,有一个静态方法和mmap有关系。

+ (id)dataWithContentsOfFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;
NSDataReadingOptions有一个参数是NSDataReadingMappedIfSafe。
Mapped的意思是使用mmap,这个ifSafe是什么意思呢?和另外一个参数NSDataReadingMappedAlways有什么区别?

先看看这三个参数具体的意思:
NSDataReadingUncached : 不要缓存,如果该文件只会读取一次,这个设置可以提高性能;
NSDataReadingMappedIfSafe : 在保证安全的前提下使用mmap;
NSDataReadingMappedAlways : 使用mmap;

如果使用mmap,则在NSData的生命周期内,都不能删除对应的文件。
如果文件是在固定磁盘,非可移动磁盘、网络磁盘,则满足NSDataReadingMappedIfSafe。对iOS而言,这个NSDataReadingMappedIfSafe=NSDataReadingMappedAlways

那什么情况下应该用对应的参数?
如果文件很大,直接使用dataWithContentsOfFile方法,会导致load整个文件,出现内存占用过多的情况;此时用NSDataReadingMappedIfSafe,则会使用mmap建立文件映射,减少内存的占用。
使用场景举例——视频加载,视频文件通常比较大,但是使用的过程中不会同时读取整个视频文件的内容,可以使用mmap优化。

mmap就是文件的内存映射,通常读取文件是将文件读取到内存,会占用真正的物理内存;而mmap是用进程的内存虚拟地址空间去映射实际的文件中,这个过程由操作系统处理。mmap不会为文件分配物理内存,而是相当于将内存地址指向文件的磁盘地址,后续对这些内存进行的读写操作,会由操作系统同步到磁盘上的文件。
iOS中使用mmap可以用c方法的mmap(),也可以使用NSData的接口带上NSDataReadingMappedIfSafe参数。前者自由度更大,后者用于读取数据。

mmap苹果官方文档
NSDataReadingMappedIfSafe
iOS内存映射mmap详解
linux中的页缓存和文件IO
从内核文件系统看文件读写过程
linux内存映射mmap原理分析


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK