前言
mmap在日常開發(fā)中偶爾會遇到的一個關(guān)鍵詞,最常用到的場景是MMKV,其次用到的是日志打印。雖然都已經(jīng)被封裝好启妹,但也需要了解下mmap的基本原理和過程。
正文
進(jìn)程是App運(yùn)行的基本單位醉旦,進(jìn)程之間相對獨(dú)立饶米。iOS系統(tǒng)中App運(yùn)行的內(nèi)存空間地址是虛擬空間地址,存儲數(shù)據(jù)是在各自的沙盒车胡。
當(dāng)我們在App中去讀寫沙盒中的文件時檬输,我們會使用NSFileManager去查找文件,然后可以使用NSData去加載二進(jìn)制數(shù)據(jù)匈棘。文件操作的更底層實(shí)現(xiàn)過程褪猛,是使用linux的read()
、write()
函數(shù)直接操作文件句柄(也叫文件描述符羹饰、fd)伊滋。
在操作系統(tǒng)層面,當(dāng)App讀取一個文件時队秩,實(shí)際是有兩步:先將文件從磁盤讀取到物理內(nèi)存笑旺,再從系統(tǒng)空間拷貝到用戶空間(可以認(rèn)為是復(fù)制到系統(tǒng)給App統(tǒng)一分配的內(nèi)存)。
iOS系統(tǒng)使用頁緩存機(jī)制馍资,通過MMU(Memory Management Unit)將虛擬內(nèi)存地址和物理地址進(jìn)行映射筒主,并且由于進(jìn)程的地址空間和系統(tǒng)的地址空間不一樣,所以還需要多一次拷貝。
而mmap將磁盤上文件的地址信息與進(jìn)程用的虛擬邏輯地址進(jìn)行映射乌妙,建立映射的過程與普通的內(nèi)存讀取不同:正常的是將文件拷貝到內(nèi)存使兔,mmap只是建立映射而不會將文件加載到內(nèi)存中。
這樣做的注意事項(xiàng):
- 1藤韵、犧牲較大的虛擬內(nèi)存虐沥,映射區(qū)域有多大就需要虛擬內(nèi)存有多大;(故而太大的文件不適合映射整個文件泽艘,32位虛擬內(nèi)存最大是4GB欲险,可以只映射部分)
- 2、因?yàn)橛成溆蓄~外的性能消耗匹涮,所以適用于頻繁讀操作的場景天试;(單次使用的場景不建議使用)
- 3、因?yàn)槊看尾僮鲀?nèi)存會同步到磁盤然低,所以不適用于移動磁盤或者網(wǎng)絡(luò)磁盤上的文件喜每;
- 4、變長文件不適用雳攘;
iOS中的mmap
以官網(wǎng)的demo為例灼卢,其他的代碼很簡明直接,核心就在于mmap函數(shù)来农。
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:映射開始地址,設(shè)置NULL則讓系統(tǒng)決定映射開始地址崇堰;
length:映射區(qū)域的長度沃于,單位是Byte;
prot:映射內(nèi)存的保護(hù)標(biāo)志海诲,主要是讀寫相關(guān)繁莹,是位運(yùn)算標(biāo)志;(記得與下面fd對應(yīng)句柄打開的設(shè)置一致)
flags:映射類型特幔,通常是文件和共享類型咨演;
fd:文件句柄;
off_toffset:被映射對象的起點(diǎn)偏移蚯斯;
用官網(wǎng)的代碼做參考薄风,寫了一個讀寫的例子:
#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是常見的緩存工具,但是數(shù)據(jù)的同步有時會不及時拍嵌,比如說在crash前保存的值很容易出現(xiàn)保存失敗的情況遭赂,在App重新啟動之后讀取不到保存的值。
MMKV很好的解決了NSUserDefault的局限横辆,具體的好處可以見官網(wǎng)撇他。
但是同樣由于其獨(dú)特設(shè)計(jì),在數(shù)據(jù)量較大、操作頻繁的場景下困肩,會產(chǎn)生性能問題划纽。
這里的使用給出兩個建議:
1、不要全部用defaultMMKV锌畸,根據(jù)業(yè)務(wù)大的類型做聚合勇劣,避免某一個MMKV數(shù)據(jù)過大,特別是對于某些只會出現(xiàn)一次的新手引導(dǎo)蹋绽、紅點(diǎn)之類的邏輯芭毙,盡可能按業(yè)務(wù)聚合,使用多個MMKV的對象卸耘;
2退敦、對于需要頻繁讀寫的數(shù)據(jù),可以在內(nèi)存持有一份數(shù)據(jù)緩存蚣抗,必要時再更新到MMKV侈百;
NSData與mmap
NSData是我們常用類,有一個靜態(tài)方法和mmap有關(guān)系翰铡。
+ (id)dataWithContentsOfFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;
NSDataReadingOptions有一個參數(shù)是NSDataReadingMappedIfSafe钝域。
Mapped的意思是使用mmap,這個ifSafe是什么意思呢锭魔?和另外一個參數(shù)NSDataReadingMappedAlways有什么區(qū)別例证?
先看看這三個參數(shù)具體的意思:
NSDataReadingUncached
: 不要緩存,如果該文件只會讀取一次迷捧,這個設(shè)置可以提高性能织咧;
NSDataReadingMappedIfSafe
: 在保證安全的前提下使用mmap;
NSDataReadingMappedAlways
: 使用mmap漠秋;
如果使用mmap笙蒙,則在NSData的生命周期內(nèi),都不能刪除對應(yīng)的文件庆锦。
如果文件是在固定磁盤捅位,非可移動磁盤、網(wǎng)絡(luò)磁盤搂抒,則滿足NSDataReadingMappedIfSafe艇搀。對iOS而言,這個NSDataReadingMappedIfSafe=NSDataReadingMappedAlways
求晶。
那什么情況下應(yīng)該用對應(yīng)的參數(shù)中符?
如果文件很大,直接使用dataWithContentsOfFile
方法誉帅,會導(dǎo)致load整個文件淀散,出現(xiàn)內(nèi)存占用過多的情況右莱;此時用NSDataReadingMappedIfSafe,則會使用mmap建立文件映射档插,減少內(nèi)存的占用慢蜓。
使用場景舉例——視頻加載,視頻文件通常比較大郭膛,但是使用的過程中不會同時讀取整個視頻文件的內(nèi)### 總結(jié)
mmap就是文件的內(nèi)存映射晨抡,通常讀取文件是將文件讀取到內(nèi)存,會占用真正的物理內(nèi)存则剃;而mmap是用進(jìn)程的內(nèi)存虛擬地址空間去映射實(shí)際的文件中耘柱,這個過程由操作系統(tǒng)處理。mmap不會為文件分配物理內(nèi)存棍现,而是相當(dāng)于將內(nèi)存地址指向文件的磁盤地址调煎,后續(xù)對這些內(nèi)存進(jìn)行的讀寫操作,會由操作系統(tǒng)同步到磁盤上的文件己肮。
iOS中使用mmap可以用c方法的mmap()士袄,也可以使用NSData的接口帶上NSDataReadingMappedIfSafe參數(shù)。前者自由度更大谎僻,后者用于讀取數(shù)據(jù)娄柳。
附錄
mmap蘋果官方文檔
NSDataReadingMappedIfSafe
iOS內(nèi)存映射mmap詳解
linux中的頁緩存和文件IO
從內(nèi)核文件系統(tǒng)看文件讀寫過程
linux內(nèi)存映射mmap原理分析