iOS内存映射mmap详解

进程和线程?

因为后面的知识涉及到进程,所以我们先来简单了解一下进程和线程。下面的内容摘自iOS-线程&&进程的深入理解

进程基本概念

  • 进程就是一个正在运行的一个应用程序
  • 每一个进度都是独立的,每一个进程均在专门且手保护的内存空间内
  • iOS是怎么管理自己的内存的,见博客:iOS — 内存分配与分区
  • 在Linux系统中,想要新开启一个进程是一件非常简单的事情只需要一句话:fork(),在fork()之后就会包含两个进程,此时可以根据返回的PID来判断是子进程还是父进程
  • iOS中是一个非常封闭的系统,每一个App(一个进程)都有自己独特的内存和磁盘空间,别的App(进程)是不允许访问的(越狱不在讨论范围)

常规文件操作

常规文件操作(read/write)有那几个重要步骤:

  1. 进程发起读文件请求
  2. 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode
  3. inode在address_space上查找要请求的文件页是否已经缓存在内核页高速缓冲中。如果存在,则直接返回这片文件页的内容
  4. 如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到内核页高速缓冲。之后再次发起读页面过程,进而将内核页高速缓冲中的数据发给用户进程

需要注意的几点:

  1. 常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。由于页缓存处在内核空间,不能被用户进程直接寻址,所以需要将页缓存中数据页再次拷贝到内存对应的用户空间中
  2. read/write是系统调用很耗时,如下图,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,实际上完成了两次数据拷贝
  3. 如果两个进程都对磁盘中的一个文件内容进行访问,那么这个内容在物理内存中有三份:进程A的地址空间 + 进程B的地址空间 + 内核页高速缓冲空间
  4. 写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝

关于内核有疑问不懂的可以参考我的这篇文章Linux 内核剖析,想了解更多linux文件系统相关知识的可以参考这篇文章从内核文件系统看文件读写过程

下面这个图来自linux内存映射mmap原理分析,很形象的描述了整个进程访问磁盘中文件的过程。

image

mmap内存映射

同样的我会放一个mmap映射过程图,以求让大家对mmap映射有更直观理解,图片也还是来自linux内存映射mmap原理分析

image

在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。

既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程了。

mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,如图1中过程2所示。这个过程与内存映射无关。

前面讲过,建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。这个过程与内存映射无关。

如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。这个过程也与内存映射无关。

mmap内存映射的实现过程,总的来说可以分为三个阶段:

  1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
  2. 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

如果想了解每个阶段更多详细内容,请看这里认真分析mmap:是什么 为什么 怎么用

mmap使用分析

这一部分来自苹果官方开发文档Mapping Files Into Memory

适合的场景

  • 您有一个很大的文件,其内容您想要随机访问一个或多个时间
  • 您有一个小文件,它的内容您想要立即读入内存并经常访问。这种技术最适合那些大小不超过几个虚拟内存页的文件。(页是地址空间的最小单位,虚拟页和物理页的大小是一样的,通常为4KB。)
  • 您需要在内存中缓存文件的特定部分。文件映射消除了缓存数据的需要,这使得系统磁盘缓存中的其他数据空间更大

当随机访问一个非常大的文件时,通常最好只映射文件的一小部分。映射大文件的问题是文件会消耗活动内存。如果文件足够大,系统可能会被迫将其他部分的内存分页以加载文件。将多个文件映射到内存中会使这个问题更加复杂。

不适合的场景

  • 您希望从开始到结束的顺序从头到尾读取一个文件
  • 这个文件有几百兆字节或者更大。将大文件映射到内存中会快速地填充内存,并可能导致分页,这将抵消首先映射文件的好处。对于大型顺序读取操作,禁用磁盘缓存并将文件读入一个小内存缓冲区
  • 该文件大于可用的连续虚拟内存地址空间。对于64位应用程序来说,这不是什么问题,但是对于32位应用程序来说,这是一个问题
  • 该文件位于可移动驱动器上
  • 该文件位于网络驱动器上

代码实现

这段代码实现比较简单,源自Mapping Files Into Memory

import Foundation
import Darwin

func ProcessFile(inPathName: String) {
    
    var dataLength: size_t?
    var dataPtr: UnsafeMutableRawPointer?
    var start: UnsafeMutableRawPointer?
    
    if mapFile(inPathName: inPathName, outDataPtr: &dataPtr, outDataLength: &dataLength) {
        start = dataPtr
        dataPtr = dataPtr! + 3
        memcpy(dataPtr, "CCCC", 4)
        // Unmap files:
        munmap(start, 7)
    }
    
    
    
}


func mapFile(inPathName: String, outDataPtr: inout UnsafeMutableRawPointer?, outDataLength: inout size_t?) -> Bool {
    
    var fileDescriptor: Int32
    var statInfo = stat()
    
    outDataPtr = nil
    outDataLength = 0
    
    // Open the file
    fileDescriptor = open(inPathName, O_RDWR, 0)
    
    if fileDescriptor < 0 {
        return false
    }
    
    // We now know the file exists. Retrieve the file size.
    if fstat(fileDescriptor, &statInfo) != 0 {
        return false
    }else {
        ftruncate(fileDescriptor, statInfo.st_size+4)
        fsync(fileDescriptor)
        outDataPtr = mmap(nil, Int(statInfo.st_size+4), PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, fileDescriptor, 0)
        if outDataPtr == MAP_FAILED {
            return false
        }else{
            outDataLength = size_t(statInfo.st_size)
        }
    }
    
    // Now close the file. The kernel doesn’t use our file descriptor.
    close(fileDescriptor)
    
    return true
    
    
}
    let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
        let str = "AAA"
        let filePath = "\(path ?? "")/text.txt"
        try? str.write(toFile: filePath, atomically: true, encoding: .utf8)
        ProcessFile(inPathName: filePath)
        let result = try? String(contentsOfFile: filePath, encoding: .utf8)
        print(result)

在iOS的应用

具体在项目中怎么去使用mmap呢?我推荐你看看以下的文章和代码:

MMKV--基于 mmap 的 iOS 高性能通用 key-value 组件
iOS图片加载速度极限优化—FastImageCache解析
FastImageCache

之后我也会在一个开源项目中使用mmap,到时候会更加详细的讲实现的细节,老铁来波关注吧。

最后

说实话如果没有一些操作系统相关知识,很难完全弄明白整个过程。因为涉及到进程,用户空间,内存空间,逻辑地址,物理地址,系统调用,中断,磁盘I/O等一系列的知识。我曾尝试画图以便能说的更清楚,但最后还是放弃了,因为只会越说越复杂。如果有什么疑问可以留言,能回答的都会尽量回答。

参考文章:

iOS-线程&&进程的深入理解
Mapping Files Into Memory
linux内存映射mmap原理分析
mmap实例及原理分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容