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 有所幫助。

關注公衆號 “碼農的荒島求生” 並回復 “資料” 閱讀全部核心原創

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bKq-b9Ga2IA2nbhi9weZtw