關鍵詞:mmap()、munmap()、msync()、SIGSEGV、SIGBUS、MAP_NORESERVE、MAP_FIXED、mremap()、remap_file_pages()等等。
1. 概述
mmap()系統調用在調用進程的虛擬地址空間中創建一個新內存映射。映射分為兩種:
- 文件映射:將一個文件的一部分直接映射到調用進程的虛擬內存中。一旦一個文件被映射之后就可以通過在相應的內存區域中操作字節來訪問文件內容了。映射的分頁會在需要的時候從文件中加載。
- 匿名映射:一個匿名映射沒有對應的文件,這種映射的分頁會被初始化為0。
一個進程的映射中的內存可以與其他進程中的映射共享:
- 當兩個進程映射了一個文件的同一個區域時他們會共享物理內存的相同分頁。
- 通過fork()創建的子進程會繼承其父進程的映射的副本,並且這些映射所引用的物理內存分頁與父進程中相應映射所引用的分頁相同。
關於私有映射和共享映射:
- 私有映射(MAP_PRIVATE):在映射內容上發生的變更對其他進程不可見,對於文件映射來講,變更將不會在底層文件上進行。初始時是共享的,但對影射內容所做出的變更對各個進程來講則是私有的。內核使用了寫時復制完成這個任務,當一個進程試圖修改一個分頁的內容是,內核首先會為該進程創建一個新分頁並將需修改的分頁中的內容復制到新分頁中。
- 共享映射(MAP_SHARED):在映射內容上發生的變更對所有共享同一個映射的其他進程都可見,對文件映射來講,變更將發生在底層的文件上。
以上四種不同內存映射的創建和使用方式如下:
- 私有文件映射:映射的內容被初始化為一個文件區域中的內容。多個映射同一個文件的的進程初始時會共享同樣的內存物理分頁,單系統使用寫時復制使得一個進程對映射所做出的變更對其他進程不可見。主要用途是使用一個文件的內容來初始化一塊內存區域。常見的例子包括根據二進制可執行文件或共享庫文件的相應部分來初始化一個進程的文本和數據段。
- 私有匿名映射:每次調用mmap()創建一個私有匿名映射時都會產生一個新映射,該映射與同一或不同進程創建的其他匿名映射是不同的,既不會共享物理內存分頁。私有匿名映射的主要用途是為一個進程分配新用零填充內存。
- 共享文件映射:所有映射一個文件的同一區域的進程會共享同樣的內存物理分頁,這些分頁的內容會被初始化為該文件區域。對映射內容的修改將直接在文件中進行。
- 共享匿名映射:每次調用mmap()創建一個共享匿名映射時都會產生一個新的、與任何其他映射不共享分頁的截然不同的映射。和私有匿名映射區別在於映射的分頁不會被寫時復制。for()的子進程繼承映射,父子進程共享同樣的RAM分頁,並且一個進程對應攝內容的變更會對其他進程可見。
一個進程在執行exec()時映射會丟失,但通過fork()的子進程會繼承映射,映射類型(MAP_PRIVATE或MAP_SHARED)也會被繼承。
2. 創建一個映射:mmap()
mmap()系統調用在進程的虛擬地址空間中創建一個新映射。
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); Returns starting address of mapping on success, or MAP_FAILED on error
addr:指定了映射被放置的虛擬地址。如果addr為NULL,那么內核會為映射選擇一個合適的地址。
length:指定了映射的字節數。內核會以分頁大小為單位來創建映射,實際上length會被向上提升為分頁大小的下一個倍數。
prot:是一個位掩碼,指定了施加於應設置上的保護信息,其取值要么是PROT_NONE,要么是另三個標記的組合。
flags:是一個控制映射操作各個方面選項的位掩碼。
MAP_PRIVATE:創建一個私有映射。區域中內容上所發生的變更對使用同一映射的其他進程是不可見的,對於文件映射來講,所發生的的變更將不會反應在底層文件上。
MAP_SHARED:創建一個共享映射。區域中內容上所發生的變更對使用MAP_SHARED特性映射同一區域的進程是可見的,對於文件映射來講,所發生的變更將直接反應在底層文件上。
fd和offset:用於文件映射,fd標識被映射文件的文件描述符;offset指定映射在文件中的起點,它必須是系統分頁大小的倍數。
成功時mmap()會返回新映射的起始地址,錯誤是mmap()會返回MAP_FAILED。MAP_FAILED等同於((void *)-1)。
下面的示例通過mmap()將一個文件映射,然后將其內容輸出到STDOUT。
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main(int argc, char *argv[]) { char *addr; int fd; struct stat sb; if (argc != 2 || strcmp(argv[1], "--help") == 0) printf("%s file\n", argv[0]); fd = open(argv[1], O_RDONLY); if (fd == -1) { printf("open failed"); return -1; } if (fstat(fd, &sb) == -1) { printf("fstat failed"); return -1; } if (sb.st_size == 0) exit(EXIT_SUCCESS); addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);-------將文件句柄fd全部內容以Read、Private映射到addr起始的映射區域。 if (addr == MAP_FAILED) { printf("mmap failed"); return -1; } if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size) {---------將映射區域全部內容輸出到STDOUT。 printf("partial/failed write"); return -1; } exit(EXIT_SUCCESS); }
3. 解除映射區域:munmap()
munmap()系統調研執行與mmap()相反操作,即從調用進程的虛擬地址空間中刪除一個映射。
#include <sys/mman.h> int munmap(void *addr, size_t length); Returns 0 on success, or –1 on error
addr:是待解除映射的地址范圍的起始地址。
length:是一個非負整數,指定了待解除映射區域的大小。范圍為系統分區頁大小的下一個倍數的地址空間將會被解除映射。
當一個進程終止或執行了一個exec()之后進程中所有的映射會自動被解除。
為確保一個共享文件映射的內容會被寫入到底層文件中,在使用munmap()解除一個映射之前需要調用msync()。
4. 文件映射
創建一個文件映射的步驟:
1. 獲取文件的一個描述符,通常通過open()來完成。
2. 將文件描述符作為fd參數傳入mmap()調用。
即可將打開文件的內容映射到調用進程的地址空間中,即使文件被關閉,也不會對映射產生影響。
fd引用文件時必須要具備與prot和flags參數值匹配的權限。
offset參數指定了從文件區域中的哪個字節開始映射,他必須是系統分頁大小的倍數。offset指定為0則從文件的起始位置開始映射。
length參數指定了映射的字節數。
4.1 私有文件映射
私有文件映射用途:
- 允許多個執行同一個程序或使用同一個共享庫的進程共享同樣的文本段,它是從底層可執行文件或庫文件的相應部分映射而來的。(盡管可執行文件文本段只允許讀取和執行訪問,但在被映射時仍然使用了MAP_PRIVATE,這是因為調試器或自修改的程序能夠修改程序文本,而這樣的變更不應該發生在底層文件上或影響其他進程。)
- 應設一個可執行文件或共享庫的初始化數據段。這種映射會被處理成私有是的對映射數據段內容的變更不會發生在底層文件上。
mmap()的這種用法通常對程序是不可見的,這些映射是由程序加載器和動態鏈接器創建的。
4.2 共享文件映射
多個進程創建了同一個文件區域的共享映射時,它們會共享同樣的物理內存分頁。
共享文件映射存在兩個用途:內存映射I/O和IPC。
內存映射I/O
用於共享文件映射中的內容是從文件初始化而來的,並且對映射內容所做出的變更都會自動反映到文件上,因此可以簡單地通過訪問內存中的字節來執行文件I/O,而依靠內核來確保對內存的變更會被傳遞到映射文件中。
內存映射I/O相對於read()/write()優勢
內存映射I/O具備兩個潛在的優勢:使用內存訪問來取代read()和write()系統調用能夠簡化一些應用程序邏輯;在一些情況下,它能夠比使用傳統的I/O系統調用執行文件I/O這種做法提供更好的性能。
內存映射I/O為什么會具有優勢?
正常的read()或者write()需要兩次傳輸:一次是在文件和內核高速緩沖區之間,另一次是在高速緩沖區和用戶空間緩沖區之間。使用mmap()則無需二次傳輸,對於輸入來講,一旦內核將相應文件塊映射金內存之后,用戶即可使用這些數據;對於輸出來講,用戶進程僅需要修改內存中內容,然后可依靠內核內存管理器來自動更新底層文件。
mmap()還能夠減少所需使用內存來提升性能。當使用read()或write()時,數據將被保存在兩個緩沖區中:一個位於用戶空間,一個位於內核空間。當使用mmap()時,內核空間和用戶空間會共享一個緩沖區。多個進程正在同一個文件上執行I/O,那么他們通過使用mmap()就能夠共享通過一個內核緩沖區,從而又能夠節省內存消耗。
內存映射I/O有什么劣勢?
對於小數據量I/O來講,內存映射I/O開銷,即映射、缺頁故障、講出映射以及更新硬件內存管理單元的超前轉換緩沖器,實際上比簡單的read()或write()大。
有時候內核難以高效地處理可寫入映射的回寫,需要借助msync()或sync_file_range()有助於提高效率。
使用共享文件映射的IPC
由於所有使用同樣文件區域的共享映射的進程共享同樣的內存物理分頁,因此共享文件映射第二個用途是作為一種IPC方法。
使用共享文件映射IPC和System V共享內存對象之間區別在於區域中內容上的變更會反應到映射文件上。
4.3 邊界情況
通常情況下,一個映射的大小是系統分頁大小整數倍,並且映射會完全落入映射文件的范圍之內。
映射完全落入映射文件范圍之內,但區域大小不是系統分頁大小整數倍
假設系統分頁大小為4096字節,被映射文件大小為9500字節,將文件首6000字節映射。
- 要求映射的6000字節,會被對齊到8192字節。
- 內存實際可以訪問范圍為0~8091,並且對齊修改會落實到實際文件中。
- 文件8192~9499區域沒有被映射,超出8191內存訪問會產生SIGSEGV異常。
擴充底層文件結尾映射
假設系統分頁大小為4096字節,被映射文件大小為2200字節,mmap()長度為8192。
- 要求映射的8192字節,被分為三部分:0~2199 - 映射到文件的可訪問部分,2200~4095 - 沒有映射到文件的可訪問部分,4096~8191 - 不可訪問部分。
- 映射到文件的可訪問部分,變更會更新到底層文件;沒有映射到文件的可訪問部分,被初始化為0,不會被映射到底層文件上,也不會與映射同一個文件的其他進程共享。
- 4096~8191范圍的不可訪問部分,對齊訪問會產生SIGBUS異常。
- 對8192~之后的地址訪問,同樣產生SIGSEGV異常。
4.4 內存保護和文件訪問模式交互
一般來講,PROT_READ和PROT_EXEC保護要求被影射的文件使用O_RDONLY或O_RDWR打開,而PROT_WRITE保護要求被映射的文件使用O_WRONLY或O_RDWR打開。
5. 同步映射區域:msync()
內核會自動將發生在MAP_SHARED映射內容上的變更寫入到底層文件中,但是不保證這種操作會在何時發生。
msync()系統調用讓應用程序能夠顯式控制何時完成共享映射和映射文件之間的同步。
調用msync()還允許一個 應用程序確保在可寫入映射上發生的更新會對在該文件上執行read()的其他進程可見。
#include <sys/mman.h> int msync(void *addr, size_t length, int flags); Returns 0 on success, or –1 on error
addr指定的地址必須是分頁對齊的。
length會被向上攝入到系統分頁大小下一個整數倍。
flags指定msync()同步方式。
MS_SYNC執行一個同步文件寫入。這個調動會阻塞知道內存區域中所有被修改過的分頁被寫入到文件為止。MS_ASYNC執行一個異步文件寫入。變更會在后面某個時刻被寫入磁盤並立即對在相應文件區域中執行read()的其他進程可見。
MS_SYNC操作之后,內存區域會與磁盤同步;MS_ASYNC操作之后,內存區域僅僅是與內核高速緩沖區同步。
MS_INVALIDATE使映射數據的緩存副本失效。當內存區域中所有被修改過的分頁被同步到文件中之后,內存區域中所有與底層文件不一致的分頁會被標記為無效。
6. 其他mmap()標記
7. 匿名映射
MAP_ANONYMOUS和/dev/zero
在Linux上,使用mmap()創建匿名映射存在梁紅不同但等價方法:
- flags指定MAP_ANONYMOUS並將fd指定為-1。
- 打開/dev/zero設備文件並將得到的文件描述符傳遞給mmap()。
這兩種映射得到的字節都會被初始化Wie0,並且offset都會被忽略。
MAP_PRIVATE匿名映射
MAP_SHARED匿名映射用來分配進程私有的內存塊並將其中的內容初始化為0.
MAP_SHARED匿名映射
MAP_SHARED匿名映射允許相關進程共享一塊內存區域而無需一個對應的映射文件。
8. 重新映射一個映射區域:mremap()
mreamap()系統個調用用來執行映射區域大小變更。
#define _GNU_SOURCE #include <sys/mman.h> void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...); Returns starting address of remapped region on success, or MAP_FAILED on error
old_address和old_size指定了需擴展或收縮的既有映射的位置和大小。old_address通常是一個由之前mmap()調用返回的值。
new_size是映射預期的新大小。
在執行重映射的過程中內核可能會為映射在進程的虛擬地址空間中重新指定一個位置,是否允許這種行為是由flags參數控制。flags要么是0,要么包含下列幾個值。
MREMAP_MAYMOVE:如果指定了標記,內核可能會為映射在進程的虛擬地址空間中重新指定一個位置。如果沒有指定,並且在當前位置處沒有足夠的空間來擴展這個映射,那么就返回ENOMEM錯誤。
MREMAP_FIXED:只能和MAP_MAYMOVE一起使用。如果指定了標記,那么mremap()會接收一個額外參數void *new_address,該參數指定了一個分頁對齊的地址,並且映射將會被遷移至該地址處。
mremap()在成功時會返回映射的起始地址。由於這個地址可能有變化,從而導致指向這個區域中的指針可能會變得無效,因此使用mremap()的應用程序在引用映射區域中的地址是應該只使用偏移量。
9. MAP_NORESERVE和過度利用交換空間
內核如何處理交換空間的預留是由調用mmap()時是否使用了MAP_NORESERVE標記以及影響系統層面的交換空間過度利用操作的/proc接口來控制的。
/proc/sys/vm/overcommit_memory包含一個整數值:0表示拒絕明顯的過度利用;1表示所有情況下都允許過度利用;2表示采用嚴格的過度利用。
overcommit_memory為2情況下,內核會在所有mmap()分配上執行嚴格的幾張並將系統中此類分配的總量控制在小於等於:
overcommit_ratio是一個整數-用百分比表示-它位於/proc/sys/vm/overcommit_ratio文件中。這個文件包含默認值是50,表示內核最多可分配的空間為系統RAM總量的50%。
過度利用監控只適用於下面映射:
私有可寫映射,這種映射的交換開銷等於所有使用該映射的進程為該映射所分配的空間總和。
共享匿名映射,這種映射的交換開銷等於映射的大小。
10. MAP_FIXED標記
mmap()的flags參數MAP_FIXED標記會強制內核原樣地解釋addr中的地址,addr必須是分頁對齊的。
如果在調用mmap()時指定了MAP_FIXED,並且內存區域的起始位置為addr,覆蓋的length字節與之前的映射的分頁重疊了,那么重疊的分頁會被新映射取代。使用這個特性可以可移植地將一個文件的多個部分映射進一塊連續的內存區域。
- 使用mmap()創建一個匿名映射,在mmap()調用將addr指定為NULL並不指定MAP_FIXED標記。
- 使用一系列指定了MAP_FIXED標記的mmap()調用來將文件區域映射進在上一步創建的映射的不同部分中。
11. 非線性映射:remap_file_pages()
使用mmap()創建的文件映射是連續的,映射文件的分頁與內存區域的分頁存在一個順序的、一對一的對應關系。
非線性映射文件分頁的順序與它們在連續內存中出現的順序不同的映射。
remap_file_pages()系統調用在無需創建多個vma情況下創建非線性映射:
- 使用mmap()創建一個映射。
- 使用一個或多個remap_file_pages()調用來調整內存分頁和文件分頁之間的對應關系。
#define _GNU_SOURCE #include <sys/mman.h> int remap_file_pages(void *addr, size_t size, int prot, size_t pgoff, int flags); Returns 0 on success, or –1 on error
pgoff指定了文件區域的起始位置,其單位是系統分頁大小。
size參數指定了文件區域的長度,其單位為字節。
addr參數起兩個作用,它標識了分頁需要調整的既有映射。addr必須是一個位於之前通過mmap()映射的區域中的地址;指定了通過pgoff和size標識出的文件分頁所處的內存地址。
addr和size都應該是系統分頁大小的整數倍。
prot參數會被忽略,其值必須是0.
flags參數當前未被使用。
remap_file_pages()僅適用於共享映射。
12. 總結
mmap()系統調用在調用進程的虛擬地址空間中創建一個新內存映射。munmap()系統調用執行你操作,僅從進程的地址空間中刪除一個映射。
映射可以分為兩種:基於文件的映射和匿名映射。文件映射將一個文件區域中的內容映射到進程的虛擬地址空間中。匿名映射並沒有對應的穩健趨於,該映射中的字節會被初始化為0.
映射既可以是私有的,也可以是共享的。對文件映射來講,這種差別確定了內核是否會將映射內容上發生的變更傳遞到底層文件上。使用MAP_PRIVATE,映射內容上發生的變更對其他進程是不可見的,也不會反應到映射文件上。MAP_SHARED文件映射的做法則相反,在映射上發生的變更對其他進程可見並且會反應到映射文件上。
msync()系統調用顯式地控制一個映射的內容何時與映射文件進行同步。
內存映射用途
- 分配進程私有的內存(私有匿名內存)
- 對一個進程的文本段和初始化數據段中的內容進行初始化(私有文件映射)
- 通過fork()關聯起來的進程之間的共享內存(共享匿名映射)
- 執行內存映射I/O,還可以將其與無關進程之間的內存共享結合起來(共享文件映射)
兩個信號:SIGSEGV和SIGBUS
如果在映射時違反了應設置上的保護規則(或訪問一個當前未被映射的地址),那么就會產生一個SIGSEGV信號。
對於基於文件的映射來講,如果訪問的映射部分在文件中沒有相關區域與之對應(即映射大於底層文件),那么就會產生一個SIGBUS信號。
使用MAP_NORESERVE標記可以控制每個mmap()調用的過度利用情況,而是用/proc文件則可以控制整個系統的過度利用情況。
mremap()系統調用允許調整一個既有映射的大小。remap_file_pages()系統調用允許創建非線性文件映射。