iOS内存映射mmap详解

作者: 小凉介 | 来源:发表于2018-06-05 12:21 被阅读168次

    进程和线程?

    因为后面的知识涉及到进程,所以我们先来简单了解一下进程和线程。下面的内容摘自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)
            mapFile.processFile(pathName: 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实例及原理分析

    相关文章

      网友评论

        本文标题:iOS内存映射mmap详解

        本文链接:https://www.haomeiwen.com/subject/zwpysftx.html