什么是 MMap
MMap(Memory Mapping)是一种内存映射技术,它允许将文件或其他对象映射到进程的地址空间。在 iOS 开发中,mmap 是一个强大的系统调用,能够将磁盘文件的内容直接映射到内存地址空间,使得对文件的读写操作可以像访问内存一样简单高效。
与传统的文件 I/O(通过 read()/write() 系统调用)不同,mmap 通过虚拟内存机制,让应用程序可以像访问数组一样访问文件内容,操作系统会自动处理磁盘和内存之间的数据传输。
核心特点
- 零拷贝:数据不需要在用户空间和内核空间之间复制
- 延迟加载:只有真正访问数据时才从磁盘加载(页面调度)
- 共享内存:多个进程可以映射同一文件实现进程间通信
- 高效访问:随机访问文件时性能更优
MMap 的工作原理
传统文件 I/O 流程
应用程序 -> read() -> 内核缓冲区 -> 用户空间缓冲区 -> 应用程序
磁盘 -> DMA -> 内核缓冲区
这个过程涉及多次数据拷贝:
- 磁盘数据通过 DMA 拷贝到内核缓冲区
- 内核缓冲区数据拷贝到用户空间缓冲区
- 应用程序从用户空间缓冲区读取数据
MMap 文件映射流程
应用程序 -> 直接访问虚拟内存 -> 页表映射 -> 物理内存/磁盘
使用 mmap 后:
- 文件被映射到进程的虚拟地址空间
- 访问映射区域时触发缺页中断
- 操作系统自动从磁盘加载数据页到物理内存
- 应用程序直接读写内存,无需额外拷贝
虚拟内存与页面调度
mmap 依赖于操作系统的虚拟内存机制:
- 虚拟地址空间:应用程序看到的是连续的虚拟地址
- 物理内存页:实际数据存储在物理内存页中
- 页表映射:虚拟地址通过页表映射到物理地址
- 按需加载:只有访问时才加载对应页面(Lazy Loading)
MMap 的优势与劣势
优势
1. 性能优势
- 减少数据拷贝次数(零拷贝技术)
- 随机访问效率高,无需频繁 seek
- 大文件处理时节省内存(不需要一次性加载)
2. 编程便利性
- 像操作数组一样操作文件
- 无需手动管理缓冲区
- 多进程可以共享内存映射
3. 系统优化
- 操作系统自动管理页面换入换出
- 利用文件系统的缓存机制
- 支持写时复制(Copy-on-Write)
劣势
1. 内存压力
- 映射大文件会占用虚拟地址空间
- 32位系统地址空间有限
- 可能触发频繁的页面换入换出
2. 使用限制
- 不适合小文件(映射开销大于直接读写)
- 顺序读取时可能不如流式读取高效
- 映射失败时错误处理复杂
3. 并发问题
- 多进程写入需要额外的同步机制
- 文件大小变化时需要重新映射
- 内存映射区域的错误可能导致崩溃
iOS 中 MMap 的应用场景
1. 大文件读取
处理大型日志文件、数据库文件时,使用 mmap 可以避免将整个文件加载到内存:
// 传统方式(不推荐大文件)
let data = try Data(contentsOf: fileURL) // 一次性加载全部内容
// mmap 方式(推荐)
let data = try Data(contentsOf: fileURL, options: .alwaysMapped)
2. 进程间通信(IPC)
通过共享内存映射实现高效的进程间通信:
- App 和 Extension 之间共享数据
- 多进程架构中的数据共享
- 插件系统的数据交换
3. 数据持久化
许多高性能数据库使用 mmap 来实现数据持久化:
- MMKV(微信开源的高性能键值存储)
- LMDB(Lightning Memory-Mapped Database)
- Realm(部分场景使用 mmap)
4. 图片解码优化
在图片加载和解码时,使用 mmap 可以减少内存峰值:
// 使用 mmap 映射图片文件
if let data = try? Data(contentsOf: imageURL, options: .mappedIfSafe) {
let image = UIImage(data: data)
}
5. 日志系统
高性能日志库通常使用 mmap 来实现异步写入:
- 减少日志写入对主线程的影响
- 崩溃时日志不丢失
- 支持大量日志快速写入
MMap 的基本使用
C 语言 API
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// 打开文件
int fd = open("/path/to/file", O_RDWR);
if (fd == -1) {
perror("open");
return;
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return;
}
// 内存映射
void *addr = mmap(NULL, // 让系统选择映射地址
sb.st_size, // 映射大小
PROT_READ | PROT_WRITE, // 读写权限
MAP_SHARED, // 共享映射
fd, // 文件描述符
0); // 文件偏移量
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return;
}
// 使用映射区域
char *data = (char *)addr;
data[0] = 'H';
data[1] = 'e';
data[2] = 'l';
data[3] = 'l';
data[4] = 'o';
// 同步到磁盘(可选)
msync(addr, sb.st_size, MS_SYNC);
// 解除映射
munmap(addr, sb.st_size);
// 关闭文件
close(fd); #import <Foundation/Foundation.h>
#import <sys/mman.h>
#import <sys/stat.h>
#import <fcntl.h>
@interface MMapHelper : NSObject
+ (NSData *)mapFileAtPath:(NSString *)path error:(NSError **)error;
@end
@implementation MMapHelper
+ (NSData *)mapFileAtPath:(NSString *)path error:(NSError **)error {
const char *filePath = [path UTF8String];
// 打开文件
int fd = open(filePath, O_RDONLY);
if (fd == -1) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
return nil;
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
close(fd);
return nil;
}
// 内存映射
void *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd); // 映射后可以关闭文件描述符
if (addr == MAP_FAILED) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
return nil;
}
// 包装为 NSData
return [NSData dataWithBytesNoCopy:addr
length:sb.st_size
freeWhenDone:YES];
}
@end
// 使用示例
NSError *error;
NSData *data = [MMapHelper mapFileAtPath:@"/path/to/file" error:&error];
if (data) {
// 直接访问数据
const char *bytes = data.bytes;
NSLog(@"First byte: %c", bytes[0]);
} import Foundation
class MMapHelper {
/// 使用 mmap 映射文件
static func mapFile(at url: URL) throws -> Data {
let fd = open(url.path, O_RDONLY)
guard fd >= 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
defer { close(fd) }
// 获取文件大小
var sb = stat()
guard fstat(fd, &sb) == 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
let size = Int(sb.st_size)
// 内存映射
let addr = mmap(nil, size, PROT_READ, MAP_PRIVATE, fd, 0)
guard addr != MAP_FAILED else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
// 包装为 Data(自动管理内存)
return Data(bytesNoCopy: addr!, count: size, deallocator: .custom { ptr, length in
munmap(ptr, length)
})
}
/// 使用系统 API(推荐)
static func mapFileUsingFoundation(at url: URL) throws -> Data {
return try Data(contentsOf: url, options: .alwaysMapped)
}
}
// 使用示例
do {
let fileURL = URL(fileURLWithPath: "/path/to/file")
let data = try MMapHelper.mapFile(at: fileURL)
// 访问数据
data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
if let baseAddress = ptr.baseAddress {
let firstByte = baseAddress.load(as: UInt8.self)
print("First byte: \(firstByte)")
}
}
} catch {
print("Error: \(error)")
} 高级用法:共享内存
使用 mmap 实现进程间通信(IPC):
import Foundation
class SharedMemory {
private let name: String
private var fileDescriptor: Int32 = -1
private var mappedAddress: UnsafeMutableRawPointer?
private let size: Int
init(name: String, size: Int) {
self.name = name
self.size = size
}
/// 创建共享内存
func create() throws {
// 创建共享内存对象
fileDescriptor = shm_open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR)
guard fileDescriptor >= 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
// 设置共享内存大小
guard ftruncate(fileDescriptor, off_t(size)) == 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
// 映射到进程地址空间
mappedAddress = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_SHARED, fileDescriptor, 0)
guard mappedAddress != MAP_FAILED else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
}
/// 打开已存在的共享内存
func open() throws {
fileDescriptor = shm_open(name, O_RDWR, 0)
guard fileDescriptor >= 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
mappedAddress = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_SHARED, fileDescriptor, 0)
guard mappedAddress != MAP_FAILED else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
}
/// 写入数据
func write(data: Data) {
guard let address = mappedAddress else { return }
data.withUnsafeBytes { ptr in
memcpy(address, ptr.baseAddress, min(data.count, size))
}
}
/// 读取数据
func read(length: Int) -> Data? {
guard let address = mappedAddress else { return nil }
return Data(bytes: address, count: min(length, size))
}
/// 清理资源
func close() {
if let address = mappedAddress {
munmap(address, size)
mappedAddress = nil
}
if fileDescriptor >= 0 {
Darwin.close(fileDescriptor)
fileDescriptor = -1
}
}
/// 删除共享内存
func unlink() {
shm_unlink(name)
}
deinit {
close()
}
}
// 进程 A:创建并写入
let sharedMem = SharedMemory(name: "/my_shared_memory", size: 1024)
try? sharedMem.create()
let message = "Hello from Process A".data(using: .utf8)!
sharedMem.write(data: message)
// 进程 B:读取
let sharedMem2 = SharedMemory(name: "/my_shared_memory", size: 1024)
try? sharedMem2.open()
if let data = sharedMem2.read(length: 1024),
let message = String(data: data, encoding: .utf8) {
print("Received: \(message)")
} #import <Foundation/Foundation.h>
#import <sys/mman.h>
#import <fcntl.h>
@interface SharedMemory : NSObject
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) size_t size;
- (instancetype)initWithName:(NSString *)name size:(size_t)size;
- (BOOL)create:(NSError **)error;
- (BOOL)open:(NSError **)error;
- (void)writeData:(NSData *)data;
- (NSData *)readDataWithLength:(size_t)length;
- (void)close;
- (void)unlink;
@end
@implementation SharedMemory {
int _fd;
void *_addr;
}
- (instancetype)initWithName:(NSString *)name size:(size_t)size {
if (self = [super init]) {
_name = name;
_size = size;
_fd = -1;
_addr = NULL;
}
return self;
}
- (BOOL)create:(NSError **)error {
_fd = shm_open([_name UTF8String], O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (_fd < 0) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
return NO;
}
if (ftruncate(_fd, _size) != 0) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
close(_fd);
return NO;
}
_addr = mmap(NULL, _size, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, 0);
if (_addr == MAP_FAILED) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
close(_fd);
return NO;
}
return YES;
}
- (BOOL)open:(NSError **)error {
_fd = shm_open([_name UTF8String], O_RDWR, 0);
if (_fd < 0) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
return NO;
}
_addr = mmap(NULL, _size, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, 0);
if (_addr == MAP_FAILED) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errno
userInfo:nil];
}
close(_fd);
return NO;
}
return YES;
}
- (void)writeData:(NSData *)data {
if (_addr) {
memcpy(_addr, data.bytes, MIN(data.length, _size));
}
}
- (NSData *)readDataWithLength:(size_t)length {
if (_addr) {
return [NSData dataWithBytes:_addr length:MIN(length, _size)];
}
return nil;
}
- (void)close {
if (_addr) {
munmap(_addr, _size);
_addr = NULL;
}
if (_fd >= 0) {
close(_fd);
_fd = -1;
}
}
- (void)unlink {
shm_unlink([_name UTF8String]);
}
- (void)dealloc {
[self close];
}
@end 实战案例:高性能日志系统
使用 mmap 实现一个简单的日志系统:
import Foundation
class MMapLogger {
private let fileURL: URL
private var fileHandle: FileHandle?
private var mappedData: UnsafeMutableRawPointer?
private var mappedSize: Int = 0
private var writeOffset: Int = 0
private let maxFileSize: Int
private let queue = DispatchQueue(label: "com.logger.mmap")
init(logPath: String, maxSize: Int = 10 * 1024 * 1024) { // 默认 10MB
self.fileURL = URL(fileURLWithPath: logPath)
self.maxFileSize = maxSize
setupLogFile()
}
private func setupLogFile() {
// 创建文件(如果不存在)
if !FileManager.default.fileExists(atPath: fileURL.path) {
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
}
do {
// 打开文件
fileHandle = try FileHandle(forUpdating: fileURL)
// 扩展文件到指定大小
try fileHandle?.truncate(atOffset: UInt64(maxFileSize))
// 映射文件
let fd = fileHandle!.fileDescriptor
mappedData = mmap(nil, maxFileSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
if mappedData == MAP_FAILED {
print("mmap failed")
mappedData = nil
} else {
mappedSize = maxFileSize
}
} catch {
print("Setup log file error: \(error)")
}
}
func log(_ message: String) {
queue.async { [weak self] in
guard let self = self,
let data = mappedData else { return }
let timestamp = Date().timeIntervalSince1970
let logEntry = "[\(timestamp)] \(message)\n"
guard let logData = logEntry.data(using: .utf8) else { return }
// 检查是否需要循环覆盖
if writeOffset + logData.count > mappedSize {
writeOffset = 0 // 循环写入
}
// 写入日志
logData.withUnsafeBytes { ptr in
memcpy(data.advanced(by: writeOffset), ptr.baseAddress, logData.count)
}
writeOffset += logData.count
// 同步到磁盘(可选,影响性能)
// msync(data, mappedSize, MS_ASYNC)
}
}
func flush() {
if let data = mappedData {
msync(data, mappedSize, MS_SYNC)
}
}
deinit {
if let data = mappedData {
munmap(data, mappedSize)
}
try? fileHandle?.close()
}
}
// 使用示例
let logger = MMapLogger(logPath: "/tmp/app.log")
logger.log("Application started")
logger.log("User logged in")
logger.log("Data synchronized")
logger.flush() // 确保写入磁盘
性能对比测试
对比传统文件 I/O 和 mmap 的性能:
import Foundation
class PerformanceTest {
static func testTraditionalIO(fileURL: URL, iterations: Int) -> TimeInterval {
let start = Date()
for i in 0..<iterations {
let data = "Log entry \(i)\n".data(using: .utf8)!
try? data.append(to: fileURL)
}
return Date().timeIntervalSince(start)
}
static func testMMap(fileURL: URL, iterations: Int) -> TimeInterval {
let start = Date()
let logger = MMapLogger(logPath: fileURL.path)
for i in 0..<iterations {
logger.log("Log entry \(i)")
}
logger.flush()
return Date().timeIntervalSince(start)
}
}
// 运行测试
let traditionalTime = PerformanceTest.testTraditionalIO(
fileURL: URL(fileURLWithPath: "/tmp/traditional.log"),
iterations: 10000
)
let mmapTime = PerformanceTest.testMMap(
fileURL: URL(fileURLWithPath: "/tmp/mmap.log"),
iterations: 10000
)
print("Traditional I/O: \(traditionalTime)s")
print("MMap: \(mmapTime)s")
print("MMap is \(traditionalTime / mmapTime)x faster")
注意事项与最佳实践
1. 文件大小变化
当文件大小需要改变时,必须重新映射:
func resizeMapping(newSize: Int) throws {
// 解除旧映射
if let oldAddr = mappedData {
munmap(oldAddr, mappedSize)
}
// 调整文件大小
try fileHandle?.truncate(atOffset: UInt64(newSize))
// 重新映射
let fd = fileHandle!.fileDescriptor
mappedData = mmap(nil, newSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
guard mappedData != MAP_FAILED else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
mappedSize = newSize
}
2. 错误处理
访问映射区域时可能产生信号(如 SIGSEGV、SIGBUS):
// 设置信号处理器(Objective-C 中更常见)
signal(SIGBUS) { signal in
print("Bus error: attempting to access memory outside mapped region")
}
3. 内存建议(madvise)
告诉操作系统如何使用映射内存:
// 顺序访问
madvise(mappedData, mappedSize, MADV_SEQUENTIAL)
// 随机访问
madvise(mappedData, mappedSize, MADV_RANDOM)
// 不再需要
madvise(mappedData, mappedSize, MADV_DONTNEED)
// 预加载
madvise(mappedData, mappedSize, MADV_WILLNEED)
4. 线程安全
多线程访问映射区域时需要同步:
class ThreadSafeMMap {
private let lock = NSLock()
private var mappedData: UnsafeMutableRawPointer?
func write(data: Data, at offset: Int) {
lock.lock()
defer { lock.unlock() }
guard let addr = mappedData else { return }
data.withUnsafeBytes { ptr in
memcpy(addr.advanced(by: offset), ptr.baseAddress, data.count)
}
}
}
5. 内存压力处理
在内存紧张时释放映射:
class MMapCache {
private var mappings: [String: UnsafeMutableRawPointer] = [:]
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleMemoryWarning),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
@objc private func handleMemoryWarning() {
// 释放部分映射
mappings.forEach { _, addr in
munmap(addr, mappedSize)
}
mappings.removeAll()
}
}
MMap 与其他技术对比
MMap vs 传统文件 I/O
| 特性 | MMap | 传统 I/O |
|---|---|---|
| 数据拷贝 | 无需用户态内核态拷贝 | 需要多次拷贝 |
| 随机访问 | 高效 | 需要 seek |
| 顺序访问 | 较好 | 很好 |
| 小文件 | 映射开销大 | 高效 |
| 大文件 | 节省内存 | 需要大量内存 |
| 编程复杂度 | 简单(像数组) | 需要缓冲区管理 |
MMap vs NSCache
| 特性 | MMap | NSCache |
|---|---|---|
| 持久化 | 自动持久化 | 仅内存 |
| 容量限制 | 文件大小 | 内存大小 |
| 崩溃恢复 | 数据不丢失 | 数据丢失 |
| 访问速度 | 略慢(可能缺页) | 很快 |
| 进程共享 | 支持 | 不支持 |
MMap vs Core Data
| 特性 | MMap | Core Data |
|---|---|---|
| 查询能力 | 无 | 强大的查询 |
| 关系管理 | 无 | 支持关系 |
| 原始性能 | 更快 | 稍慢 |
| 学习曲线 | 低 | 高 |
| 适用场景 | 简单数据/日志 | 复杂数据模型 |
实际应用:MMKV 简介
MMKV 是微信开源的基于 mmap 的高性能键值存储框架:
import MMKV
// 初始化
MMKV.initialize(rootDir: nil)
// 获取默认实例
let mmkv = MMKV.default()
// 写入数据
mmkv?.set("value", forKey: "key")
mmkv?.set(123, forKey: "number")
mmkv?.set(true, forKey: "bool")
// 读取数据
let value = mmkv?.string(forKey: "key")
let number = mmkv?.int32(forKey: "number")
let bool = mmkv?.bool(forKey: "bool")
// MMKV 的优势
// 1. 基于 mmap,性能极高
// 2. 进程安全(多进程访问)
// 3. 数据加密支持
// 4. 增量更新,不会阻塞
// 5. 自动处理文件损坏
总结
MMap 是一项强大的技术,在以下场景中特别有用:
- 大文件处理:避免一次性加载整个文件到内存
- 高性能日志:异步写入,崩溃不丢失
- 进程间通信:共享内存方式实现 IPC
- 数据库实现:LMDB、MMKV 等都基于 mmap
- 图片解码:减少内存峰值
但也需要注意:
- 小文件不适合使用 mmap(映射开销大)
- 需要处理好内存映射的生命周期
- 多进程写入需要额外的同步机制
- 文件大小变化时需要重新映射
掌握 mmap 技术,可以显著提升 iOS 应用在文件操作、数据持久化和进程间通信等方面的性能。