進程和線程?
因為后面的知識涉及到進程,所以我們先來簡單了解一下進程和線程。下面的內容摘自iOS-線程&&進程的深入理解
進程基本概念
- 進程就是一個正在運行的一個應用程序
- 每一個進度都是獨立的,每一個進程均在專門且手保護的內存空間內
- iOS是怎么管理自己的內存的,見博客:iOS — 內存分配與分區(qū)
- 在Linux系統(tǒng)中,想要新開啟一個進程是一件非常簡單的事情只需要一句話:fork(),在fork()之后就會包含兩個進程,此時可以根據返回的PID來判斷是子進程還是父進程
- iOS中是一個非常封閉的系統(tǒng),每一個App(一個進程)都有自己獨特的內存和磁盤空間,別的App(進程)是不允許訪問的(越獄不在討論范圍)
常規(guī)文件操作
常規(guī)文件操作(read/write)有那幾個重要步驟:
- 進程發(fā)起讀文件請求
- 內核通過查找進程文件符表,定位到內核已打開文件集上的文件信息,從而找到此文件的inode
- inode在address_space上查找要請求的文件頁是否已經緩存在內核頁高速緩沖中。如果存在,則直接返回這片文件頁的內容
- 如果不存在,則通過inode定位到文件磁盤地址,將數(shù)據從磁盤復制到內核頁高速緩沖。之后再次發(fā)起讀頁面過程,進而將內核頁高速緩沖中的數(shù)據發(fā)給用戶進程
需要注意的幾點:
- 常規(guī)文件操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。由于頁緩存處在內核空間,不能被用戶進程直接尋址,所以需要將頁緩存中數(shù)據頁再次拷貝到內存對應的用戶空間中
- read/write是系統(tǒng)調用很耗時,如下圖,它首先將文件內容從硬盤拷貝到內核空間的一個緩沖區(qū),然后再將這些數(shù)據拷貝到用戶空間,實際上完成了兩次數(shù)據拷貝
- 如果兩個進程都對磁盤中的一個文件內容進行訪問,那么這個內容在物理內存中有三份:進程A的地址空間 + 進程B的地址空間 + 內核頁高速緩沖空間
- 寫操作也是一樣,待寫入的buffer在內核空間不能直接訪問,必須要先拷貝至內核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據拷貝
關于內核有疑問不懂的可以參考我的這篇文章Linux 內核剖析,想了解更多l(xiāng)inux文件系統(tǒng)相關知識的可以參考這篇文章從內核文件系統(tǒng)看文件讀寫過程。
下面這個圖來自linux內存映射mmap原理分析,很形象的描述了整個進程訪問磁盤中文件的過程。
mmap內存映射
同樣的我會放一個mmap映射過程圖,以求讓大家對mmap映射有更直觀理解,圖片也還是來自linux內存映射mmap原理分析
在內存映射的過程中,并沒有實際的數(shù)據拷貝,文件沒有被載入內存,只是邏輯上被放入了內存,具體到代碼,就是建立并初始化了相關的數(shù)據結構(struct address_space),這個過程有系統(tǒng)調用mmap()實現(xiàn),所以建立內存映射的效率很高。
既然建立內存映射沒有進行實際的數(shù)據拷貝,那么進程又怎么能最終直接通過內存操作訪問到硬盤上的文件呢?那就要看內存映射之后的幾個相關的過程了。
mmap()會返回一個指針ptr,它指向進程邏輯地址空間中的一個地址,這樣以后,進程無需再調用read或write對文件進行讀寫,而只需要通過ptr就能夠操作文件。但是ptr所指向的是一個邏輯地址,要操作其中的數(shù)據,必須通過MMU將邏輯地址轉換成物理地址,如圖1中過程2所示。這個過程與內存映射無關。
前面講過,建立內存映射并沒有實際拷貝數(shù)據,這時,MMU在地址映射表中是無法找到與ptr相對應的物理地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函數(shù)會在swap中尋找相對應的頁面,如果找不到(也就是該文件從來沒有被讀入內存的情況),則會通過mmap()建立的映射關系,從硬盤上將文件讀取到物理內存中,如圖1中過程3所示。這個過程與內存映射無關。
如果在拷貝數(shù)據時,發(fā)現(xiàn)物理內存不夠用,則會通過虛擬內存機制(swap)將暫時不用的物理頁面交換到硬盤上,如圖1中過程4所示。這個過程也與內存映射無關。
mmap內存映射的實現(xiàn)過程,總的來說可以分為三個階段:
- 進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
- 調用內核空間的系統(tǒng)調用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關系
- 進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內容到物理內存(主存)的拷貝
如果想了解每個階段更多詳細內容,請看這里認真分析mmap:是什么 為什么 怎么用
mmap使用分析
這一部分來自蘋果官方開發(fā)文檔Mapping Files Into Memory
適合的場景
- 您有一個很大的文件,其內容您想要隨機訪問一個或多個時間
- 您有一個小文件,它的內容您想要立即讀入內存并經常訪問。這種技術最適合那些大小不超過幾個虛擬內存頁的文件。(頁是地址空間的最小單位,虛擬頁和物理頁的大小是一樣的,通常為4KB。)
- 您需要在內存中緩存文件的特定部分。文件映射消除了緩存數(shù)據的需要,這使得系統(tǒng)磁盤緩存中的其他數(shù)據空間更大
當隨機訪問一個非常大的文件時,通常最好只映射文件的一小部分。映射大文件的問題是文件會消耗活動內存。如果文件足夠大,系統(tǒng)可能會被迫將其他部分的內存分頁以加載文件。將多個文件映射到內存中會使這個問題更加復雜。
不適合的場景
- 您希望從開始到結束的順序從頭到尾讀取一個文件
- 這個文件有幾百兆字節(jié)或者更大。將大文件映射到內存中會快速地填充內存,并可能導致分頁,這將抵消首先映射文件的好處。對于大型順序讀取操作,禁用磁盤緩存并將文件讀入一個小內存緩沖區(qū)
- 該文件大于可用的連續(xù)虛擬內存地址空間。對于64位應用程序來說,這不是什么問題,但是對于32位應用程序來說,這是一個問題
- 該文件位于可移動驅動器上
- 該文件位于網絡驅動器上
代碼實現(xiàn)
這段代碼實現(xià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的應用
具體在項目中怎么去使用mmap呢?我推薦你看看以下的文章和代碼:
MMKV--基于 mmap 的 iOS 高性能通用 key-value 組件
iOS圖片加載速度極限優(yōu)化—FastImageCache解析
FastImageCache
之后我也會在一個開源項目中使用mmap,到時候會更加詳細的講實現(xiàn)的細節(jié),老鐵來波關注吧。
最后
說實話如果沒有一些操作系統(tǒng)相關知識,很難完全弄明白整個過程。因為涉及到進程,用戶空間,內存空間,邏輯地址,物理地址,系統(tǒng)調用,中斷,磁盤I/O等一系列的知識。我曾嘗試畫圖以便能說的更清楚,但最后還是放棄了,因為只會越說越復雜。如果有什么疑問可以留言,能回答的都會盡量回答。
參考文章:
iOS-線程&&進程的深入理解
Mapping Files Into Memory
linux內存映射mmap原理分析
mmap實例及原理分析