解鎖Linux內存映射:讓你的程序飛起來
在Linux的廣袤世界里,內存映射就像是一座橋梁,連接著磁盤上的文件與內存空間,是操作系統中至關重要的概念,在文件操作、進程間通信等諸多場景中發揮著不可替代的作用。無論是處理大數據量的文件讀寫,還是實現多個進程間的高效數據共享,內存映射都能展現出其獨特的優勢,為系統性能帶來質的飛躍 。
想象一下,當你需要處理一個超級大的文件時,如果按照傳統的文件 I/O 方式,數據在磁盤、內核緩沖區和用戶空間之間來回拷貝,不僅效率低下,還可能耗費大量的時間和系統資源。而內存映射則打破了這種繁瑣的流程,它讓你直接將文件映射到內存空間,就像文件數據已經在內存中一樣,你可以像操作內存一樣輕松地對文件進行讀寫,極大地提高了效率。
再比如,在多個進程需要共享數據的場景下,內存映射可以讓這些進程共享同一塊內存區域,實現數據的實時同步和高效交互,避免了復雜的數據傳遞和同步機制。接下來,就讓我們深入探索 Linux 內存映射的奧秘吧!
一、內存映射是什么
1.1定義剖析
內存映射,英文名為 Memory - mapped I/O,從字面意思理解,就是將磁盤文件的數據映射到內存中。在 Linux 系統中,這一機制允許進程把一個文件或者設備的數據關聯到內存地址空間,使得進程能夠像訪問內存一樣對文件進行操作 。舉個簡單的例子,假設有一個文本文件,通常我們讀取它時,會使用read函數,數據從磁盤先讀取到內核緩沖區,再拷貝到用戶空間。而內存映射則直接在進程的虛擬地址空間中為這個文件創建一個映射區域,進程可以直接通過指針訪問這個映射區域,就好像文件數據已經在內存中一樣,大大簡化了文件操作的流程 。
1.2工作原理大揭秘
內存映射的工作原理涉及到虛擬內存、頁表以及文件系統等多個方面的知識。當進程調用mmap函數進行內存映射時,大致會經歷以下幾個關鍵步驟 :
虛擬內存區域創建:系統首先在進程的虛擬地址空間中尋找一段滿足要求的連續空閑虛擬地址,然后為這段虛擬地址分配一個vm_area_struct結構,這個結構用于描述虛擬內存區域的各種屬性,如起始地址、結束地址、權限等,并將其插入到進程的虛擬地址區域鏈表或樹中 。就好比在一片空地上,規劃出一塊特定大小和用途的區域,并做好標記。
地址映射建立:通過待映射的文件指針,找到對應的文件描述符,進而鏈接到內核 “已打開文件集” 中該文件的文件結構體。再通過這個文件結構體,調用內核函數mmap,定位到文件磁盤物理地址,然后通過remap_pfn_range函數建立頁表,實現文件物理地址和進程虛擬地址的一一映射關系 。這一步就像是在規劃好的區域和實際的文件存儲位置之間建立起一條通道,讓數據能夠順利流通。不過,此時只是建立了地址映射,真正的數據還沒有拷貝到內存中 。
數據加載(缺頁異常處理):當進程首次訪問映射區域中的數據時,由于數據還未在物理內存中,會觸發缺頁異常。內核會捕獲這個異常,然后在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有找到,則調用nopage函數把所缺的頁從磁盤裝入到主存中 。這個過程就像是當你需要使用某個物品,但它不在身邊,你就需要去存放它的地方把它取回來。之后,進程就可以對這片主存進行正常的讀或寫操作,如果寫操作改變了數據內容,系統會在一定時間后自動將臟頁面回寫臟頁面到對應磁盤地址,完成寫入到文件的過程 。當然,也可以調用msync函數來強制同步,讓數據立即保存到文件里 。
二、內存映射機制
mmap內存映射的實現過程,總的來說可以分為三個階段:
①進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域
1、進程在用戶空間調用庫函數mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址
3、為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化
4、將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中
②調用內核空間的系統調用函數mmap(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
5、為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件相關各項信息。
6、通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數。
7、內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。
8、通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址并沒有任何數據關聯到主存中。
③進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
注:前兩個階段僅在于創建虛擬區間并完成地址映射,但是并沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。
9、進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。
10、缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程。
11、調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。
12、之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件里了。
3.1內存映射分類
(1)按文件分
文件映射:簡單來說,就是把文件的一個區間映射到進程的虛擬地址空間,數據源來自存儲設備上的文件。這種映射類型在很多場景中都有廣泛應用,比如當我們需要讀取一個大文件時,如果使用傳統的read函數,數據會先從磁盤讀取到內核緩沖區,再拷貝到用戶空間,這個過程涉及多次數據拷貝,效率較低 。而文件映射則直接將文件映射到進程的虛擬地址空間,進程可以像訪問內存一樣直接對文件進行讀寫操作 。
假設我們有一個數據庫文件,里面存儲著大量的數據。當數據庫管理系統需要讀取其中的數據時,可以通過文件映射將文件映射到內存中,這樣數據庫系統就可以直接在內存中快速查找和讀取數據,大大提高了數據訪問的速度。在進行文件映射時,通常會使用mmap函數,通過設置合適的參數,如文件描述符、映射長度、權限等,來實現文件到內存的映射 。
匿名映射:匿名映射與文件映射不同,它沒有文件支持,是直接將物理內存映射到進程的虛擬地址空間,沒有明確的數據源 。匿名映射通常用于需要分配一段臨時內存的場景,比如進程在運行過程中需要創建一些臨時的數據結構,這些數據不需要持久化存儲在文件中,就可以使用匿名映射來分配內存 。在 C 語言中,當我們使用malloc函數申請較大內存時(通常大于 128KB,這個閾值可能因系統而異),glibc庫的內存分配器ptmalloc會使用mmap進行匿名映射來向內核申請虛擬內存 。
在多線程編程中,當一個線程需要分配一些私有的臨時內存來存儲中間計算結果時,也可以使用匿名映射。匿名映射的特點是數據只存在于內存中,進程結束后,映射的內存會被自動回收,不會對磁盤文件產生任何影響 。在使用mmap函數進行匿名映射時,需要設置MAP_ANONYMOUS標志,同時文件描述符參數fd一般設置為 - 1 。
(2)按權限分
- 私有映射:寫時復制,變更不會再底層文件進行
- 共享映射:變更發生在底層文件
將上面兩兩組合:
- 私有文件映射:使用一個文件的內容來初始化一塊內存區域
- 私有匿名映射:為一個進程分配新的內存
- 共享文件映射:代替 read() 和 write() 、IPC
- 共享匿名映射:實現相關進程實現類似于共享內存
進程執行 exec() 時映射會丟失,但通過 fork() 的子進程會繼承映射
3.2API函數
(1)創建一個映射
#include <sys/mman.h>
void *mmap( void *addr, size_t length, int prot, int flags, int fd, off_t offset );
成功返回新映射的起始地址,失敗返回 MAP_FAILED。
參數 addr:映射被放置的虛擬地址,推薦為NULL(內核會自動選擇合適地址)
參數 length:映射的字節數
參數 prot:位掩碼,可以取OR
違反了保護信息,內核會向進程發送SIGSEGV信號。
- PROT_NONE:區域無法訪問,可以作為一個進程分配的起始位置或結束位置的守護分頁
- PROT_WRITE:區域內容可修改
- PROT_READ:區域內容可讀取
- PROT_EXEC:區域內容可執行
參數 flags:位掩碼,必須包含下列值中的一個
MAP_PROVATE:創建私有映射
MAP_SHARED:創建共享映射
參數 fd:被映射的文件的文件描述符(調用之后就能夠關閉文件描述符)。在打開描述符 fd 引用的文件時必須要具備與 prot 和 flags參數值匹配的權限。特別的,文件必須總是被打開允許讀取。
參數 offset:映射在文件中的起點
(2)解除映射區域
#include <sys/mman.h>
int munmap( void *addr, size_t length );
- 參數 addr:待解除映射的起始地址
- 參數 length:待解除映射區域的字節數
可以解除一個映射的部分映射,這樣原來的映射要么收縮,要么被分成兩個,這取決于在何處開始解除映射。還可以指定一個跨越多個映射的地址范圍,這樣的話所有在范圍內的映射都會被解除。
(3)同步映射區域
#include <sys/mman.h>
int msync( void *addr, size_t length, int flags );
參數 flags:
- MS_SYNC:阻塞直到內存區域中所有被修改過的分頁被寫入磁盤
- MS_ASYNC:在某個時刻被寫入磁盤
(4)重寫映射一個映射區域
#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap( void *old_address, size_t old_size, size_t new_size, int fflags, ... );
- 參數 old_address 和 old_size 指既有映射的位置和大小。
- 參數 new_size 指定新映射的大小
參數 flags:
0
MREMAP_MAYMOVE:為映射在進程的虛擬地址空間中重新指定一個位置
MREMAP_FIXED:配合 MREMAP_MAYMOVE 一起使用,mremap 會接收一個額外的參數 void *new_address
(5)創建私有文件映射
創建一個私有文件映射,并打印文件內容
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main( int argc, char **argv )
{
int fd = open( argv[1], O_RDONLY );
if( fd == -1 ) {
perror("open");
}
/*獲取文件信息*/
struct stat sb;
if( fstat( fd, &sb ) == -1 ) {
perror("fstat");
}
/*私有文件映射*/
char *addr = mmap( NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
if( addr == MAP_FAILED ) {
perror("mmap");
}
/*將addr的內容寫到標準輸出*/
if( write( STDOUT_FILENO, addr, sb.st_size ) != sb.st_size ) {
perror("write");
}
exit( EXIT_SUCCESS );
}
(6)創建共享匿名映射
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int main( int argc, char **argv )
{
/*獲取虛擬設備的文件描述符*/
int fd = open( "/dev/zero", O_RDWR );
if( fd == -1 ) {
perror("open");
}
int *addr = mmap( NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );
if( addr == MAP_FAILED ) {
perror("mmap");
}
if( close( fd ) == -1 ) {
perror("close");
}
*addr = 1;
switch( fork() ) {
case -1:
perror("fork");
break;
case 0:
printf("child *addr = %d\n", *addr);
(*addr)++;
/*解除映射*/
if( munmap(addr, sizeof(int)) == -1 ) {
perror("munmap");
}
_exit( EXIT_SUCCESS );
break;
default:
/*等待子進程結束*/
if( wait(NULL) == -1 ) {
perror("wait");
}
printf("parent *addr = %d\n", *addr );
if( munmap( addr, sizeof(int) ) == -1 ) {
perror("munmap");
}
exit( EXIT_SUCCESS );
break;
}
}
三、內存映射系統調用mmap
在 Linux 內存映射的實現過程中,mmap函數扮演著核心角色,它就像是一把神奇的鑰匙,能夠打開內存與文件之間的通道,讓我們可以輕松地進行內存映射操作 。
3.1mmap函數參數詳解
mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間后,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。
注:實際上,mmap()系統調用并不是完全為了用于共享內存而設計的。它本身提供了不同于一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而Posix或系統V的共享內存IPC則純粹用于共享目的,當然mmap()實現共享內存也是其主要應用之一。
mmap()系統調用形式如下:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
參數fd為即將映射到進程空間的文件描述字,一般由open()返回,同時,fd可以指定為-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用于具有親緣關系的進程間通信)。len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。prot 參數指定共享內存的訪問權限??扇∪缦聨讉€值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。
flags由以下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。offset參數一般設為0,表示從文件頭開始映射。參數addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值為最后文件映射到進程空間的地址,進程可直接操作起始地址為該值的有效地址。這里不再詳細介紹mmap()的參數,讀者可參考mmap()手冊頁獲得進一步的信息。
下面來詳細解讀一下各個參數的含義和作用 :
start:它是映射區的開始地址,當我們將其設置為NULL時,就意味著把選擇映射起始地址的權利交給了內核,內核會根據系統的實際情況,在進程地址空間中挑選一個合適的地址來建立映射 。比如在一個進程中,我們調用mmap函數并將start設為NULL,內核就會在該進程可用的虛擬地址空間里找到一段滿足條件的連續地址作為映射的起始點 。
length:這個參數表示映射區的長度,也就是我們希望將文件的多大區域映射到內存中,它決定了映射區域的大小 。假如我們有一個 10MB 的文件,而我們只想將其中的 1MB 映射到內存中進行操作,那么就可以將length設置為 1MB 對應的字節數 。
prot:它指定了期望的內存保護標志,這個標志不能與文件的打開模式產生沖突,常見的取值有以下幾種 :
- PROT_EXEC:表示映射的頁內容可以被執行,當我們映射的是一個可執行文件或者共享庫中的代碼段時,就需要設置這個標志 。比如在運行一個 C 語言程序時,程序中的可執行代碼部分被映射到內存中,就會設置PROT_EXEC標志,使得這些代碼能夠被 CPU 執行 。
- PROT_READ:意味著頁內容可以被讀取,這是最常用的標志之一,當我們需要讀取文件內容時,就會設置這個標志 。比如讀取一個文本文件的內容,就需要設置PROT_READ標志來允許對映射區域進行讀取操作 。
- PROT_WRITE:表示頁可以被寫入,如果我們想要對映射的文件進行修改,就需要設置這個標志 。例如我們打開一個文件進行讀寫操作,在調用mmap函數時就需要設置PROT_WRITE標志 。
- PROT_NONE:表示頁不可訪問,這種情況比較特殊,一般用于某些特定的內存管理場景,比如在隔離一些敏感數據區域時可能會用到 。
flags:該參數指定了映射對象的類型、映射選項以及映射頁是否可以共享,它的值可以是一個或者多個以下位的組合體 :
- MAP_SHARED:這個標志非常重要,它表示與其它所有映射這個對象的進程共享映射空間 。當一個進程對共享區進行寫入操作時,就相當于輸出到文件 。不過,直到調用msync函數或者munmap函數,文件實際上才會被更新 。在多進程協作處理同一個文件的場景中,就可以使用MAP_SHARED標志 。比如多個進程需要同時讀取和修改一個配置文件,通過設置MAP_SHARED標志,它們可以共享同一個映射空間,實現數據的實時同步 。
- MAP_PRIVATE:用于建立一個寫入時拷貝的私有映射,在這種映射方式下,內存區域的寫入不會影響到原文件 。這個標志和MAP_SHARED是互斥的,只能使用其中一個 。當我們希望對文件進行一些臨時的修改,而又不想影響原文件時,就可以使用MAP_PRIVATE標志 。比如在對一個文件進行臨時的分析和處理時,我們可以使用MAP_PRIVATE映射,對映射區域的修改不會改變原文件 。
- MAP_ANONYMOUS:表示匿名映射,即映射區不與任何文件關聯 。當我們需要分配一段臨時的內存空間,而不需要從文件中讀取數據或者將數據寫入文件時,就可以使用匿名映射 。比如在進行一些臨時的計算任務時,我們可以使用MAP_ANONYMOUS標志分配一塊內存來存儲中間結果 。
- MAP_FIXED:使用指定的映射起始地址,如果由start和length參數指定的內存區重疊于現存的映射空間,重疊部分將會被丟棄 。如果指定的起始地址不可用,操作將會失敗,并且起始地址必須落在頁的邊界上 。一般情況下,我們不建議使用這個標志,因為它可能會導致一些不可預測的問題,除非我們對內存布局有非常明確的需求 。
fd:它是有效的文件描述詞,用于標識要映射的文件 。當我們進行文件映射時,需要先使用open函數打開文件,然后將返回的文件描述符傳遞給mmap函數 。如果設置了MAP_ANONYMOUS標志,為了兼容問題,其值應為 - 1 。比如我們要映射一個名為test.txt的文件,首先使用open("test.txt", O_RDWR)打開文件,得到文件描述符fd,然后將fd傳遞給mmap函數進行映射操作 。
offset:表示被映射對象內容的起點,也就是從文件的哪個位置開始映射,這個值必須是分頁大小的整數倍 。在大多數情況下,我們會將其設置為 0,表示從文件的開頭開始映射 。比如文件的分頁大小是 4KB,如果我們想從文件的第 8KB 位置開始映射,那么offset就應該設置為 8KB 。
系統調用mmap()用于共享內存的兩種方式:
①使用普通文件提供的內存映射:適用于任何進程之間;此時,需要打開或創建一個文件,然后再調用mmap();典型調用代碼如下:
fd=open(name, flag, mode);
if(fd<0
...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通過mmap()實現共享內存的通信方式有許多特點和要注意的地方,我們將在范例中進行具體說明。
②使用特殊文件提供匿名內存映射:適用于具有親緣關系的進程之間;由于父子進程特殊的親緣關系,在父進程中先調用mmap(),然后調用fork()。那么在調用fork()之后,子進程繼承父進程匿名映射后的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區域進行通信了。注意,這里不是一般的繼承關系。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。
對于具有親緣關系的進程實現共享內存最好的方式應該是采用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標志即可,參見范例2。
系統調用munmap()
int munmap( void * addr, size_t len )
該調用在進程地址空間中解除一個映射關系,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生。
系統調用msync()
int msync ( void * addr , size_t len, int flags)
一般說來,進程在映射空間的對共享內容的改變并不直接寫回到磁盤文件中,往往在調用munmap()后才執行該操作??梢酝ㄟ^調用msync()實現磁盤上文件內容與共享內存區的內容一致。
3.2返回值解析
mmap函數的返回值也很關鍵,它能告訴我們映射操作是否成功 。當mmap成功執行時,會返回被映射區的指針,我們可以通過這個指針來訪問映射的內存區域 。例如:
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd = open("test.txt", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
size_t length = 1024; // 映射1024字節
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 使用ptr訪問映射區域
//...
// 解除映射
if (munmap(ptr, length) == -1) {
perror("munmap");
}
close(fd);
return 0;
}
在上述代碼中,如果mmap函數成功,ptr就會指向映射區的起始地址,我們可以通過ptr來對映射區域進行讀寫等操作 。而當mmap函數失敗時,會返回MAP_FAILED,其值為(void *)-1,同時errno會被設置為相應的錯誤代碼,以指示錯誤的原因 。
常見的錯誤原因包括:
- EACCES:表示訪問出錯,可能是因為權限不足,比如我們嘗試以寫權限映射一個只讀文件時,就會出現這個錯誤 。
- EBADF:意味著fd不是有效的文件描述詞,可能是文件沒有正確打開或者文件描述符已經被關閉 。
- EINVAL:表示一個或者多個參數無效,比如length為負數,或者offset不是分頁大小的整數倍等 。
- ENOMEM:表示內存不足,或者進程已超出最大內存映射數量,當系統內存緊張,無法為映射分配足夠的內存時,就會出現這個錯誤 。
3.3mmap系統調用和直接使用IPC共享內存之間的差異
mmap系統調用用于將文件映射到進程的地址空間中,而共享內存是一種不同的機制,用于進程間通信。這兩種方法都用于數據共享和高效的內存訪問,但它們有一些關鍵區別:
(1)數據源和持久化
- mmap: 通過 mmap 映射的數據通常來自文件系統中的文件。這意味著數據是持久化的——即使程序終止,文件中的數據依然存在。當你通過映射的內存區域修改數據時,這些更改最終會反映到磁盤上的文件中。
- 共享內存:共享內存是一塊匿名的(或者有時與特定文件關聯的)內存區域,它可以被多個進程訪問。與 mmap 映射的文件不同,共享內存通常是非持久的,即數據僅在計算機運行時存在,一旦系統關閉或重啟,存儲在共享內存中的數據就會丟失。
(2)使用場景
- mmap:mmap 特別適合于需要頻繁讀寫大文件的場景,因為它可以減少磁盤 I/O 操作的次數。它也允許文件的一部分被映射到內存中,這對于處理大型文件尤為有用。
- 共享內存:共享內存通常用于進程間通信(IPC),允許多個進程訪問相同的內存區域,這樣可以非常高效地在進程之間交換數據。
(3)性能和效率
- mmap:映射文件到內存可以提高文件訪問的效率,尤其是對于隨機訪問或頻繁讀寫的場景。系統可以利用虛擬內存管理和頁面緩存機制來優化訪問。
- 共享內存:共享內存提供了一種非??焖俚臄祿粨Q方式,因為所有的通信都在內存中進行,沒有文件 I/O 操作。
(4)同步和一致性
- mmap:使用 mmap 時,必須考慮到文件內容的同步問題。例如,使用 msync 調用來確保內存中的更改被同步到磁盤文件中。
- 共享內存:在共享內存的環境中,進程需要使用某種形式的同步機制(如信號量、互斥鎖)來避免競爭條件和數據不一致。
四、存儲映射I/O
在現在的項目中需要用到mmap建立內存映射文件,順便把存儲映射I/O看了一下,這個東西還真是加載索引的良好工具,存儲映射I/O可以使一個磁盤文件與存儲空間中的一個緩沖區相映射,這樣可以從緩沖區中讀取數據,就相當于讀文件中的相應字節,而當將數據存入緩沖區時,最后相應字節就自動寫入文件中。
利用mmap建立內存映射文件一般會分為兩條線:寫文件,讀文件,在分別介紹這兩條線之前首先將存儲映射I/O的常用函數介紹一下。
4.1存儲映射I/O基本函數
(1) mmap函數, 這個函數會告訴內核將一個給定的文件映射到一個存儲區域中,其函數原型為:
void* mmap(void *addr,size_t len,int prot,int flags,int fields,off_t off);
其中,參數addr用于指定存儲映射區的起始地址,通常設定為0,這表示由系統選擇該映射區的起始地址,參數len是指定映射的字節數,參數port指定映射區的方式,如PROT_READ,PROT_WRITE,值得注意的是映射區的保護不能超過文件open模式訪問權限。參數flags是設置映射區的屬性,一般設為MAP_SHARED,這一標志說明本進程的存儲操作相當于文件的write操作,參數fields是指定操作的文件描述符,參數off是要映射字節在文件中的起始偏移量。如果函數調用成功,函數的返回值是存儲映射區的起始地址;如果調用失敗,則返回MAP_FAILED。
(2) msync函數,這個函數會將存儲映射區的修改沖洗到被映射的文件中,其函數原型為:
int msync(void *addr,size_t len,int flags)
其中,參數flags參數設定如何控制沖洗存儲區,可以選擇MS_ASYNC,這表明是異步操作,函數調用立即返回,而選擇MS_SYNC,函數調用則等待寫操作完成后才會返回。
(3) munmap函數,這個函數會解除文件和存儲映射區之間的映射。
int munmap(caddr_t addr,size_t len)
4.2寫入映射緩沖區
當我們想向映射緩沖區中寫入數據時,首先需要確定映射文件的大小,在打開文件后,可以利用修改文件大小的函數重新設定文件的大小,接下來就可以對該緩沖區進行寫操作。
int fd = open(file_name,O_RDWR|O_CREAT);
ftruncate(fd,size);
mmap(0,size,PROT_WRITE,MAP_SHARED,fd,0);
4.3從映射緩沖區讀取
當我們想從映射緩沖區中讀取數據時,需要利用stat系列函數得到文件大小,進行利用在映射存儲區中打開該文件。
int fd = open(file_name,O_RDONLY);
struct stat stat_buf;
fstat(fd,&stat_buf);
void *data = mmap(0,stat_buf.st_size,PROT_READ,
MAP_SHARED,fd,0);
4.4實例:用存儲映射 I/O 復制文件
#include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>
#define COPYINCR (1024*1024*1024) /* 1 GB */
int
main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = 0;
if (argc != 3)
err_quit("usage: %s <fromfile> <tofile>", argv[0]);
if ((fdin = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s for reading", argv[1]);
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < 0)
err_sys("can't creat %s for writing", argv[2]);
if (fstat(fdin, &sbuf) < 0) /* need size of input file */
err_sys("fstat error");
if (ftruncate(fdout, sbuf.st_size) < 0) /* set output file size */
err_sys("ftruncate error");
while (fsz < sbuf.st_size) {
if ((sbuf.st_size - fsz) > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz;
if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED,
fdin, fsz)) == MAP_FAILED)
err_sys("mmap error for input");
if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,
MAP_SHARED, fdout, fsz)) == MAP_FAILED)
err_sys("mmap error for output");
memcpy(dst, src, copysz); /* does the file copy */
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
}
exit(0);
}
五、內存映射的應用場景
5.1大文件處理
在當今數據爆炸的時代,處理大文件是許多應用場景中不可避免的挑戰。無論是數據分析、多媒體處理還是日志管理,大文件的讀寫操作都對系統性能提出了極高的要求 。傳統的文件I/O方式在面對大文件時往往顯得力不從心,頻繁的磁盤 I/O 操作會導致系統性能大幅下降,而內存映射技術則為大文件處理提供了一種高效的解決方案 。
內存映射之所以能顯著提升大文件處理效率,關鍵在于它減少了數據的讀寫次數。以一個 1GB 的日志文件為例,假設我們需要統計其中特定關鍵詞出現的次數。如果使用傳統的read函數逐塊讀取文件內容到用戶空間進行處理,每讀取一次都涉及數據從磁盤到內核緩沖區,再到用戶空間的拷貝過程 。
而采用內存映射,文件直接被映射到進程的虛擬地址空間,進程可以像訪問內存一樣直接讀取文件內容,避免了數據在不同緩沖區之間的多次拷貝,大大減少了 I/O 操作的開銷 。并且,內存映射還可以利用操作系統的頁緩存機制,當進程訪問映射區域中的數據時,如果數據已經在頁緩存中,就可以直接從內存中讀取,無需再次訪問磁盤,進一步提高了數據訪問的速度 。
5.2進程間通信
在多進程編程的世界里,進程間通信(IPC)是實現不同進程之間數據交互和協作的關鍵。內存映射為進程間通信提供了一種高效且直接的方式,通過映射同一文件或匿名內存,不同進程可以共享同一塊內存區域,實現數據的實時共享和交互 。
以一個簡單的生產者 - 消費者模型為例,生產者進程和消費者進程需要共享一個數據緩沖區。我們可以通過內存映射創建一個共享的內存區域,生產者進程將數據寫入這個共享區域,消費者進程則從該區域讀取數據 。在這個過程中,內存映射利用了操作系統的虛擬內存機制,使得不同進程的虛擬地址可以映射到相同的物理內存頁,從而實現數據的共享 。
并且,為了保證數據的一致性和同步性,通常會結合信號量、互斥鎖等同步機制來協調不同進程對共享內存的訪問 。比如,生產者在向共享內存寫入數據前,先獲取互斥鎖,防止其他進程同時寫入;寫入完成后,釋放互斥鎖,并發送信號量通知消費者有新數據可用 。這樣,通過內存映射和同步機制的配合,不同進程可以高效、安全地進行數據共享和通信 。
5.3動態庫加載
在 Linux 系統中,動態庫是一種重要的代碼共享機制,它允許多個程序共享同一份代碼和數據,從而節省內存空間和磁盤空間 。內存映射在動態庫加載過程中扮演著至關重要的角色,它將動態庫的代碼段和數據段映射到進程的虛擬地址空間,使得進程能夠高效地訪問動態庫中的函數和變量 。
當一個可執行程序依賴于某個動態庫時,在程序啟動階段,系統會通過內存映射將動態庫加載到內存中。具體來說,動態鏈接器(如ld.so)會首先解析可執行文件的依賴關系,找到需要加載的動態庫 。然后,它使用內存映射將動態庫的代碼段映射到進程的虛擬地址空間,并設置相應的權限,如代碼段通常設置為只讀和可執行權限 。對于動態庫的數據段,也會根據其屬性進行映射,如全局變量所在的數據段可能設置為可讀寫權限 。
在映射過程中,動態鏈接器還會處理動態庫中的符號表,將可執行文件中的符號引用與動態庫中的實際函數和變量地址進行綁定,確保程序在運行時能夠正確地調用動態庫中的功能 。通過內存映射加載動態庫,不僅提高了程序的啟動速度,還實現了代碼的共享,多個進程可以共享同一個動態庫的內存映射,減少了內存的占用 。
六、實戰演練:代碼中的內存映射
6.1簡單文件讀寫示例
下面是一個使用mmap函數進行文件讀寫的簡單示例代碼:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *filepath = "example.txt";
// 打開文件
int fd = open(filepath, O_RDWR);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
// 獲取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return EXIT_FAILURE;
}
// 將文件映射到內存
char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 現在可以像操作普通內存一樣訪問文件內容
printf("File contents before modification:\n%s\n", mapped);
// 修改文件內容
strcpy(mapped, "Hello, mmap!");
// 解除映射
if (munmap(mapped, sb.st_size) == -1) {
perror("munmap");
}
// 關閉文件
close(fd);
return EXIT_SUCCESS;
}
代碼解釋:
- 打開文件:使用open函數打開名為example.txt的文件,O_RDWR標志表示以讀寫模式打開文件。如果打開失敗,通過perror函數打印錯誤信息并返回。
- 獲取文件大小:利用fstat函數獲取文件的相關信息,包括文件大小,將結果存儲在sb結構體中。若獲取失敗,同樣打印錯誤信息并關閉文件返回。
- 內存映射:調用mmap函數將文件映射到內存中。NULL表示讓系統自動選擇映射的起始地址,sb.st_size指定映射的長度為文件的大小,PROT_READ | PROT_WRITE表示映射區域具有可讀可寫權限,MAP_SHARED表示映射區域的修改會同步到文件,fd是前面打開文件返回的文件描述符,0表示從文件開頭開始映射。如果映射失敗,打印錯誤信息并關閉文件返回。
- 訪問和修改映射區域:通過mapped指針可以像操作普通內存一樣訪問和修改文件內容。這里使用strcpy函數將字符串Hello, mmap!復制到映射區域,從而修改了文件的內容。
- 解除映射:使用munmap函數解除內存映射,參數為映射的起始地址mapped和映射長度sb.st_size。如果解除失敗,打印錯誤信息。
- 關閉文件:最后使用close函數關閉文件。
6.2進程間通信示例
以下是通過內存映射實現進程間通信的示例代碼,這里以父子進程為例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
int fd;
char *shared_memory;
pid_t pid;
// 創建一個臨時文件用于內存映射
fd = open("temp_file", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
// 拓展文件大小
if (lseek(fd, SHM_SIZE - 1, SEEK_SET) == -1) {
perror("lseek");
close(fd);
return EXIT_FAILURE;
}
// 寫入一個字節,使文件大小達到SHM_SIZE
if (write(fd, "", 1) != 1) {
perror("write");
close(fd);
return EXIT_FAILURE;
}
// 將文件映射到內存
shared_memory = (char *)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 創建子進程
pid = fork();
if (pid == -1) {
perror("fork");
munmap(shared_memory, SHM_SIZE);
close(fd);
return EXIT_FAILURE;
} else if (pid == 0) {
// 子進程
strcpy(shared_memory, "Hello from child!");
_exit(EXIT_SUCCESS);
} else {
// 父進程
wait(NULL);
printf("Data read from shared memory: %s\n", shared_memory);
wait(NULL);
}
// 解除映射
if (munmap(shared_memory, SHM_SIZE) == -1) {
perror("munmap");
}
// 關閉文件
close(fd);
// 刪除臨時文件
if (unlink("temp_file") == -1) {
perror("unlink");
}
return EXIT_SUCCESS;
}
關鍵步驟分析:
- 創建臨時文件并拓展大?。菏褂胦pen函數創建一個名為temp_file的臨時文件,O_RDWR | O_CREAT | O_TRUNC標志表示以讀寫模式創建文件,如果文件存在則截斷。然后通過lseek函數將文件指針移動到SHM_SIZE - 1的位置,再使用write函數寫入一個字節,使文件大小達到SHM_SIZE,為后續的內存映射做準備。
- 內存映射:調用mmap函數將臨時文件映射到內存中,得到一個指向共享內存區域的指針shared_memory。
- 創建子進程:使用fork函數創建子進程。子進程和父進程會共享這個內存映射區域。
- 子進程操作:在子進程中,使用strcpy函數將字符串Hello from child!復制到共享內存區域,然后調用_exit函數退出子進程。
- 父進程操作:父進程通過wait函數等待子進程結束,然后從共享內存區域讀取數據并打印。
- 清理資源:最后,父進程解除內存映射,關閉文件,并刪除臨時文件,釋放相關資源。
6.3分塊內存映射處理大文件示例
(1)內存映射文件可以用于3個不同的目的:
- 系統使用內存映射文件,以便加載和執行. exe和DLL文件。這可以大大節省頁文件空間和應用程序啟動運行所需的時間。
- 可以使用內存映射文件來訪問磁盤上的數據文件。這使你可以不必對文件執行I/O操作,并且可以不必對文件內容進行緩存。
- 可以使用內存映射文件,使同一臺計算機上運行的多個進程能夠相互之間共享數據。Windows確實提供了其他一些方法,以便在進程之間進行數據通信,但是這些方法都是使用內存映射文件來實現的,這使得內存映射文件成為單個計算機上的多個進程互相進行通信的最有效的方法。
(2)使用內存映射數據文件
若要使用內存映射文件,必須執行下列操作步驟:
- 1) 創建或打開一個文件內核對象,該對象用于標識磁盤上你想用作內存映射文件的文件。
- 2) 創建一個文件映射內核對象,告訴系統該文件的大小和你打算如何訪問該文件。
- 3) 讓系統將文件映射對象的全部或一部分映射到你的進程地址空間中。
當完成對內存映射文件的使用時,必須執行下面這些步驟將它清除:
- 1) 告訴系統從你的進程的地址空間中撤消文件映射內核對象的映像。
- 2) 關閉文件映射內核對象。
- 3) 關閉文件內核對象。
文件操作是應用程序最為基本的功能之一,Win32 API和MFC均提供有支持文件處理的函數和類,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile類等。一般來說,以上這些函數可以滿足大多數場合的要求,但是對于某些特殊應用領域所需要的動輒幾十GB、幾百GB、乃至幾TB的海量存儲,再以通常的文件處理方法進行處理顯然是行不通的。所以可以使用內存文件映射來處理數據,網上也有鋪天蓋地的文章,但是映射大文件的時候又往往會出錯,需要進行文件分塊內存映射,這里就是這樣的一個例子,教你如何把文件分塊映射到內存。
//
// 該函數用于讀取從CCD攝像頭采集來的RAW視頻數據當中的某一幀圖像,
// RAW視頻前596字節為頭部信息,可以從其中讀出視頻總的幀數,
// 幀格式為1024*576*8
/*
參數:
pszPath:文件名
dwFrame: 要讀取第幾幀,默認讀取第2幀
*/
BOOL MyFreeImage::LoadXRFrames(TCHAR *pszPath, DWORD dwFrame/* = 2*/ )
{
// get the frames of X-Ray frames
BOOL bLoop = TRUE;
int i;
int width = 1024;
int height = 576;
int bitcount = 8; //1, 4, 8, 24, 32
//
//Build bitmap header
BITMAPFILEHEADER bitmapFileHeader;
BITMAPINFOHEADER bitmapInfoHeader;
BYTE rgbquad[4]; // RGBQUAD
int index = 0;
DWORD widthbytes = ((bitcount*width + 31)/32)*4; //每行都是4的倍數 DWORD的倍數 這里是 576-
TRACE1("widthbytes=%d\n", widthbytes);
switch(bitcount) {
case 1:
index = 2;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 2*4);
break;
case 4:
index = 16;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 16*4);
break;
case 8:
index = 256;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 256*sizeof(RGBQUAD));
break;
case 24:
case 32:
index = 0;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER));
break;
default:
break;
}
//構造Bitmap文件頭BITMAPFILEHEADER
bitmapFileHeader.bfType = 0x4d42; // 很重要的標志位 BM 標識
bitmapFileHeader.bfSize = (DWORD)(bitmapFileHeader.bfOffBits + height * widthbytes); //bmp文件長度
bitmapFileHeader.bfReserved1 = 0;
bitmapFileHeader.bfReserved2 = 0;
//構造Bitmap文件信息頭BITMAPINFOHEADER
bitmapInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfoHeader.biWidth = width;
bitmapInfoHeader.biHeight = height;
bitmapInfoHeader.biPlanes = 1;
bitmapInfoHeader.biBitCount = bitcount;
bitmapInfoHeader.biCompression = BI_RGB; // 未壓縮
bitmapInfoHeader.biSizeImage = height * widthbytes;
bitmapInfoHeader.biXPelsPerMeter = 3780;
bitmapInfoHeader.biYPelsPerMeter = 3780;
bitmapInfoHeader.biClrUsed = 0;
bitmapInfoHeader.biClrImportant = 0;
//創建BMP內存映像,寫入位圖頭部
BYTE *pMyBmp = new BYTE[bitmapFileHeader.bfSize]; // 我的位圖pMyBmp
BYTE *curr = pMyBmp; // curr指針指示pMyBmp的位置
memset(curr, 0, bitmapFileHeader.bfSize);
//寫入頭信息
memcpy(curr, &bitmapFileHeader,sizeof(BITMAPFILEHEADER));
curr = pMyBmp + sizeof(BITMAPFILEHEADER);
memcpy(curr, &bitmapInfoHeader,sizeof(BITMAPINFOHEADER));
curr += sizeof(BITMAPINFOHEADER);
//構造調色板 , 當像素大于8位時,就沒有調色板了。
if(bitcount == 8)
{
rgbquad[3] = 0; //rgbReserved
for(i = 0; i < index; i++)
{
rgbquad[0] = rgbquad[1] = rgbquad[2] = i;
memcpy(curr, rgbquad, sizeof(RGBQUAD));
curr += sizeof(RGBQUAD);
}
}else if(bitcount == 1)
{
rgbquad[3] = 0; //rgbReserved
for(i = 0; i < index; i++)
{
rgbquad[0] = rgbquad[1] = rgbquad[2] = (256 - i)%256;
memcpy(curr, rgbquad, sizeof(RGBQUAD));
curr += sizeof(RGBQUAD);
}
}
//
// 文件映射,從文件中查找圖像的數據
//Open the real file on the file system
HANDLE hFile = CreateFile(pszPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to CreateFile, Error:%d\n"), dwError);
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
//Create the file mapping object
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
if (hMapping == NULL)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to CreateFileMapping, Error:%d\n"), dwError);
// Close handle
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
// Retrieve allocation granularity
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;
// Retrieve file size
// Retrieve file size
DWORD dwFileSizeHigh;
__int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
qwFileSize |= (((__int64)dwFileSizeHigh) << 32);
CloseHandle(hFile);
// Read Image
__int64 qwFileOffset = 0; // 偏移地址
DWORD dwBytesInBlock = 0, // 映射的塊大小
dwStandardBlock = 100* dwAllocationGranularity ; // 標準塊大小
DWORD dwFrameSize = height*width; // 計算一幀圖像的數據量,不包括頭部信息
DWORD dwCurrentFrame = 1;
dwBytesInBlock = dwStandardBlock;
if (qwFileSize < dwStandardBlock)
dwBytesInBlock = (DWORD)qwFileSize;
//Map the view
LPVOID lpData = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,
static_cast<DWORD>((qwFileOffset & 0xFFFFFFFF00000000) >> 32), static_cast<DWORD>(qwFileOffset & 0xFFFFFFFF), dwBytesInBlock);
if (lpData == NULL)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to MapViewOfFile, Error:%d\n"), dwError);
// Close Handle
if (hMapping != NULL)
{
CloseHandle(hMapping);
hMapping = NULL;
}
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
BYTE *lpBits = (BYTE *)lpData;
BYTE *curr1, *curr2, *lpEnd;
curr1 = lpBits; // seek to start
curr2 = lpBits + 596; // seek to first frame
lpEnd = lpBits + dwBytesInBlock; // seek to end
// Read video infomation
KMemDataStream streamData( curr1, dwBytesInBlock);
ReadXRHeader(streamData);
while(bLoop)
{
DWORD dwTmp = lpEnd - curr2; //內存緩沖剩余的字節
if ( dwTmp >= dwFrameSize )
{
if(dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwFrameSize);
bLoop = FALSE;
}
curr2 += dwFrameSize;
}else //內存中不夠一幀數據
{
DWORD dwTmp2 = dwFrameSize - dwTmp; // 一副完整的幀還需要dwTmp2字節
if (dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwTmp);
curr += dwTmp;
}
//1、首先計算文件的偏移位置
qwFileOffset += dwBytesInBlock;