從記憶體對映mmap說開去

cjlcooper發表於2019-04-08

avatar

關於作者

E-moss,程式設計師,愛好閱讀和擼狗,主要從事iOS開發工作,公眾號:知本集。  
主要分享和編寫技術方面文章,不定期分享讀書筆記,亦可訪問“知本集”Git地址:https://github.com/knowtheroot/KnowTheRoot_iOS,歡迎提出問題和討論。
複製程式碼

文章目錄

  • 作業系統讀寫檔案流程
  • mmap記憶體對映
  • mmap的優點
  • mmap的實踐應用

什麼是記憶體對映?

所謂記憶體對映,就是將檔案的磁碟扇區對映到程式的虛擬記憶體空間的過程。

作業系統中的程式

  • 程式就是一個正在執行的應用程式
  • 每一個程式都是獨立的,並且每一個程式都在一個獨立的、受保護的空間內
  • 在Linux系統中,通常使用fork()方法來開啟一個新的程式
  • 在iOS系統中,每一個程式都有自己的記憶體和磁碟空間,其他的程式是不被允許訪問的

一、作業系統讀寫檔案流程

讀寫操作的流程

1.程式發起一個讀檔案請求;

2.核心通過查詢程式檔案符表,定位到核心已開啟的檔案集上的檔案資訊,從而找到對應檔案的inode;

3.inode在地址空間(address_space)上查詢要請求的檔案是否已經快取在核心頁的快取記憶體中,如果存在,則直接放回該檔案的內容;

4.如果檔案不存在快取記憶體中,則通過inode定位到檔案的磁碟地址,將資料從磁碟複製到核心頁快取記憶體。之後再次範聖琦讀頁面的過程,將核心快取記憶體中的資料傳送給使用者程式

什麼是inode?

全稱為index node,既儲存檔案元資訊的區域,中文譯名“索引節點”。
例如包含:檔案許可權、檔案擁有者的UID、檔案的大小等等。

作業系統讀寫的特點

1.系統在read/write的時候是很耗時的,例如在讀檔案的時候,將檔案內容從硬碟拷貝到核心空間的一個緩衝區,然後再將這些資料拷貝到使用者空間,實際上完成了兩次資料拷貝
2.同理,寫入操作同樣耗時,待寫入的buffer在核心空間不能直接訪問,必須要先拷貝至核心空間對應的主存,再寫回磁碟中(延遲寫回),也是需要兩次資料拷貝
3.如果兩個程式都對磁碟中的一個檔案內容進行訪問,那麼這個內容在實體記憶體中有三份:程式A的地址空間 + 程式B的地址空間 + 核心頁高速緩衝空間;

此時我們找到了檔案讀取的痛點:兩次拷貝導致效率過低

二、mmap記憶體對映

對映

“對映”這個詞,就和數學課上說的“一一對映”是一個意思,就是建立一種一一對應關係,在這裡主要是指硬碟上檔案 的位置與程式邏輯地址空間 中一塊大小相同的區域之間的一一對應

注意:這種對應關係純屬是邏輯上的概念,物理上是不存在的,原因是程式的邏輯地址空間本身就是不存在的。

具體到程式碼,就是建立並初始化了相關的資料結構(struct address_space),這個過程有系統呼叫mmap()實現,所以建立記憶體對映的效率很高。

記憶體對映過程

1.mmap()會返回一個指標ptr,它指向程式邏輯地址空間中的一個地址,這樣以後,程式無需再呼叫read或write對檔案進行讀寫,而只需要通過ptr就能夠操作檔案;
2.但是ptr所指向的是一個邏輯地址,要操作其中的資料,必須通過MMU將邏輯地址轉換成實體地址;
3.建立記憶體對映並沒有實際拷貝資料,這時,MMU在地址對映表中是無法找到與ptr相對應的實體地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函式會在swap中尋找相對應的頁面,如果找不到(也就是該檔案從來沒有被讀入記憶體的情況),則會通過mmap()建立的對映關係,從硬碟上將檔案讀取到實體記憶體中;
4.如果在拷貝資料時,發現實體記憶體不夠用,則會通過虛擬記憶體機制(swap)將暫時不用的物理頁面交換到硬碟上;

MMU是Memory Management Unit的縮寫,中文名是記憶體管理單元,它是中央處理器(CPU)中用來管理虛擬儲存器、物理儲存器的控制線路,同時也負責虛擬地址對映為實體地址,以及提供硬體機制的記憶體訪問授權,多使用者多程式作業系統。

mmap記憶體對映的實現過程,總的來說可以分為三個階段:
1.程式啟動對映過程,並在虛擬地址空間中為對映建立虛擬對映區域;
2.呼叫核心空間的系統呼叫函式mmap(不同於使用者空間函式),實現檔案實體地址和程式虛擬地址的一一對映關係;
3.程式發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝;

對映過程核心

前兩個階段僅在於建立虛擬區間並完成地址對映,但是並沒有將任何檔案資料的拷貝至主存。真正的檔案讀取是當程式發起讀或寫操作時

  • 程式的讀或寫操作訪問虛擬地址空間這一段對映地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址對映,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁異常
  • 缺頁異常進行一系列判斷,確定無非法操作後,核心發起請求調頁過程
  • 調頁過程先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫nopage函式把所缺的頁從磁碟裝入到主存中。
  • 之後程式即可直接對這片主存進行讀或者寫的操作。

效率

常規檔案操作

之前說過,常規檔案操作為了提高讀寫效率和保護磁碟,使用了頁快取機制,由於頁快取處在核心空間,不能被使用者程式直接定址,這樣就出現了兩次拷貝的過程,這也是常規檔案操作的效能限制。

記憶體對映

使用mmap操作檔案中,建立新的虛擬記憶體區域和建立檔案磁碟地址和虛擬記憶體區域對映這兩步,沒有任何檔案拷貝操作
之後訪問資料時發現記憶體中並無資料而發起的缺頁異常過程,可以通過已經建立好的對映關係,只使用一次資料拷貝,就從磁碟中將資料傳入記憶體的使用者空間中,供程式使用

結論

  • 常規檔案操作需要從磁碟到頁快取再到使用者主存的兩次資料拷貝。
  • 而mmap操控檔案,只需要從磁碟到使用者主存的一次資料拷貝過程。mmap的關鍵點是實現了使用者空間和核心空間的資料直接互動而省去了空間不同資料不通的繁瑣過程。因此mmap效率更高。

mmap的例子

對硬碟上一個名為“mmap_test”的檔案進行操作,檔案中存有10000個整數,程式兩次使用不同的方法將它們讀出,加1,再寫回硬碟。

gettimeofday( &tv1, NULL );
fd = open( "mmap_test", O_RDWR );
array = mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
for( i=0; i<MAX; ++i )
 
++array[ i ];
munmap( array, sizeof(int)*MAX );
msync( array, sizeof(int)*MAX, MS_SYNC );
free( array );
close( fd );
gettimeofday( &tv2, NULL );
複製程式碼

三、mmap的優點

  • 對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。
  • 實現了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。
  • 可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap對映很好的解決。換句話說,但凡是需要用磁碟空間代替記憶體的時候,mmap都可以發揮其功效

mmap的實踐應用——MMKV

以下摘自《微信開發團隊guoling的技術分享》

什麼是MMKV? MMKV 是基於 mmap 記憶體對映的 key-value 元件,底層序列化/反序列化使用 protobuf 實現,效能高,穩定性強。 MMKV的實現 記憶體準備

通過 mmap 記憶體對映檔案,提供一段可供隨時寫入的記憶體塊,App 只管往裡面寫資料,由 iOS 負責將記憶體回寫到檔案,不必擔心 crash 導致資料丟失。

資料處理

資料序列化方面我們選用 protobuf 協議,pb 在效能和空間佔用上都有不錯的表現。考慮到我們要提供的是通用 kv 元件,key 可以限定是 string 字串型別,value 則多種多樣(int/bool/double等)。要做到通用的話,考慮將 value 通過 protobuf 協議序列化成統一的記憶體塊(buffer),然後就可以將這些 KV 物件序列化到記憶體中。

關於MMKV的原理之後將會專門新開一篇文章來詳解和應用。

相關文章