前言
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不會為文件分配物理內存,而是相當于將內存地址指向文件的磁盤地址,后續對這些內存進行的讀寫操作,會由操作系統同步到磁盤上的文件。
iOS中使用mmap可以用c方法的mmap(),也可以使用NSData的接口帶上NSDataReadingMappedIfSafe參數。前者自由度更大,后者用于讀取數據。
附錄
mmap蘋果官方文檔
NSDataReadingMappedIfSafe
iOS內存映射mmap詳解
linux中的頁緩存和文件IO
從內核文件系統看文件讀寫過程
linux內存映射mmap原理分析