Mmap可以讓程序員解鎖哪些騷操作?
大家好,我是小風(fēng)哥!
今天這篇文章帶你講解下稍顯神秘的mmap到底是怎么一回事。
簡單的與麻煩的
用代碼讀寫內(nèi)存對(duì)程序員來說是非常方便非常自然的,但用代碼讀寫磁盤對(duì)程序員來說就不那么方便不那么自然了。
回想一下,你在代碼中讀寫內(nèi)存有多簡單:
定義一個(gè)數(shù)組:
- int a[100];
- a[0] = 2;
看到了吧,這時(shí)你就在寫內(nèi)存,甚至你可能在寫這段代碼時(shí)下意識(shí)里都沒有去想讀內(nèi)存這件事。
再想想你是怎樣讀磁盤文件的?
- char buf[1024];
- int fd = open("/filepath/abc.txt");
- read(fd, buf, 1024);
- // 操作buf等等
看到了吧,讀寫磁盤文件其實(shí)是一件很麻煩的事情,你需要open一個(gè)文件,意思是告訴操作系統(tǒng)“Hey,操作系統(tǒng),我要開始讀abc.txt這個(gè)文件了,把這個(gè)文件的所有信息準(zhǔn)備好,然后給我一個(gè)代號(hào)”。這個(gè)代號(hào)就是所謂的文件描述符,拿到文件描述符后你才能繼續(xù)接下來的讀寫操作。
為什么麻煩
現(xiàn)在你應(yīng)該看到了,操作磁盤文件要比操作內(nèi)存復(fù)雜很多,根本原因就在于尋址方式不同。
對(duì)內(nèi)存來說我們可以直接按照字節(jié)粒度去尋址,但對(duì)磁盤上保存的文件來說則不是這樣的,磁盤上保存的文件是按照塊(block)的粒度來尋址的,因此你必須先把磁盤中的文件讀取到內(nèi)存中,然后再按照字節(jié)粒度來操作文件內(nèi)容。
你可能會(huì)想既然直接操作內(nèi)存很簡單,那么我們有沒有辦法像讀寫內(nèi)存那樣去直接讀寫磁盤文件呢?
答案是肯定的。
要開腦洞了
對(duì)于像我們這樣在用戶態(tài)編程的程序員來說,內(nèi)存在我們眼里就是一段連續(xù)的空間。啊哈,巧了,磁盤上保存的文件在程序員眼里也存放在一段連續(xù)的空間中(有的同學(xué)可能會(huì)說文件其實(shí)是在磁盤上離散存放的,請(qǐng)注意,我們?cè)谶@里只從文件使用者的角度來講)。
那么這兩段空間有沒有辦法關(guān)聯(lián)起來呢?
答案是肯定的,怎么關(guān)聯(lián)呢?
答案就是。。。。。。你猜對(duì)了嗎?答案是通過虛擬內(nèi)存。
關(guān)于虛擬內(nèi)存我們已經(jīng)講解過很多次了,虛擬內(nèi)存就是假的地址空間,是進(jìn)程看到的幻象,其目的是讓每個(gè)進(jìn)程都認(rèn)為自己獨(dú)占內(nèi)存,關(guān)于虛擬內(nèi)存完整的詳細(xì)講解請(qǐng)參考博主的深入理解操作系統(tǒng),關(guān)注公眾號(hào)碼農(nóng)的荒島求生并回復(fù)操作系統(tǒng)即可。
既然進(jìn)程看到地址空間是假的那么一切都好辦了。
既然是假的,那么就有做手腳的操作空間,怎么做手腳呢?
從普通程序員眼里看文件不是保存在一段連續(xù)的磁盤空間上嗎?我們可以直接把這段空間映射到進(jìn)程的內(nèi)存中,就像這樣:
假設(shè)文件長度是100字節(jié),我們把該文件映射到了進(jìn)程的內(nèi)存中,地址是從600 ~ 800,那么當(dāng)你直接讀寫600 ~ 800這段內(nèi)存時(shí),實(shí)際上就是在直接操作磁盤文件。
這一切是怎么做到呢?
魔術(shù)師操作系統(tǒng)
原來這一切背后的功勞是操作系統(tǒng)。
當(dāng)我們首次讀取600~800這段地址空間時(shí),操作系統(tǒng)會(huì)檢測的這一操作,因?yàn)榇藭r(shí)這段內(nèi)存中什么內(nèi)容都還沒有,此時(shí)操作系統(tǒng)自己讀取磁盤文件填充到這段內(nèi)存空間中,此后程序就可以像讀內(nèi)存一樣直接讀取磁盤內(nèi)容了。
寫操作也很簡單,用戶程序依然可以直接修改這塊內(nèi)存,此后操作系統(tǒng)會(huì)在背后將修改內(nèi)容寫回磁盤。
現(xiàn)在你應(yīng)該看到了,其實(shí)采用mmap這種方法磁盤依然還是按照塊的粒度來尋址的,只不過在操作系統(tǒng)的一番騷操作下對(duì)于用戶態(tài)的程序來說“看起來”我們能像讀寫內(nèi)存那樣直接讀寫磁盤文件了,從按塊粒度尋址到按照字節(jié)粒度尋址,這中間的差異就是操作系統(tǒng)來填補(bǔ)的。
我想你現(xiàn)在應(yīng)該大體明白mmap是什么意思了。
接下來你肯定要問的問題就是,mmap有什么好處呢?我為什么要使用mmap?
內(nèi)存copy與系統(tǒng)調(diào)用
我們常用的標(biāo)準(zhǔn)IO,也就是read/write其底層是涉及到系統(tǒng)調(diào)用的,同時(shí)當(dāng)使用read/write讀寫文件內(nèi)容時(shí),需要將數(shù)據(jù)從內(nèi)核態(tài)copy到用戶態(tài),修改完畢后再從用戶態(tài)copy到內(nèi)核態(tài),顯然,這些都是有開銷的。
而mmap則無此問題,基于mmap讀寫磁盤文件不會(huì)招致系統(tǒng)調(diào)用以及額外的內(nèi)存copy開銷,但mmap也不是完美的,mmap也有自己的缺點(diǎn)。
其中一方面在于為了創(chuàng)建并維持地址空間與文件的映射關(guān)系,內(nèi)核中需要有特定的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)這一映射,這當(dāng)然是有性能開銷的,除此之外另一點(diǎn)就是缺頁問題,page fault。
注意,缺頁中斷也是有開銷的,而且不同的內(nèi)核由于內(nèi)部的實(shí)現(xiàn)機(jī)制不同,其系統(tǒng)調(diào)用、數(shù)據(jù)copy以及缺頁處理的開銷也不同,因此就性能上來說我們不能肯定的說mmap就比標(biāo)準(zhǔn)IO好。這要看標(biāo)準(zhǔn)IO中的系統(tǒng)調(diào)用、內(nèi)存調(diào)用的開銷與mmap方法中的缺頁中斷處理的開銷哪個(gè)更小,開銷小的一方將展現(xiàn)出更優(yōu)異的性能。
還是那句話,談到性能,單純的理論分析就不是那么好用了,你需要基于真實(shí)的場景基于特定的操作系統(tǒng)以及硬件去測試才能有結(jié)論。
大文件處理
到目前為止我想大家對(duì)mmap最直觀的理解就是可以像直接讀寫內(nèi)存那樣來操作磁盤文件,這是其中一個(gè)優(yōu)點(diǎn)。
另一個(gè)優(yōu)點(diǎn)在于mmap其實(shí)是和操作系統(tǒng)中的虛擬內(nèi)存密切相關(guān)的,這就為mmap帶來了一個(gè)很有趣的優(yōu)勢。
這個(gè)優(yōu)勢在于處理大文件場景,這里的大文件指的是文件的大小超過你的物理內(nèi)存,在這種場景下如果你使用傳統(tǒng)的read/write,那么你必須一塊一塊的把文件搬到內(nèi)存,處理完文件的一小部分再處理下一部分。
這種需要在內(nèi)存中開辟一塊空間——也就是我們常說的buffer,的方案聽上去就麻煩有沒有,而且還需要操作系統(tǒng)把數(shù)據(jù)從內(nèi)核態(tài)copy到用戶態(tài)的buffer中。
但如果用mmap情況就不一樣了,只要你的進(jìn)程地址空間足夠大,可以直接把這個(gè)大文件映射到你的進(jìn)程地址空間中,即使該文件大小超過物理內(nèi)存也可以,這就是虛擬內(nèi)存的巧妙之處了,當(dāng)物理內(nèi)存的空閑空間所剩無幾時(shí)虛擬內(nèi)存會(huì)把你進(jìn)程地址空間中不常用的部分扔出去,這樣你就可以繼續(xù)在有限的物理內(nèi)存中處理超大文件了,這個(gè)過程對(duì)程序員是透明的,虛擬內(nèi)存都給你處理好了。關(guān)于虛擬內(nèi)存的透徹講解請(qǐng)參考博主的深入理解操作系統(tǒng),關(guān)注公眾號(hào)碼農(nóng)的荒島求生并回復(fù)操作系統(tǒng)即可。
注意,mmap與虛擬內(nèi)存的結(jié)合在處理大文件時(shí)可以簡化代碼設(shè)計(jì),但在性能上是否優(yōu)于傳統(tǒng)的read/write方法就不一定了,還是那句話關(guān)于mmap與傳統(tǒng)IO在涉及到性能時(shí)你需要基于真實(shí)的應(yīng)用場景測試。
使用mmap處理大文件要注意一點(diǎn),如果你的系統(tǒng)是32位的話,進(jìn)程的地址空間就只有4G,這其中還有一部分預(yù)留給操作系統(tǒng),因此在32位系統(tǒng)下可能不足以在你的進(jìn)程地址空間中找到一塊連續(xù)的空間來映射該文件,在64位系統(tǒng)下則無需擔(dān)心地址空間不足的問題,這一點(diǎn)要注意。
節(jié)省內(nèi)存
這可能是mmap最大的優(yōu)勢,以及最好的應(yīng)用場景了。
假設(shè)有一個(gè)文件,很多進(jìn)程的運(yùn)行都依賴于此文件,而且還是有一個(gè)假設(shè),那就是這些進(jìn)程是以只讀(read-only)的方式依賴于此文件。
你一定在想,這么神奇?很多進(jìn)程以只讀的方式依賴此文件?有這樣的文件嗎?
答案是肯定的,這就是動(dòng)態(tài)鏈接庫。
要想弄清楚動(dòng)態(tài)鏈接庫,我們就不得不從靜態(tài)庫說起。
假設(shè)有三個(gè)程序A、B、C依賴一個(gè)靜態(tài)庫,那么鏈接器在生成可執(zhí)行程序A、B、C時(shí)會(huì)把該靜態(tài)庫copy到A、B、C中,就像這樣:
假設(shè)你本身要寫的代碼只有2MB大小,但卻依賴了一個(gè)100MB的靜態(tài)庫,那么最終生成的可執(zhí)行程序就是102MB,盡管你本身的代碼只有2MB。
而且從圖中我們可以看出,可執(zhí)行程序A、B、C中都有一部分靜態(tài)庫的副本,這里面的內(nèi)容是完全一樣的,那么很顯然,這些可執(zhí)行程序放在磁盤上會(huì)浪費(fèi)磁盤空間,加載到內(nèi)存中運(yùn)行時(shí)會(huì)浪費(fèi)內(nèi)存空間。
那么該怎么解決這個(gè)問題呢?
很簡單,可執(zhí)行程序A、B、C中為什么都要各自保存一份完全一樣的數(shù)據(jù)呢?其實(shí)我們只需要在可執(zhí)行程序A、B、C中保存一小點(diǎn)信息,這點(diǎn)信息里記錄了依賴了哪個(gè)庫,那么當(dāng)可執(zhí)行程序運(yùn)行起來后再把相應(yīng)的庫加載到內(nèi)存中:
依然假設(shè)你本身要寫的代碼只有2MB大小,此時(shí)依賴了一個(gè)100MB的動(dòng)態(tài)鏈接庫,那么最終生成的可執(zhí)行程序就是2MB,盡管你依賴了一個(gè)100MB的庫。
而且從圖中可以看出,此時(shí)可執(zhí)行程序ABC中已經(jīng)沒有冗余信息了,這不但節(jié)省磁盤空間,而且節(jié)省內(nèi)存空間,讓有限的內(nèi)存可以同時(shí)運(yùn)行更多的進(jìn)程,是不是很酷。
現(xiàn)在我們已經(jīng)知道了動(dòng)態(tài)庫的妙用,但我們并沒有說明動(dòng)態(tài)庫是怎么節(jié)省內(nèi)存的,接下來mmap就該登場了。
你不是很多進(jìn)程都依賴于同一個(gè)庫嘛,那么我就用mmap把該庫直接映射到各個(gè)進(jìn)程的地址空間中,盡管每個(gè)進(jìn)程都認(rèn)為自己地址空間中加載了該庫,但實(shí)際上該庫在內(nèi)存中只有一份。
mmap就這樣很神奇和動(dòng)態(tài)鏈接庫聯(lián)動(dòng)起來了,關(guān)于鏈接器以及靜態(tài)庫動(dòng)態(tài)庫等更加詳細(xì)的講解你可以關(guān)注公眾號(hào)碼農(nóng)的荒島求生并回復(fù)鏈接器即可。
想用好mmap沒那么容易
現(xiàn)在你應(yīng)該大體了解mmap,想用好mmap你必須對(duì)虛擬內(nèi)存有一個(gè)較為透徹的理解,并且能對(duì)你的應(yīng)用場景有一個(gè)透徹的理解,在使用mmap之前問問自己是不是還有更好的辦法,因此,對(duì)于新手來說并不推薦使用該機(jī)制。
總結(jié)
mmap在博主眼里是一種很獨(dú)特的機(jī)制,這種機(jī)制最大的誘惑在于可以像讀寫內(nèi)存樣方便的操作磁盤文件,這簡直就像魔法一樣,因此在一些場景下可以簡化代碼設(shè)計(jì)。
但談到mmap的與標(biāo)準(zhǔn)IO(read/write)的性能情況就比較復(fù)雜了,標(biāo)準(zhǔn)IO設(shè)計(jì)到系統(tǒng)調(diào)用以及用戶態(tài)內(nèi)核態(tài)的copy問題,而mmap則涉及到維持內(nèi)存與磁盤文件的映射關(guān)系以及缺頁處理的開銷,單純的從理論分析這二者半斤八兩,如果你的應(yīng)用場景對(duì)性能要求較高,那么你需要基于真實(shí)場景進(jìn)行測試。
我是小風(fēng)哥,希望這篇文章對(duì)大家理解mmap有所幫助。