Skip to content

iOS 中的 MMap 内存映射技术详解

轩辕十四
Published date:

什么是 MMap

MMap(Memory Mapping)是一种内存映射技术,它允许将文件或其他对象映射到进程的地址空间。在 iOS 开发中,mmap 是一个强大的系统调用,能够将磁盘文件的内容直接映射到内存地址空间,使得对文件的读写操作可以像访问内存一样简单高效。

与传统的文件 I/O(通过 read()/write() 系统调用)不同,mmap 通过虚拟内存机制,让应用程序可以像访问数组一样访问文件内容,操作系统会自动处理磁盘和内存之间的数据传输。

核心特点

MMap 的工作原理

传统文件 I/O 流程

应用程序 -> read() -> 内核缓冲区 -> 用户空间缓冲区 -> 应用程序
磁盘    -> DMA    -> 内核缓冲区

这个过程涉及多次数据拷贝:

  1. 磁盘数据通过 DMA 拷贝到内核缓冲区
  2. 内核缓冲区数据拷贝到用户空间缓冲区
  3. 应用程序从用户空间缓冲区读取数据

MMap 文件映射流程

应用程序 -> 直接访问虚拟内存 -> 页表映射 -> 物理内存/磁盘

使用 mmap 后:

  1. 文件被映射到进程的虚拟地址空间
  2. 访问映射区域时触发缺页中断
  3. 操作系统自动从磁盘加载数据页到物理内存
  4. 应用程序直接读写内存,无需额外拷贝

虚拟内存与页面调度

mmap 依赖于操作系统的虚拟内存机制:

MMap 的优势与劣势

优势

1. 性能优势

2. 编程便利性

3. 系统优化

劣势

1. 内存压力

2. 使用限制

3. 并发问题

iOS 中 MMap 的应用场景

1. 大文件读取

处理大型日志文件、数据库文件时,使用 mmap 可以避免将整个文件加载到内存:

// 传统方式(不推荐大文件)
let data = try Data(contentsOf: fileURL) // 一次性加载全部内容

// mmap 方式(推荐)
let data = try Data(contentsOf: fileURL, options: .alwaysMapped)

2. 进程间通信(IPC)

通过共享内存映射实现高效的进程间通信:

3. 数据持久化

许多高性能数据库使用 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

特性MMapNSCache
持久化自动持久化仅内存
容量限制文件大小内存大小
崩溃恢复数据不丢失数据丢失
访问速度略慢(可能缺页)很快
进程共享支持不支持

MMap vs Core Data

特性MMapCore 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 是一项强大的技术,在以下场景中特别有用:

  1. 大文件处理:避免一次性加载整个文件到内存
  2. 高性能日志:异步写入,崩溃不丢失
  3. 进程间通信:共享内存方式实现 IPC
  4. 数据库实现:LMDB、MMKV 等都基于 mmap
  5. 图片解码:减少内存峰值

但也需要注意:

掌握 mmap 技术,可以显著提升 iOS 应用在文件操作、数据持久化和进程间通信等方面的性能。

Previous
iOS 队列的两种实现:循环数组 vs 链表
Next
数据结构:循环数组详解