iOS內(nèi)存映射mmap詳解

進(jìn)程和線程?

因?yàn)楹竺娴闹R(shí)涉及到進(jìn)程,所以我們先來(lái)簡(jiǎn)單了解一下進(jìn)程和線程。下面的內(nèi)容摘自iOS-線程&&進(jìn)程的深入理解

進(jìn)程基本概念

  • 進(jìn)程就是一個(gè)正在運(yùn)行的一個(gè)應(yīng)用程序
  • 每一個(gè)進(jìn)度都是獨(dú)立的,每一個(gè)進(jìn)程均在專門且手保護(hù)的內(nèi)存空間內(nèi)
  • iOS是怎么管理自己的內(nèi)存的,見博客:iOS — 內(nèi)存分配與分區(qū)
  • 在Linux系統(tǒng)中,想要新開啟一個(gè)進(jìn)程是一件非常簡(jiǎn)單的事情只需要一句話:fork(),在fork()之后就會(huì)包含兩個(gè)進(jìn)程,此時(shí)可以根據(jù)返回的PID來(lái)判斷是子進(jìn)程還是父進(jìn)程
  • iOS中是一個(gè)非常封閉的系統(tǒng),每一個(gè)App(一個(gè)進(jìn)程)都有自己獨(dú)特的內(nèi)存和磁盤空間,別的App(進(jìn)程)是不允許訪問的(越獄不在討論范圍)

常規(guī)文件操作

常規(guī)文件操作(read/write)有那幾個(gè)重要步驟:

  1. 進(jìn)程發(fā)起讀文件請(qǐng)求
  2. 內(nèi)核通過查找進(jìn)程文件符表,定位到內(nèi)核已打開文件集上的文件信息,從而找到此文件的inode
  3. inode在address_space上查找要請(qǐng)求的文件頁(yè)是否已經(jīng)緩存在內(nèi)核頁(yè)高速緩沖中。如果存在,則直接返回這片文件頁(yè)的內(nèi)容
  4. 如果不存在,則通過inode定位到文件磁盤地址,將數(shù)據(jù)從磁盤復(fù)制到內(nèi)核頁(yè)高速緩沖。之后再次發(fā)起讀頁(yè)面過程,進(jìn)而將內(nèi)核頁(yè)高速緩沖中的數(shù)據(jù)發(fā)給用戶進(jìn)程

需要注意的幾點(diǎn):

  1. 常規(guī)文件操作為了提高讀寫效率和保護(hù)磁盤,使用了頁(yè)緩存機(jī)制。由于頁(yè)緩存處在內(nèi)核空間,不能被用戶進(jìn)程直接尋址,所以需要將頁(yè)緩存中數(shù)據(jù)頁(yè)再次拷貝到內(nèi)存對(duì)應(yīng)的用戶空間中
  2. read/write是系統(tǒng)調(diào)用很耗時(shí),如下圖,它首先將文件內(nèi)容從硬盤拷貝到內(nèi)核空間的一個(gè)緩沖區(qū),然后再將這些數(shù)據(jù)拷貝到用戶空間,實(shí)際上完成了兩次數(shù)據(jù)拷貝
  3. 如果兩個(gè)進(jìn)程都對(duì)磁盤中的一個(gè)文件內(nèi)容進(jìn)行訪問,那么這個(gè)內(nèi)容在物理內(nèi)存中有三份:進(jìn)程A的地址空間 + 進(jìn)程B的地址空間 + 內(nèi)核頁(yè)高速緩沖空間
  4. 寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對(duì)應(yīng)的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝

關(guān)于內(nèi)核有疑問不懂的可以參考我的這篇文章Linux 內(nèi)核剖析,想了解更多l(xiāng)inux文件系統(tǒng)相關(guān)知識(shí)的可以參考這篇文章從內(nèi)核文件系統(tǒng)看文件讀寫過程。

下面這個(gè)圖來(lái)自linux內(nèi)存映射mmap原理分析,很形象的描述了整個(gè)進(jìn)程訪問磁盤中文件的過程。

image

mmap內(nèi)存映射

同樣的我會(huì)放一個(gè)mmap映射過程圖,以求讓大家對(duì)mmap映射有更直觀理解,圖片也還是來(lái)自linux內(nèi)存映射mmap原理分析

image

在內(nèi)存映射的過程中,并沒有實(shí)際的數(shù)據(jù)拷貝,文件沒有被載入內(nèi)存,只是邏輯上被放入了內(nèi)存,具體到代碼,就是建立并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)(struct address_space),這個(gè)過程有系統(tǒng)調(diào)用mmap()實(shí)現(xiàn),所以建立內(nèi)存映射的效率很高。

既然建立內(nèi)存映射沒有進(jìn)行實(shí)際的數(shù)據(jù)拷貝,那么進(jìn)程又怎么能最終直接通過內(nèi)存操作訪問到硬盤上的文件呢?那就要看內(nèi)存映射之后的幾個(gè)相關(guān)的過程了。

mmap()會(huì)返回一個(gè)指針ptr,它指向進(jìn)程邏輯地址空間中的一個(gè)地址,這樣以后,進(jìn)程無(wú)需再調(diào)用read或write對(duì)文件進(jìn)行讀寫,而只需要通過ptr就能夠操作文件。但是ptr所指向的是一個(gè)邏輯地址,要操作其中的數(shù)據(jù),必須通過MMU將邏輯地址轉(zhuǎn)換成物理地址,如圖1中過程2所示。這個(gè)過程與內(nèi)存映射無(wú)關(guān)。

前面講過,建立內(nèi)存映射并沒有實(shí)際拷貝數(shù)據(jù),這時(shí),MMU在地址映射表中是無(wú)法找到與ptr相對(duì)應(yīng)的物理地址的,也就是MMU失敗,將產(chǎn)生一個(gè)缺頁(yè)中斷,缺頁(yè)中斷的中斷響應(yīng)函數(shù)會(huì)在swap中尋找相對(duì)應(yīng)的頁(yè)面,如果找不到(也就是該文件從來(lái)沒有被讀入內(nèi)存的情況),則會(huì)通過mmap()建立的映射關(guān)系,從硬盤上將文件讀取到物理內(nèi)存中,如圖1中過程3所示。這個(gè)過程與內(nèi)存映射無(wú)關(guān)。

如果在拷貝數(shù)據(jù)時(shí),發(fā)現(xiàn)物理內(nèi)存不夠用,則會(huì)通過虛擬內(nèi)存機(jī)制(swap)將暫時(shí)不用的物理頁(yè)面交換到硬盤上,如圖1中過程4所示。這個(gè)過程也與內(nèi)存映射無(wú)關(guān)。

mmap內(nèi)存映射的實(shí)現(xiàn)過程,總的來(lái)說(shuō)可以分為三個(gè)階段:

  1. 進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
  2. 調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
  3. 進(jìn)程發(fā)起對(duì)這片映射空間的訪問,引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

如果想了解每個(gè)階段更多詳細(xì)內(nèi)容,請(qǐng)看這里認(rèn)真分析mmap:是什么 為什么 怎么用

mmap使用分析

這一部分來(lái)自蘋果官方開發(fā)文檔Mapping Files Into Memory

適合的場(chǎng)景

  • 您有一個(gè)很大的文件,其內(nèi)容您想要隨機(jī)訪問一個(gè)或多個(gè)時(shí)間
  • 您有一個(gè)小文件,它的內(nèi)容您想要立即讀入內(nèi)存并經(jīng)常訪問。這種技術(shù)最適合那些大小不超過幾個(gè)虛擬內(nèi)存頁(yè)的文件。(頁(yè)是地址空間的最小單位,虛擬頁(yè)和物理頁(yè)的大小是一樣的,通常為4KB。)
  • 您需要在內(nèi)存中緩存文件的特定部分。文件映射消除了緩存數(shù)據(jù)的需要,這使得系統(tǒng)磁盤緩存中的其他數(shù)據(jù)空間更大

當(dāng)隨機(jī)訪問一個(gè)非常大的文件時(shí),通常最好只映射文件的一小部分。映射大文件的問題是文件會(huì)消耗活動(dòng)內(nèi)存。如果文件足夠大,系統(tǒng)可能會(huì)被迫將其他部分的內(nèi)存分頁(yè)以加載文件。將多個(gè)文件映射到內(nèi)存中會(huì)使這個(gè)問題更加復(fù)雜。

不適合的場(chǎng)景

  • 您希望從開始到結(jié)束的順序從頭到尾讀取一個(gè)文件
  • 這個(gè)文件有幾百兆字節(jié)或者更大。將大文件映射到內(nèi)存中會(huì)快速地填充內(nèi)存,并可能導(dǎo)致分頁(yè),這將抵消首先映射文件的好處。對(duì)于大型順序讀取操作,禁用磁盤緩存并將文件讀入一個(gè)小內(nèi)存緩沖區(qū)
  • 該文件大于可用的連續(xù)虛擬內(nèi)存地址空間。對(duì)于64位應(yīng)用程序來(lái)說(shuō),這不是什么問題,但是對(duì)于32位應(yīng)用程序來(lái)說(shuō),這是一個(gè)問題
  • 該文件位于可移動(dòng)驅(qū)動(dòng)器上
  • 該文件位于網(wǎng)絡(luò)驅(qū)動(dòng)器上

代碼實(shí)現(xiàn)

這段代碼實(shí)現(xiàn)比較簡(jiǎn)單,源自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的應(yīng)用

具體在項(xiàng)目中怎么去使用mmap呢?我推薦你看看以下的文章和代碼:

MMKV--基于 mmap 的 iOS 高性能通用 key-value 組件
iOS圖片加載速度極限優(yōu)化—FastImageCache解析
FastImageCache

之后我也會(huì)在一個(gè)開源項(xiàng)目中使用mmap,到時(shí)候會(huì)更加詳細(xì)的講實(shí)現(xiàn)的細(xì)節(jié),老鐵來(lái)波關(guān)注吧。

最后

說(shuō)實(shí)話如果沒有一些操作系統(tǒng)相關(guān)知識(shí),很難完全弄明白整個(gè)過程。因?yàn)樯婕暗竭M(jìn)程,用戶空間,內(nèi)存空間,邏輯地址,物理地址,系統(tǒng)調(diào)用,中斷,磁盤I/O等一系列的知識(shí)。我曾嘗試畫圖以便能說(shuō)的更清楚,但最后還是放棄了,因?yàn)橹粫?huì)越說(shuō)越復(fù)雜。如果有什么疑問可以留言,能回答的都會(huì)盡量回答。

參考文章:

iOS-線程&&進(jìn)程的深入理解
Mapping Files Into Memory
linux內(nèi)存映射mmap原理分析
mmap實(shí)例及原理分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容