本系列前一篇已經分析了lightningmdb的總體架構和主要的數據結構。本文將介紹一下MMAP原理以及lmdb中如何使用它。node
1. Memory Map原理linux
內存映射文件與虛擬內存有些相似,經過內存映射文件能夠保留一個地址空間的區域,同時將物理存儲器提交給此區域,只是內存文件映射的物理存儲器來自一個已經存在於磁盤上的文件,而非系統的頁文件,並且在對該文件進行操做以前必須首先對文件進行映射,就如同將整個文件從磁盤加載到內存。由此能夠看出,使用內存映射文件處理存儲於磁盤上的文件時,將不須要由應用程序對文件執行I/O操做,這意味着在對文件進行處理時將沒必要再爲文件申請並分配緩存,全部的文件緩存操做均由系統直接管理,因爲取消了將文件數據加載到內存、數據從內存到文件的回寫以及釋放內存塊等步驟,使得內存映射文件在處理大數據量的文件時能起到至關重要的做用。另外,實際工程中的系統每每須要在多個進程之間共享數據,若是數據量小,處理方法是靈活多變的,若是共享數據容量巨大,那麼就須要藉助於內存映射文件來進行。實際上,內存映射文件正是解決本地多個進程間數據共享的最有效方法。算法
根據網友實測,mmap的操做效率是普通文件io操做的2-4倍。其緣由主要就是避免了io操做過程當中,內存申請、複製以及跨內核空間的轉換。數據庫
2. windows與linux實現的方式windows
windows下經過內存映射文件(CreateFileMapping)系列函數完成,其公開的API架構以下圖所示緩存
它是內存管理的一種方式,是進行進程間大數據共享的基本方式。數據結構
使用的基本方式是:架構
首先要經過CreateFile()函數來建立或打開一個文件內核對象,這個對象標識了磁盤上將要用做內存映射文件的文件。在用CreateFile()將文件映像在物理存儲器的位置通告給操做系統後,只指定了映像文件的路徑,映像的長度尚未指定。爲了指定文件映射對象須要多大的物理存儲空間還須要經過CreateFileMapping()函數來建立一個文件映射內核對象以告訴系統文件的尺寸以及訪問文件的方式。在建立了文件映射對象後,還必須爲文件數據保留一個地址空間區域,並把文件數據做爲映射到該區域的物理存儲器進行提交。由MapViewOfFile()函數負責經過系統的管理而將文件映射對象的所有或部分映射到進程地址空間。此時,對內存映射文件的使用和處理同一般加載到內存中的文件數據的處理方式基本同樣,在完成了對內存映射文件的使用時,還要經過一系列的操做完成對其的清除和使用過資源的釋放。這部分相對比較簡單,能夠經過UnmapViewOfFile()完成從進程的地址空間撤消文件數據的映像、經過CloseHandle()關閉前面建立的文件映射對象和文件對象。併發
linux下經過mmap系列函數實現。基本過程如圖所示:app
通常的文件io操做方式以下圖所示:
從以上兩圖比較可知,直接文件io將不可避免的進行屢次內存複製。
基於以上的系統內存映射原理可知,內存映射是系統內核級的內存管理方式,其在不致使swap(由於物理內存不夠)等附加磁盤io的前提下,
效率是很高的,所以其在數據庫領域也有必定的適應性。基於內存映射的數據庫系統,在實際的數據文件小於進程可用物理內存大小時,
效率遠遠高於通常的數據庫系統,當數據文件比較大時,若應用訪問的頁面很是分散且數目巨大時,好比全表掃描時,這時內存映射將頻繁
出發缺頁異常,進而頻繁進行swap,從而一次io變成2次io,效率反而降低。若應用訪問基本爲索引掃描,則以上狀況能夠避免,哪怕數據
文件遠大於實際可用物理內存,則效率仍是不錯的。同時系統內存映射方式實現的數據庫系統將大大簡化內存管理、緩存管理、外存管理
等,所以其是必定規模和特定應用的首選實現方式,lmdb主要也是基於以上幾點考慮使用內存映射。
3. lmdb使用方式
lmdb在建立環境(env對象)的時候首先檢查文件頭的相關信息,並得到文件大小,在打開的過程當中經過系統函數對文件進行映射。
其餘時刻都直接使用內存指針,經過系統級別的缺頁異常獲取對應的數據。頁面內數據的獲取和使用MDB_CURSOR_GET進行。
頁面的獲取和key查詢經過mdb_page_get/mdb_page_search完成。
要理解爲何mmap映射的地址空間和指針對於lmdb代碼是可用的,首先得理解lmdb的頁面數據組織方式,如下示例以葉子頁進行解釋,
branch頁面與其相似。
葉子頁面的數據的組織方式以下所示:
pgno | pad | flags | overflows① | nd_index1 | nd_index2 | nd_index3 | nd_index4 |
node4[ | lo | hi② | flags | keysize | data(key) | data*(value) | ] |
node3[ | lo | hi | flags | keysize | data(key) | data*(value) | ] |
node2[ | lo | hi | flags | keysize | data(key) | data*(value) | ] |
node1[ | lo | hi | flags | keysize | data(key) | data*(value) | ] |
①overflows是一個union對象,表明可用空間低、高地址或者overflow的頁面數。
overflow頁面是連續頁面,data只需指向第一個頁面便可,後續頁面無需pgno也
不會致使其餘pgno出錯。
②節點大小由lo以及hi的低16位決定。
節點的key是可變大小,由keysize決定,具體內容包含在data中
節點的value佔用內存比較大的,具體有環境指定最大節點大小。其data將指向overflow頁面。
頁面頭部大小及內容是固定的,具體的含義表明根據flags決定,在頭部以後緊接的是node,真正的key-value值對所在位置的索引,所以訪問這些node時
經過指針計算便可獲得對應的位置。在對頁面進行檢索式經過二分查找肯定。
節點的索引部分,nd_index根據key的大小排序,即key[index2]必定大於或等於key[index1]。按照插入排序算法,進行節點插入,而且從page頭向
page中間靠攏。
節點內容部分,按照插入順序,從頁面地址最高處向頁面中間考慮。node內容部分保持無序狀態,即加入key爲1,2,3,4,插入順序爲1,4,3,2, 索引部分
爲1,2,3,4,而數據部分則爲1,4,3,2.
數據部分和索引部分都是直接存儲數據(經過memcpy)而非存儲指針,所以序列化以後再經過mmap進行映射時,數據是可用的。memcpy在此不可避免的
另外一個緣由是data是從應用程序傳遞過來的,不進行復制直接存儲將致使再次訪問時致使內存不可用異常(segment fault 錯誤)。
所以在lmdb中,最重要的是如何將頁面給映射進進程地址空間。lmdb經過mdb_page_get函數以pageno爲主要參數得到頁面並返回頁面指針。若僅僅是
只讀事務且環境對象是以只讀方式打開的,page的獲取很簡單,根據page=mapadress + pagno * pagesize得到。基於此方式能夠工做的緣由是前文提到的
在lmdb中B+Tree的是基於append-only B+Tree改造的。對於數據增長、修改、刪除致使頁面增長時,pageno也增長,當舊頁面(數據舊版本)被重用時,
pageno保持不變,所以pageno保持了在數據文件中的順序性,從而在獲取頁面時,只須要進行簡單計算便可以。同時在建立env對象時,數據庫已經被整個
映射進整個進程空間,所以系統在映射時,會給數據庫文件保留所有地址空間,從而在根據上述算法獲取真實數據庫,系統觸發缺頁錯誤,進而從數據文件中
獲取整個頁面內容。此爲最簡單有效方式,不然不將所有數據映射進地址空間,對於未映射部分還須要在訪問頁面時判斷是否已經被映射,未被映射時進行映射。
另注:lmdb對於髒頁的刷新,採起可選方式,支持經過內存映射寫入,也支持經過文件寫入。默認支持爲經過文件寫入。應用程序在進行內存映射時以只讀方式
進行打開,在須要時在經過文件方式寫入。lmdb保證任意時刻只有一個寫操做在進行,從而避免了併發時數據被破壞。
本文參考了其餘網友的一些博文,在此謝謝他們的努力工做。
【1】http://blog.csdn.net/hongchangfirst/article/details/11599369
【2】http://blog.csdn.net/hustfoxy/article/details/8710307