[原創(chuàng)] 深入剖析mmap原理 - 從三個關(guān)鍵問題說起

概述

對于mmap,您是否能從原理上解析以下三個問題:

  1. mmap比物理內(nèi)存+swap空間大情況下,是否有問題?
  2. MAP_SHARED,MAP_PRIVATE,MAP_ANONYMOUS,MAP_NORESERVE到底有什么區(qū)別?
  3. 常聽說mmap的讀寫比傳統(tǒng)的系統(tǒng)調(diào)用(read, write)快,但真的是這樣子嗎?原因是什么?

要解決這些疑問,可能還需要在操作系統(tǒng)層面多了解。本文將嘗試通過這些問題深入剖析,希望通過這篇文章,能使大家對mmap有較深入的認(rèn)識,也能在存儲引擎的設(shè)計中,有所參考。

背景

最近在研發(fā)分布式日志存儲系統(tǒng),這是一個基于Raft協(xié)議的自研分布式日志存儲系統(tǒng),Logstore則是底層存儲引擎。

Logstore中,使用mmap對數(shù)據(jù)文件進(jìn)行讀寫。Logstore的存儲結(jié)構(gòu)簡化如下圖:

logstore mmap.png

Logstore使用了Segments Files + Index Files的方式存儲Log,Segment File是存儲主體,用于存儲Log數(shù)據(jù),使用定長的方式,默認(rèn)每個512M,Index File主要用于Segment File的內(nèi)容檢索。

Logstore使用mmap的方式讀寫Segment File,Segments Files的個數(shù),主要取決于磁盤空間或者業(yè)務(wù)需求,一般情況下,Logstore會存儲1T~5T的數(shù)據(jù)。

什么是mmap

我們先看看什么是mmap。

在<<深入理解計算機(jī)系統(tǒng)>>這本書中,mmap定義為:Linux通過將一個虛擬內(nèi)存區(qū)域與一個磁盤上的對象(object)關(guān)聯(lián)起來,以初始化這個虛擬內(nèi)存區(qū)域的內(nèi)容,這個過程稱為內(nèi)存映射(memory mapping)。

在Logstore中,mapping的對象是普通文件(Segment File)。

mmap的原理

mmap在進(jìn)程虛擬內(nèi)存做了什么

我們先來簡單看一下mapping一個文件,mmap做了什么事情。如下圖所示:

map file.png

假設(shè)我們mmap的文件是FileA,在調(diào)用mmap之后,會在進(jìn)程的虛擬內(nèi)存分配地址空間,創(chuàng)建映射關(guān)系。

這里值得注意的是,mmap只是在虛擬內(nèi)存分配了地址空間,舉個例子,假設(shè)上述的FileA是2G大小

[dragon@xxx.xxx] ls -lat FileA

2147483648 Apr 25 10:22 FileA

在mmap之后,查看mmap所在進(jìn)程的maps描述,可以看到

[dragon@xxx.xxx] cat maps
....
7f35eea8d000-7f366ea8d000 rw-s 00000000 08:03 13110516 FileA
....

由上可以看到,在mmap之后,進(jìn)程的地址空間7f35eea8d000-7f366ea8d000被分配,并且map到FileA,7f366ea8d000減去7f35eea8d000,剛好是2147483648(ps: 這里是整個文件做mapping)

mmap在物理內(nèi)存做了什么

在Linux中,VM系統(tǒng)通過將虛擬內(nèi)存分割為稱作虛擬頁(Virtual Page,VP)大小固定的塊來處理磁盤(較低層)與上層數(shù)據(jù)的傳輸,一般情況下,每個頁的大小默認(rèn)是4096字節(jié)。同樣的,物理內(nèi)存也被分割為物理頁(Physical Page,PP),也為4096字節(jié)。

上述例子,在mmap之后,如下圖:

virtual-physical.png

在mmap之后,并沒有在將文件內(nèi)容加載到物理頁上,只上在虛擬內(nèi)存中分配了地址空間。當(dāng)進(jìn)程在訪問這段地址時(通過mmap在寫入或讀取時FileA),若虛擬內(nèi)存對應(yīng)的page沒有在物理內(nèi)存中緩存,則產(chǎn)生"缺頁",由內(nèi)核的缺頁異常處理程序處理,將文件對應(yīng)內(nèi)容,以頁為單位(4096)加載到物理內(nèi)存,注意是只加載缺頁,但也會受操作系統(tǒng)一些調(diào)度策略影響,加載的比所需的多,這里就不展開了。
(PS: 再具體一些,進(jìn)程在訪問7f35eea8d000這個進(jìn)程虛擬地址時,MMU通過查找頁表,發(fā)現(xiàn)對應(yīng)內(nèi)容未緩存在物理內(nèi)存中,則產(chǎn)生"缺頁")

缺頁處理后,如下圖:

virtual-physical assign.png

mmap的分類

我認(rèn)為從原理上,mmap有兩種類型,一種是有backend,一種是沒有backend。

有backend

backend mmap.png

這種模式將普通文件做memory mapping(非MAP_ANONYMOUS),所以在mmap系統(tǒng)調(diào)用時,需要傳入文件的fd。這種模式常見的有兩個常用的方式,MAP_SHARED與MAP_PRIVATE,但它們的行為卻不相同。

1) MAP_SHARED

這個方式我認(rèn)為可以從兩個角度去看:

  1. 進(jìn)程間可見:這個被提及太多,就不展開討論了
  2. 寫入/更新數(shù)據(jù)會回寫backend,也就是回寫文件:這個是很關(guān)鍵的特性,是在Logstore設(shè)計實現(xiàn)時,需要考慮的重點(diǎn)。Logstore的一個基本功能就是不斷地寫入數(shù)據(jù),從實現(xiàn)上看就是不斷地mmap文件,往內(nèi)存寫入/更新數(shù)據(jù)以達(dá)到寫入文件的目的。但物理內(nèi)存是有限的,在寫入數(shù)據(jù)超過物理內(nèi)存時,操作系統(tǒng)會進(jìn)行頁置換,根據(jù)淘汰算法,將需要淘汰的頁置換成所需的新頁,而恰恰因為是有backend的,所以mmap對應(yīng)的內(nèi)存是可以被淘汰的(若內(nèi)存頁是"臟"的,則操作系統(tǒng)會先將數(shù)據(jù)回寫磁盤再淘汰)。這樣,就算mmap的數(shù)據(jù)遠(yuǎn)大于物理內(nèi)存,操作系統(tǒng)也能很好地處理,不會產(chǎn)生功能上的問題。

2) MAP_PRIVATE

這是一個copy-on-write的映射方式。雖然他也是有backend的,但在寫入數(shù)據(jù)時,他會在物理內(nèi)存copy一份數(shù)據(jù)出來(以頁為單位),而且這些數(shù)據(jù)是不會被回寫到文件的。這里就要注意,因為更新的數(shù)據(jù)是一個副本,而且不會被回寫,這就意味著如果程序運(yùn)行時不主動釋放,若更新的數(shù)據(jù)超過可用物理內(nèi)存+swap space,就會遇到OOM Killer。

無backend

無backend通常是MAP_ANONYMOUS,就是將一個區(qū)域映射到一個匿名文件,匿名文件是由內(nèi)核創(chuàng)建的。因為沒有backend,寫入/更新的數(shù)據(jù)之后,若不主動釋放,這些占用的物理內(nèi)存是不能被釋放的,同樣會出現(xiàn)OOM Killer。

mmap比內(nèi)存+swap空間大情況下,是否有問題

到這里,這個問題就比較好解析了。我們可以將此問題分離為:

  1. 虛擬內(nèi)存是否會出問題
  2. 物理內(nèi)存是否會出問題

-- 虛擬內(nèi)存是否會出問題:

回到上述的"mmap在進(jìn)程虛擬內(nèi)存做了什么",我們知道m(xù)map會在進(jìn)程的虛擬內(nèi)存中分配地址空間,比如1G的文件,則分配1G的連續(xù)地址空間。那究竟可以maping多少呢?在64位操作系統(tǒng),尋址范圍是2^64 ,除去一些內(nèi)核、進(jìn)程數(shù)據(jù)等地址段之外,基本上可以認(rèn)為可以mapping無限大的數(shù)據(jù)(不太嚴(yán)謹(jǐn)?shù)恼f法)。

-- 物理內(nèi)存是否會出問題
回到上述"mmap的分類",對于有backend的mmap,而且是能回寫到文件的,映射比內(nèi)存+swap空間大是沒有問題的。但無法回寫到文件的,需要非常注意,主動釋放。

MAP_NORESERVE

MAP_NORESERVE是mmap的一個參數(shù),MAN的說明是"Do not reserve swap space for this mapping. When swap space is reserved, one has the guarantee that it is possible to modify the mapping."。

我們做個測試:

場景A:物理內(nèi)存+swap space: 16G,映射文件30G,使用一個進(jìn)程進(jìn)行mmap,成功后映射后持續(xù)寫入數(shù)據(jù)
場景B:物理內(nèi)存+swap space: 16G,映射文件15G,使用兩個進(jìn)程進(jìn)行mmap,成功后映射后持續(xù)寫入數(shù)據(jù)

場景 序列 映射類型 結(jié)果
A 1 MAP_PRIVATE mmap報錯
A 2 MAP_PRIVATE + MAP_NORESERVE mmap成功,在持續(xù)寫入情況下,遇到OOM Killer
A 3 MAP_SHARED mmap成功,在持續(xù)寫入正常
B 4 MAP_PRIVATE mmap成功,在持續(xù)寫入情況下,有一個進(jìn)程會遇到OOM Killer
B 5 MAP_PRIVATE + MAP_NORESERVE mmap成功,在持續(xù)寫入情況下,有一個進(jìn)程會遇到OOM Killer
B 6 MAP_SHARED mmap成功,在持續(xù)寫入正常

從上述測試可以看出,從現(xiàn)象上看,NORESERVE是繞過mmap的校驗,讓其可以mmap成功。但其實在RESERVE的情況下(序列4),從測試結(jié)果看,也沒有保障。

mmap的性能

mmap的性能經(jīng)常與系統(tǒng)調(diào)用(write/read)做對比。

我們將讀寫分開看,先嘗試從原理上分析兩者的差異,然后再通過測試驗證。

mmap的寫性能

我們先來簡單講講write系統(tǒng)調(diào)用寫文件的過程:

write process.png
  1. Step1:進(jìn)程(用戶態(tài))調(diào)用write系統(tǒng)調(diào)用,并告訴內(nèi)核需要寫入數(shù)據(jù)的開始地址與長度(告訴內(nèi)核寫入的數(shù)據(jù)在哪)。
  2. Step2:內(nèi)核write方法,將校驗用戶態(tài)的數(shù)據(jù),然后復(fù)制到kernel buffer(這里是Page Cache)。
    [ ps: 特意查了ext4 write的內(nèi)核實現(xiàn),write是直接將user buffer copy到page中 ]
  3. Step3: 由操作系統(tǒng)調(diào)用,將臟頁回寫到磁盤(通常這是異步的)

再來簡單講講使用mmap時,寫入文件流程:

  1. Step1:進(jìn)程(用戶態(tài))將需要寫入的數(shù)據(jù)直接copy到對應(yīng)的mmap地址(內(nèi)存copy)
  2. Step2:
    2.1) 若mmap地址未對應(yīng)物理內(nèi)存,則產(chǎn)生缺頁異常,由內(nèi)核處理
    2.2) 若已對應(yīng),則直接copy到對應(yīng)的物理內(nèi)存
  3. Step3:由操作系統(tǒng)調(diào)用,將臟頁回寫到磁盤(通常這是異步的)

系統(tǒng)調(diào)用會對性能有影響,那么從理論上分析:

  1. 若每次寫入的數(shù)據(jù)大小接近page size(4096),那么write調(diào)用與mmap的寫性能應(yīng)該比較接近(因為系統(tǒng)調(diào)用次數(shù)相近)
  2. 若每次寫入的數(shù)據(jù)非常小,那么write調(diào)用的性能應(yīng)該遠(yuǎn)慢于mmap的性能。

下面我們對兩者進(jìn)行性能測試:

場景:對2G的文件進(jìn)行順序?qū)懭?go語言編寫)

每次寫入大小 | mmap 耗時 | write 耗時
--------------- | ------- | -------- | --------
| 1 byte | 22.14s | >300s
| 100 bytes | 2.84s | 22.86s
| 512 bytes | 2.51s | 5.43s
| 1024 bytes | 2.48s | 3.48s
| 2048 bytes | 2.47s | 2.34s
| 4096 bytes | 2.48s | 1.74s
| 8192 bytes | 2.45s | 1.67s
| 10240 bytes | 2.49s | 1.65s

可以看到mmap在100byte寫入時已經(jīng)基本達(dá)到最大寫入性能,而write調(diào)用需要在4096(也就是一個page size)時,才能達(dá)到最大寫入性能。

從測試結(jié)果可以看出,在寫小數(shù)據(jù)時,mmap會比write調(diào)用快,但在寫大數(shù)據(jù)時,反而沒那么快(但不太確認(rèn)是否go的slice copy的性能問題,沒時間去測C了)。

測試結(jié)果與理論推導(dǎo)吻合。

mmap的讀性能

我們還是來簡單分析read調(diào)用與mmap的流程:

read process.png

從圖中可以看出,read調(diào)用確實比mmap多一次copy。因為read調(diào)用,進(jìn)程是無法直接訪問kernel space的,所以在read系統(tǒng)調(diào)用返回前,內(nèi)核需要將數(shù)據(jù)從內(nèi)核復(fù)制到進(jìn)程指定的buffer。但mmap之后,進(jìn)程可以直接訪問mmap的數(shù)據(jù)(page cache)。

從原理上看,read性能會比mmap慢。

接下來實測一下性能區(qū)別:

場景:對2G的文件進(jìn)行順序讀取(go語言編寫)
(ps: 為了避免磁盤對測試的影響,我讓2G文件都緩存在pagecache中)

每次讀取大小 | mmap 耗時 | write 耗時
--------------- | ------- | -------- | --------
| 1 byte | 8215.4ms | > 300s
| 100 bytes | 86.4ms | 8100.9ms
| 512 bytes | 16.14ms | 1851.45ms
| 1024 bytes | 8.11ms | 992.71ms
| 2048 bytes | 4.09ms | 636.85ms
| 4096 bytes | 2.07ms | 558.10ms
| 8192 bytes | 1.06ms | 444.83ms
| 10240 bytes | 867.88μs | 475.28ms

由上可以看出,在read上面,mmap比write的性能差別還是很大的。測試結(jié)果與理論推導(dǎo)吻合。

結(jié)束語

對mmap的深入了解,能幫助我們在設(shè)計存儲系統(tǒng)時,更好地進(jìn)行決策。
比如,假設(shè)需要設(shè)計一個底層的數(shù)據(jù)結(jié)構(gòu)是B+ Tree,node操作以Page單位的單機(jī)存儲引擎,根據(jù)上述推論,寫入使用系統(tǒng)調(diào)用,而讀取使用mmap,可以達(dá)到最優(yōu)的性能。而LMDB就是如此實現(xiàn)的。

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

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

  • Linux進(jìn)程通信實現(xiàn)機(jī)制有很多,也有各自優(yōu)缺點(diǎn)和適用場景,關(guān)于她們之間的對比,等各種通信機(jī)制一一介紹后,再來一個...
    batbattle閱讀 4,226評論 3 13
  • UNIX網(wǎng)絡(luò)編程第二卷進(jìn)程間通信對mmap函數(shù)進(jìn)行了說明。該函數(shù)主要用途有三個:1、將一個普通文件映射到內(nèi)存中,通...
    宇文黎琴閱讀 3,754評論 0 4
  • 金色童年幼兒園三月第一周餐譜 幼兒園 的午餐和加點(diǎn)根據(jù)幼兒膳食營養(yǎng)搭配,葷素搭配,食材新鮮,美味可口,孩子們也很喜...
    Emmi米閱讀 559評論 0 0
  • 知道問題是什么,才可以解決問題。 因為沒時間。 我報名了9月份的上海高口筆試,準(zhǔn)備報名11月份CATTI的二級筆譯...
    青漪雨燃閱讀 532評論 3 1
  • 很久的日子,總覺的有什么事情沒有做完,以前很喜歡文學(xué)的時候,看著喜歡的書,沉浸在自得其樂的讀書當(dāng)中,閑...
    宣巖西歌閱讀 472評論 2 6

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