1. 程式人生 > 其它 >mmap可以讓程式設計師解鎖哪些騷操作?

mmap可以讓程式設計師解鎖哪些騷操作?

大家好,我是小風哥!今天這篇文章帶你講解下稍顯神祕的mmap到底是怎麼一回事。

簡單的與麻煩的

用程式碼讀寫記憶體對程式設計師來說是非常方便非常自然的,但用程式碼讀寫磁碟對程式設計師來說就不那麼方便不那麼自然了。回想一下,你在程式碼中讀寫記憶體有多簡單:定義一個數組:

int a[100];a[0] = 2;

看到了吧,這時你就在寫記憶體,甚至你可能在寫這段程式碼時下意識裡都沒有去想讀記憶體這件事。

再想想你是怎樣讀磁碟檔案的?

char buf[1024];
int fd = open("/filepath/abc.txt");read(fd, buf, 1024);// 操作buf等等

看到了吧,讀寫磁碟檔案其實是一件很麻煩的事情,你需要open一個檔案,意思是告訴作業系統“Hey,作業系統,我要開始讀abc.txt這個檔案了,把這個檔案的所有資訊準備好,然後給我一個代號”。這個代號就是所謂的檔案描述符,拿到檔案描述符後你才能繼續接下來的讀寫操作。

為什麼麻煩

現在你應該看到了,操作磁碟檔案要比操作記憶體複雜很多,根本原因就在於定址方式不同。對記憶體來說我們可以直接按照位元組粒度去定址,但對磁碟上儲存的檔案來說則不是這樣的,磁碟上儲存的檔案是按照塊(block)的粒度來定址的,因此你必須先把磁碟中的檔案讀取到記憶體中,然後再按照位元組粒度來操作檔案內容。

你可能會想既然直接操作記憶體很簡單,那麼我們有沒有辦法像讀寫記憶體那樣去直接讀寫磁碟檔案呢?答案是肯定的。

要開腦洞了

對於像我們這樣在使用者態程式設計的程式設計師來說,記憶體在我們眼裡就是一段連續的空間。啊哈,巧了,磁碟上儲存的檔案在程式設計師眼裡也存放在一段連續的空間中(有的同學可能會說檔案其實是在磁碟上離散存放的,請注意,我們在這裡只從檔案使用者的角度來講)。

那麼這兩段空間有沒有辦法關聯起來呢?答案是肯定的,怎麼關聯呢?答案就是。。。。。。你猜對了嗎?答案是通過虛擬記憶體。關於虛擬記憶體我們已經講解過很多次了,虛擬記憶體就是假的地址空間,是程序看到的幻象,其目的是讓每個程序都認為自己獨佔記憶體,關於虛擬記憶體完整的詳細講解請參考博主的深入理解作業系統,關注公眾號碼農的荒島求生並回復作業系統即可。既然程序看到地址空間是假的那麼一切都好辦了。既然是假的,那麼就有做手腳的操作空間,怎麼做手腳呢?從普通程式設計師眼裡看檔案不是儲存在一段連續的磁碟空間上嗎?我們可以直接把這段空間對映到程序的記憶體中,就像這樣:

假設檔案長度是100位元組,我們把該檔案對映到了程序的記憶體中,地址是從600 ~ 800,那麼當你直接讀寫600 ~ 800這段記憶體時,實際上就是在直接操作磁碟檔案。這一切是怎麼做到呢?

魔術師作業系統

原來這一切背後的功勞是作業系統。當我們首次讀取600~800這段地址空間時,作業系統會檢測的這一操作,因為此時這段記憶體中什麼內容都還沒有,此時作業系統自己讀取磁碟檔案填充到這段記憶體空間中,此後程式就可以像讀記憶體一樣直接讀取磁碟內容了。寫操作也很簡單,使用者程式依然可以直接修改這塊記憶體,此後作業系統會在背後將修改內容寫回磁碟。現在你應該看到了,其實採用mmap這種方法磁碟依然還是按照塊的粒度來定址的,只不過在作業系統的一番騷操作下對於使用者態的程式來說“看起來”我們能像讀寫記憶體那樣直接讀寫磁碟檔案了,從按塊粒度定址到按照位元組粒度定址,這中間的差異就是作業系統來填補的。我想你現在應該大體明白mmap是什麼意思了。

接下來你肯定要問的問題就是,mmap有什麼好處呢?我為什麼要使用mmap?

記憶體copy與系統呼叫

我們常用的標準IO,也就是read/write其底層是涉及到系統呼叫的,同時當使用read/write讀寫檔案內容時,需要將資料從核心態copy到使用者態,修改完畢後再從使用者態copy到核心態,顯然,這些都是有開銷的。

而mmap則無此問題,基於mmap讀寫磁碟檔案不會招致系統呼叫以及額外的記憶體copy開銷,但mmap也不是完美的,mmap也有自己的缺點。其中一方面在於為了建立並維持地址空間與檔案的對映關係,核心中需要有特定的資料結構來實現這一對映,這當然是有效能開銷的,除此之外另一點就是缺頁問題,page fault。注意,缺頁中斷也是有開銷的,而且不同的核心由於內部的實現機制不同,其系統呼叫、資料copy以及缺頁處理的開銷也不同,因此就效能上來說我們不能肯定的說mmap就比標準IO好。這要看標準IO中的系統呼叫、記憶體呼叫的開銷與mmap方法中的缺頁中斷處理的開銷哪個更小,二手手機號碼買賣平臺地圖開銷小的一方將展現出更優異的效能。還是那句話,談到效能,單純的理論分析就不是那麼好用了,你需要基於真實的場景基於特定的作業系統以及硬體去測試才能有結論。

大檔案處理

到目前為止我想大家對mmap最直觀的理解就是可以像直接讀寫記憶體那樣來操作磁碟檔案,這是其中一個優點。另一個優點在於mmap其實是和作業系統中的虛擬記憶體密切相關的,這就為mmap帶來了一個很有趣的優勢。這個優勢在於處理大檔案場景,這裡的大檔案指的是檔案的大小超過你的實體記憶體,在這種場景下如果你使用傳統的read/write,那麼你必須一塊一塊的把檔案搬到記憶體,處理完檔案的一小部分再處理下一部分。這種需要在記憶體中開闢一塊空間——也就是我們常說的buffer,的方案聽上去就麻煩有沒有,而且還需要作業系統把資料從核心態copy到使用者態的buffer中。但如果用mmap情況就不一樣了,只要你的程序地址空間足夠大,可以直接把這個大檔案對映到你的程序地址空間中,即使該檔案大小超過實體記憶體也可以,這就是虛擬記憶體的巧妙之處了,當實體記憶體的空閒空間所剩無幾時虛擬記憶體會把你程序地址空間中不常用的部分扔出去,這樣你就可以繼續在有限的實體記憶體中處理超大檔案了,這個過程對程式設計師是透明的,虛擬記憶體都給你處理好了。關於虛擬記憶體的透徹講解請參考博主的深入理解作業系統,關注公眾號碼農的荒島求生並回復作業系統即可。注意,mmap與虛擬記憶體的結合在處理大檔案時可以簡化程式碼設計,但在效能上是否優於傳統的read/write方法就不一定了,還是那句話關於mmap與傳統IO在涉及到效能時你需要基於真實的應用場景測試。

使用mmap處理大檔案要注意一點,如果你的系統是32位的話,程序的地址空間就只有4G,這其中還有一部分預留給作業系統,因此在32位系統下可能不足以在你的程序地址空間中找到一塊連續的空間來對映該檔案,在64位系統下則無需擔心地址空間不足的問題,這一點要注意。

節省記憶體

這可能是mmap最大的優勢,以及最好的應用場景了。假設有一個檔案,很多程序的執行都依賴於此檔案,而且還是有一個假設,那就是這些程序是以只讀(read-only)的方式依賴於此檔案。你一定在想,這麼神奇?很多程序以只讀的方式依賴此檔案?有這樣的檔案嗎?答案是肯定的,這就是動態連結庫。要想弄清楚動態連結庫,我們就不得不從靜態庫說起。假設有三個程式A、B、C依賴一個靜態庫,那麼連結器在生成可執行程式A、B、C時會把該靜態庫copy到A、B、C中,就像這樣:

假設你本身要寫的程式碼只有2MB大小,但卻依賴了一個100MB的靜態庫,那麼最終生成的可執行程式就是102MB,儘管你本身的程式碼只有2MB。而且從圖中我們可以看出,可執行程式A、B、C中都有一部分靜態庫的副本,這裡面的內容是完全一樣的,那麼很顯然,這些可執行程式放在磁碟上會浪費磁碟空間,載入到記憶體中執行時會浪費記憶體空間。那麼該怎麼解決這個問題呢?很簡單,可執行程式A、B、C中為什麼都要各自儲存一份完全一樣的資料呢?其實我們只需要在可執行程式A、B、C中儲存一小點資訊,這點資訊裡記錄了依賴了哪個庫,那麼當可執行程式執行起來後再把相應的庫載入到記憶體中:

依然假設你本身要寫的程式碼只有2MB大小,此時依賴了一個100MB的動態連結庫,那麼最終生成的可執行程式就是2MB,儘管你依賴了一個100MB的庫。而且從圖中可以看出,此時可執行程式ABC中已經沒有冗餘資訊了,這不但節省磁碟空間,而且節省記憶體空間,讓有限的記憶體可以同時執行更多的程序,是不是很酷。現在我們已經知道了動態庫的妙用,但我們並沒有說明動態庫是怎麼節省記憶體的,接下來mmap就該登場了。你不是很多程序都依賴於同一個庫嘛,那麼我就用mmap把該庫直接對映到各個程序的地址空間中,儘管每個程序都認為自己地址空間中載入了該庫,但實際上該庫在記憶體中只有一份

mmap就這樣很神奇和動態連結庫聯動起來了,關於連結器以及靜態庫動態庫等更加詳細的講解你可以關注公眾號碼農的荒島求生並回復連結器即可。

想用好mmap沒那麼容易

現在你應該大體瞭解mmap,想用好mmap你必須對虛擬記憶體有一個較為透徹的理解,並且能對你的應用場景有一個透徹的理解,在使用mmap之前問問自己是不是還有更好的辦法,因此,對於新手來說並不推薦使用該機制。

總結

mmap在博主眼裡是一種很獨特的機制,這種機制最大的誘惑在於可以像讀寫記憶體樣方便的操作磁碟檔案,這簡直就像魔法一樣,因此在一些場景下可以簡化程式碼設計。但談到mmap的與標準IO(read/write)的效能情況就比較複雜了,標準IO設計到系統呼叫以及使用者態核心態的copy問題,而mmap則涉及到維持記憶體與磁碟檔案的對映關係以及缺頁處理的開銷,單純的從理論分析這二者半斤八兩,如果你的應用場景對效能要求較高,那麼你需要基於真實場景進行測試。我是小風哥,希望這篇文章對大家理解mmap有所幫助。