解鎖Linux內(nèi)存映射:讓你的程序飛起來
在Linux的廣袤世界里,內(nèi)存映射就像是一座橋梁,連接著磁盤上的文件與內(nèi)存空間,是操作系統(tǒng)中至關(guān)重要的概念,在文件操作、進(jìn)程間通信等諸多場景中發(fā)揮著不可替代的作用。無論是處理大數(shù)據(jù)量的文件讀寫,還是實(shí)現(xiàn)多個(gè)進(jìn)程間的高效數(shù)據(jù)共享,內(nèi)存映射都能展現(xiàn)出其獨(dú)特的優(yōu)勢,為系統(tǒng)性能帶來質(zhì)的飛躍 。
想象一下,當(dāng)你需要處理一個(gè)超級大的文件時(shí),如果按照傳統(tǒng)的文件 I/O 方式,數(shù)據(jù)在磁盤、內(nèi)核緩沖區(qū)和用戶空間之間來回拷貝,不僅效率低下,還可能耗費(fèi)大量的時(shí)間和系統(tǒng)資源。而內(nèi)存映射則打破了這種繁瑣的流程,它讓你直接將文件映射到內(nèi)存空間,就像文件數(shù)據(jù)已經(jīng)在內(nèi)存中一樣,你可以像操作內(nèi)存一樣輕松地對文件進(jìn)行讀寫,極大地提高了效率。
再比如,在多個(gè)進(jìn)程需要共享數(shù)據(jù)的場景下,內(nèi)存映射可以讓這些進(jìn)程共享同一塊內(nèi)存區(qū)域,實(shí)現(xiàn)數(shù)據(jù)的實(shí)時(shí)同步和高效交互,避免了復(fù)雜的數(shù)據(jù)傳遞和同步機(jī)制。接下來,就讓我們深入探索 Linux 內(nèi)存映射的奧秘吧!
一、內(nèi)存映射是什么
1.1定義剖析
內(nèi)存映射,英文名為 Memory - mapped I/O,從字面意思理解,就是將磁盤文件的數(shù)據(jù)映射到內(nèi)存中。在 Linux 系統(tǒng)中,這一機(jī)制允許進(jìn)程把一個(gè)文件或者設(shè)備的數(shù)據(jù)關(guān)聯(lián)到內(nèi)存地址空間,使得進(jìn)程能夠像訪問內(nèi)存一樣對文件進(jìn)行操作 。舉個(gè)簡單的例子,假設(shè)有一個(gè)文本文件,通常我們讀取它時(shí),會使用read函數(shù),數(shù)據(jù)從磁盤先讀取到內(nèi)核緩沖區(qū),再拷貝到用戶空間。而內(nèi)存映射則直接在進(jìn)程的虛擬地址空間中為這個(gè)文件創(chuàng)建一個(gè)映射區(qū)域,進(jìn)程可以直接通過指針訪問這個(gè)映射區(qū)域,就好像文件數(shù)據(jù)已經(jīng)在內(nèi)存中一樣,大大簡化了文件操作的流程 。
1.2工作原理大揭秘
內(nèi)存映射的工作原理涉及到虛擬內(nèi)存、頁表以及文件系統(tǒng)等多個(gè)方面的知識。當(dāng)進(jìn)程調(diào)用mmap函數(shù)進(jìn)行內(nèi)存映射時(shí),大致會經(jīng)歷以下幾個(gè)關(guān)鍵步驟 :
虛擬內(nèi)存區(qū)域創(chuàng)建:系統(tǒng)首先在進(jìn)程的虛擬地址空間中尋找一段滿足要求的連續(xù)空閑虛擬地址,然后為這段虛擬地址分配一個(gè)vm_area_struct結(jié)構(gòu),這個(gè)結(jié)構(gòu)用于描述虛擬內(nèi)存區(qū)域的各種屬性,如起始地址、結(jié)束地址、權(quán)限等,并將其插入到進(jìn)程的虛擬地址區(qū)域鏈表或樹中 。就好比在一片空地上,規(guī)劃出一塊特定大小和用途的區(qū)域,并做好標(biāo)記。
地址映射建立:通過待映射的文件指針,找到對應(yīng)的文件描述符,進(jìn)而鏈接到內(nèi)核 “已打開文件集” 中該文件的文件結(jié)構(gòu)體。再通過這個(gè)文件結(jié)構(gòu)體,調(diào)用內(nèi)核函數(shù)mmap,定位到文件磁盤物理地址,然后通過remap_pfn_range函數(shù)建立頁表,實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系 。這一步就像是在規(guī)劃好的區(qū)域和實(shí)際的文件存儲位置之間建立起一條通道,讓數(shù)據(jù)能夠順利流通。不過,此時(shí)只是建立了地址映射,真正的數(shù)據(jù)還沒有拷貝到內(nèi)存中 。
數(shù)據(jù)加載(缺頁異常處理):當(dāng)進(jìn)程首次訪問映射區(qū)域中的數(shù)據(jù)時(shí),由于數(shù)據(jù)還未在物理內(nèi)存中,會觸發(fā)缺頁異常。內(nèi)核會捕獲這個(gè)異常,然后在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有找到,則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中 。這個(gè)過程就像是當(dāng)你需要使用某個(gè)物品,但它不在身邊,你就需要去存放它的地方把它取回來。之后,進(jìn)程就可以對這片主存進(jìn)行正常的讀或?qū)懖僮鳎绻麑懖僮鞲淖兞藬?shù)據(jù)內(nèi)容,系統(tǒng)會在一定時(shí)間后自動(dòng)將臟頁面回寫臟頁面到對應(yīng)磁盤地址,完成寫入到文件的過程 。當(dāng)然,也可以調(diào)用msync函數(shù)來強(qiáng)制同步,讓數(shù)據(jù)立即保存到文件里 。
二、內(nèi)存映射機(jī)制
mmap內(nèi)存映射的實(shí)現(xiàn)過程,總的來說可以分為三個(gè)階段:
①進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
1、進(jìn)程在用戶空間調(diào)用庫函數(shù)mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
3、為此虛擬區(qū)分配一個(gè)vm_area_struct結(jié)構(gòu),接著對這個(gè)結(jié)構(gòu)的各個(gè)域進(jìn)行了初始化
4、將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹中
②調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
5、為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應(yīng)的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file),每個(gè)文件結(jié)構(gòu)體維護(hù)著和這個(gè)已打開文件相關(guān)各項(xiàng)信息。
6、通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數(shù)。
7、內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
8、通過remap_pfn_range函數(shù)建立頁表,即實(shí)現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。此時(shí),這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中。
③進(jìn)程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
注:前兩個(gè)階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r(shí)。
9、進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因?yàn)槟壳爸唤⒘说刂酚成?,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。
10、缺頁異常進(jìn)行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。
11、調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
12、之后進(jìn)程即可對這片主存進(jìn)行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時(shí)間后系統(tǒng)會自動(dòng)回寫臟頁面到對應(yīng)磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件中,而是有一段時(shí)間的延遲,可以調(diào)用msync()來強(qiáng)制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。
3.1內(nèi)存映射分類
(1)按文件分
文件映射:簡單來說,就是把文件的一個(gè)區(qū)間映射到進(jìn)程的虛擬地址空間,數(shù)據(jù)源來自存儲設(shè)備上的文件。這種映射類型在很多場景中都有廣泛應(yīng)用,比如當(dāng)我們需要讀取一個(gè)大文件時(shí),如果使用傳統(tǒng)的read函數(shù),數(shù)據(jù)會先從磁盤讀取到內(nèi)核緩沖區(qū),再拷貝到用戶空間,這個(gè)過程涉及多次數(shù)據(jù)拷貝,效率較低 。而文件映射則直接將文件映射到進(jìn)程的虛擬地址空間,進(jìn)程可以像訪問內(nèi)存一樣直接對文件進(jìn)行讀寫操作 。
假設(shè)我們有一個(gè)數(shù)據(jù)庫文件,里面存儲著大量的數(shù)據(jù)。當(dāng)數(shù)據(jù)庫管理系統(tǒng)需要讀取其中的數(shù)據(jù)時(shí),可以通過文件映射將文件映射到內(nèi)存中,這樣數(shù)據(jù)庫系統(tǒng)就可以直接在內(nèi)存中快速查找和讀取數(shù)據(jù),大大提高了數(shù)據(jù)訪問的速度。在進(jìn)行文件映射時(shí),通常會使用mmap函數(shù),通過設(shè)置合適的參數(shù),如文件描述符、映射長度、權(quán)限等,來實(shí)現(xiàn)文件到內(nèi)存的映射 。
匿名映射:匿名映射與文件映射不同,它沒有文件支持,是直接將物理內(nèi)存映射到進(jìn)程的虛擬地址空間,沒有明確的數(shù)據(jù)源 。匿名映射通常用于需要分配一段臨時(shí)內(nèi)存的場景,比如進(jìn)程在運(yùn)行過程中需要?jiǎng)?chuàng)建一些臨時(shí)的數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)不需要持久化存儲在文件中,就可以使用匿名映射來分配內(nèi)存 。在 C 語言中,當(dāng)我們使用malloc函數(shù)申請較大內(nèi)存時(shí)(通常大于 128KB,這個(gè)閾值可能因系統(tǒng)而異),glibc庫的內(nèi)存分配器ptmalloc會使用mmap進(jìn)行匿名映射來向內(nèi)核申請?zhí)摂M內(nèi)存 。
在多線程編程中,當(dāng)一個(gè)線程需要分配一些私有的臨時(shí)內(nèi)存來存儲中間計(jì)算結(jié)果時(shí),也可以使用匿名映射。匿名映射的特點(diǎn)是數(shù)據(jù)只存在于內(nèi)存中,進(jìn)程結(jié)束后,映射的內(nèi)存會被自動(dòng)回收,不會對磁盤文件產(chǎn)生任何影響 。在使用mmap函數(shù)進(jìn)行匿名映射時(shí),需要設(shè)置MAP_ANONYMOUS標(biāo)志,同時(shí)文件描述符參數(shù)fd一般設(shè)置為 - 1 。
(2)按權(quán)限分
- 私有映射:寫時(shí)復(fù)制,變更不會再底層文件進(jìn)行
- 共享映射:變更發(fā)生在底層文件
將上面兩兩組合:
- 私有文件映射:使用一個(gè)文件的內(nèi)容來初始化一塊內(nèi)存區(qū)域
- 私有匿名映射:為一個(gè)進(jìn)程分配新的內(nèi)存
- 共享文件映射:代替 read() 和 write() 、IPC
- 共享匿名映射:實(shí)現(xiàn)相關(guān)進(jìn)程實(shí)現(xiàn)類似于共享內(nèi)存
進(jìn)程執(zhí)行 exec() 時(shí)映射會丟失,但通過 fork() 的子進(jìn)程會繼承映射
3.2API函數(shù)
(1)創(chuàng)建一個(gè)映射
#include <sys/mman.h>
void *mmap( void *addr, size_t length, int prot, int flags, int fd, off_t offset );
成功返回新映射的起始地址,失敗返回 MAP_FAILED。
參數(shù) addr:映射被放置的虛擬地址,推薦為NULL(內(nèi)核會自動(dòng)選擇合適地址)
參數(shù) length:映射的字節(jié)數(shù)
參數(shù) prot:位掩碼,可以取OR
違反了保護(hù)信息,內(nèi)核會向進(jìn)程發(fā)送SIGSEGV信號。
- PROT_NONE:區(qū)域無法訪問,可以作為一個(gè)進(jìn)程分配的起始位置或結(jié)束位置的守護(hù)分頁
- PROT_WRITE:區(qū)域內(nèi)容可修改
- PROT_READ:區(qū)域內(nèi)容可讀取
- PROT_EXEC:區(qū)域內(nèi)容可執(zhí)行
參數(shù) flags:位掩碼,必須包含下列值中的一個(gè)
MAP_PROVATE:創(chuàng)建私有映射
MAP_SHARED:創(chuàng)建共享映射
參數(shù) fd:被映射的文件的文件描述符(調(diào)用之后就能夠關(guān)閉文件描述符)。在打開描述符 fd 引用的文件時(shí)必須要具備與 prot 和 flags參數(shù)值匹配的權(quán)限。特別的,文件必須總是被打開允許讀取。
參數(shù) offset:映射在文件中的起點(diǎn)
(2)解除映射區(qū)域
#include <sys/mman.h>
int munmap( void *addr, size_t length );
- 參數(shù) addr:待解除映射的起始地址
- 參數(shù) length:待解除映射區(qū)域的字節(jié)數(shù)
可以解除一個(gè)映射的部分映射,這樣原來的映射要么收縮,要么被分成兩個(gè),這取決于在何處開始解除映射。還可以指定一個(gè)跨越多個(gè)映射的地址范圍,這樣的話所有在范圍內(nèi)的映射都會被解除。
(3)同步映射區(qū)域
#include <sys/mman.h>
int msync( void *addr, size_t length, int flags );
參數(shù) flags:
- MS_SYNC:阻塞直到內(nèi)存區(qū)域中所有被修改過的分頁被寫入磁盤
- MS_ASYNC:在某個(gè)時(shí)刻被寫入磁盤
(4)重寫映射一個(gè)映射區(qū)域
#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap( void *old_address, size_t old_size, size_t new_size, int fflags, ... );
- 參數(shù) old_address 和 old_size 指既有映射的位置和大小。
- 參數(shù) new_size 指定新映射的大小
參數(shù) flags:
0
MREMAP_MAYMOVE:為映射在進(jìn)程的虛擬地址空間中重新指定一個(gè)位置
MREMAP_FIXED:配合 MREMAP_MAYMOVE 一起使用,mremap 會接收一個(gè)額外的參數(shù) void *new_address
(5)創(chuàng)建私有文件映射
創(chuàng)建一個(gè)私有文件映射,并打印文件內(nèi)容
#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的內(nèi)容寫到標(biāo)準(zhǔn)輸出*/
if( write( STDOUT_FILENO, addr, sb.st_size ) != sb.st_size ) {
perror("write");
}
exit( EXIT_SUCCESS );
}
(6)創(chuàng)建共享匿名映射
#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 )
{
/*獲取虛擬設(shè)備的文件描述符*/
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:
/*等待子進(jìn)程結(jié)束*/
if( wait(NULL) == -1 ) {
perror("wait");
}
printf("parent *addr = %d\n", *addr );
if( munmap( addr, sizeof(int) ) == -1 ) {
perror("munmap");
}
exit( EXIT_SUCCESS );
break;
}
}
三、內(nèi)存映射系統(tǒng)調(diào)用mmap
在 Linux 內(nèi)存映射的實(shí)現(xiàn)過程中,mmap函數(shù)扮演著核心角色,它就像是一把神奇的鑰匙,能夠打開內(nèi)存與文件之間的通道,讓我們可以輕松地進(jìn)行內(nèi)存映射操作 。
3.1mmap函數(shù)參數(shù)詳解
mmap()系統(tǒng)調(diào)用使得進(jìn)程之間通過映射同一個(gè)普通文件實(shí)現(xiàn)共享內(nèi)存。普通文件被映射到進(jìn)程地址空間后,進(jìn)程可以向訪問普通內(nèi)存一樣對文件進(jìn)行訪問,不必再調(diào)用read(),write()等操作。
注:實(shí)際上,mmap()系統(tǒng)調(diào)用并不是完全為了用于共享內(nèi)存而設(shè)計(jì)的。它本身提供了不同于一般對普通文件的訪問方式,進(jìn)程可以像讀寫內(nèi)存一樣對普通文件的操作。而Posix或系統(tǒng)V的共享內(nèi)存IPC則純粹用于共享目的,當(dāng)然mmap()實(shí)現(xiàn)共享內(nèi)存也是其主要應(yīng)用之一。
mmap()系統(tǒng)調(diào)用形式如下:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
參數(shù)fd為即將映射到進(jìn)程空間的文件描述字,一般由open()返回,同時(shí),fd可以指定為-1,此時(shí)須指定flags參數(shù)中的MAP_ANON,表明進(jìn)行的是匿名映射(不涉及具體的文件名,避免了文件的創(chuàng)建及打開,很顯然只能用于具有親緣關(guān)系的進(jìn)程間通信)。len是映射到調(diào)用進(jìn)程地址空間的字節(jié)數(shù),它從被映射文件開頭offset個(gè)字節(jié)開始算起。prot 參數(shù)指定共享內(nèi)存的訪問權(quán)限??扇∪缦聨讉€(gè)值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執(zhí)行), PROT_NONE(不可訪問)。
flags由以下幾個(gè)常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。offset參數(shù)一般設(shè)為0,表示從文件頭開始映射。參數(shù)addr指定文件應(yīng)被映射到進(jìn)程空間的起始地址,一般被指定一個(gè)空指針,此時(shí)選擇起始地址的任務(wù)留給內(nèi)核來完成。函數(shù)的返回值為最后文件映射到進(jìn)程空間的地址,進(jìn)程可直接操作起始地址為該值的有效地址。這里不再詳細(xì)介紹mmap()的參數(shù),讀者可參考mmap()手冊頁獲得進(jìn)一步的信息。
下面來詳細(xì)解讀一下各個(gè)參數(shù)的含義和作用 :
start:它是映射區(qū)的開始地址,當(dāng)我們將其設(shè)置為NULL時(shí),就意味著把選擇映射起始地址的權(quán)利交給了內(nèi)核,內(nèi)核會根據(jù)系統(tǒng)的實(shí)際情況,在進(jìn)程地址空間中挑選一個(gè)合適的地址來建立映射 。比如在一個(gè)進(jìn)程中,我們調(diào)用mmap函數(shù)并將start設(shè)為NULL,內(nèi)核就會在該進(jìn)程可用的虛擬地址空間里找到一段滿足條件的連續(xù)地址作為映射的起始點(diǎn) 。
length:這個(gè)參數(shù)表示映射區(qū)的長度,也就是我們希望將文件的多大區(qū)域映射到內(nèi)存中,它決定了映射區(qū)域的大小 。假如我們有一個(gè) 10MB 的文件,而我們只想將其中的 1MB 映射到內(nèi)存中進(jìn)行操作,那么就可以將length設(shè)置為 1MB 對應(yīng)的字節(jié)數(shù) 。
prot:它指定了期望的內(nèi)存保護(hù)標(biāo)志,這個(gè)標(biāo)志不能與文件的打開模式產(chǎn)生沖突,常見的取值有以下幾種 :
- PROT_EXEC:表示映射的頁內(nèi)容可以被執(zhí)行,當(dāng)我們映射的是一個(gè)可執(zhí)行文件或者共享庫中的代碼段時(shí),就需要設(shè)置這個(gè)標(biāo)志 。比如在運(yùn)行一個(gè) C 語言程序時(shí),程序中的可執(zhí)行代碼部分被映射到內(nèi)存中,就會設(shè)置PROT_EXEC標(biāo)志,使得這些代碼能夠被 CPU 執(zhí)行 。
- PROT_READ:意味著頁內(nèi)容可以被讀取,這是最常用的標(biāo)志之一,當(dāng)我們需要讀取文件內(nèi)容時(shí),就會設(shè)置這個(gè)標(biāo)志 。比如讀取一個(gè)文本文件的內(nèi)容,就需要設(shè)置PROT_READ標(biāo)志來允許對映射區(qū)域進(jìn)行讀取操作 。
- PROT_WRITE:表示頁可以被寫入,如果我們想要對映射的文件進(jìn)行修改,就需要設(shè)置這個(gè)標(biāo)志 。例如我們打開一個(gè)文件進(jìn)行讀寫操作,在調(diào)用mmap函數(shù)時(shí)就需要設(shè)置PROT_WRITE標(biāo)志 。
- PROT_NONE:表示頁不可訪問,這種情況比較特殊,一般用于某些特定的內(nèi)存管理場景,比如在隔離一些敏感數(shù)據(jù)區(qū)域時(shí)可能會用到 。
flags:該參數(shù)指定了映射對象的類型、映射選項(xiàng)以及映射頁是否可以共享,它的值可以是一個(gè)或者多個(gè)以下位的組合體 :
- MAP_SHARED:這個(gè)標(biāo)志非常重要,它表示與其它所有映射這個(gè)對象的進(jìn)程共享映射空間 。當(dāng)一個(gè)進(jìn)程對共享區(qū)進(jìn)行寫入操作時(shí),就相當(dāng)于輸出到文件 。不過,直到調(diào)用msync函數(shù)或者munmap函數(shù),文件實(shí)際上才會被更新 。在多進(jìn)程協(xié)作處理同一個(gè)文件的場景中,就可以使用MAP_SHARED標(biāo)志 。比如多個(gè)進(jìn)程需要同時(shí)讀取和修改一個(gè)配置文件,通過設(shè)置MAP_SHARED標(biāo)志,它們可以共享同一個(gè)映射空間,實(shí)現(xiàn)數(shù)據(jù)的實(shí)時(shí)同步 。
- MAP_PRIVATE:用于建立一個(gè)寫入時(shí)拷貝的私有映射,在這種映射方式下,內(nèi)存區(qū)域的寫入不會影響到原文件 。這個(gè)標(biāo)志和MAP_SHARED是互斥的,只能使用其中一個(gè) 。當(dāng)我們希望對文件進(jìn)行一些臨時(shí)的修改,而又不想影響原文件時(shí),就可以使用MAP_PRIVATE標(biāo)志 。比如在對一個(gè)文件進(jìn)行臨時(shí)的分析和處理時(shí),我們可以使用MAP_PRIVATE映射,對映射區(qū)域的修改不會改變原文件 。
- MAP_ANONYMOUS:表示匿名映射,即映射區(qū)不與任何文件關(guān)聯(lián) 。當(dāng)我們需要分配一段臨時(shí)的內(nèi)存空間,而不需要從文件中讀取數(shù)據(jù)或者將數(shù)據(jù)寫入文件時(shí),就可以使用匿名映射 。比如在進(jìn)行一些臨時(shí)的計(jì)算任務(wù)時(shí),我們可以使用MAP_ANONYMOUS標(biāo)志分配一塊內(nèi)存來存儲中間結(jié)果 。
- MAP_FIXED:使用指定的映射起始地址,如果由start和length參數(shù)指定的內(nèi)存區(qū)重疊于現(xiàn)存的映射空間,重疊部分將會被丟棄 。如果指定的起始地址不可用,操作將會失敗,并且起始地址必須落在頁的邊界上 。一般情況下,我們不建議使用這個(gè)標(biāo)志,因?yàn)樗赡軙?dǎo)致一些不可預(yù)測的問題,除非我們對內(nèi)存布局有非常明確的需求 。
fd:它是有效的文件描述詞,用于標(biāo)識要映射的文件 。當(dāng)我們進(jìn)行文件映射時(shí),需要先使用open函數(shù)打開文件,然后將返回的文件描述符傳遞給mmap函數(shù) 。如果設(shè)置了MAP_ANONYMOUS標(biāo)志,為了兼容問題,其值應(yīng)為 - 1 。比如我們要映射一個(gè)名為test.txt的文件,首先使用open("test.txt", O_RDWR)打開文件,得到文件描述符fd,然后將fd傳遞給mmap函數(shù)進(jìn)行映射操作 。
offset:表示被映射對象內(nèi)容的起點(diǎn),也就是從文件的哪個(gè)位置開始映射,這個(gè)值必須是分頁大小的整數(shù)倍 。在大多數(shù)情況下,我們會將其設(shè)置為 0,表示從文件的開頭開始映射 。比如文件的分頁大小是 4KB,如果我們想從文件的第 8KB 位置開始映射,那么offset就應(yīng)該設(shè)置為 8KB 。
系統(tǒng)調(diào)用mmap()用于共享內(nèi)存的兩種方式:
①使用普通文件提供的內(nèi)存映射:適用于任何進(jìn)程之間;此時(shí),需要打開或創(chuàng)建一個(gè)文件,然后再調(diào)用mmap();典型調(diào)用代碼如下:
fd=open(name, flag, mode);
if(fd<0
...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通過mmap()實(shí)現(xiàn)共享內(nèi)存的通信方式有許多特點(diǎn)和要注意的地方,我們將在范例中進(jìn)行具體說明。
②使用特殊文件提供匿名內(nèi)存映射:適用于具有親緣關(guān)系的進(jìn)程之間;由于父子進(jìn)程特殊的親緣關(guān)系,在父進(jìn)程中先調(diào)用mmap(),然后調(diào)用fork()。那么在調(diào)用fork()之后,子進(jìn)程繼承父進(jìn)程匿名映射后的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進(jìn)程就可以通過映射區(qū)域進(jìn)行通信了。注意,這里不是一般的繼承關(guān)系。一般來說,子進(jìn)程單獨(dú)維護(hù)從父進(jìn)程繼承下來的一些變量。而mmap()返回的地址,卻由父子進(jìn)程共同維護(hù)。
對于具有親緣關(guān)系的進(jìn)程實(shí)現(xiàn)共享內(nèi)存最好的方式應(yīng)該是采用匿名內(nèi)存映射的方式。此時(shí),不必指定具體的文件,只要設(shè)置相應(yīng)的標(biāo)志即可,參見范例2。
系統(tǒng)調(diào)用munmap()
int munmap( void * addr, size_t len )
該調(diào)用在進(jìn)程地址空間中解除一個(gè)映射關(guān)系,addr是調(diào)用mmap()時(shí)返回的地址,len是映射區(qū)的大小。當(dāng)映射關(guān)系解除后,對原來映射地址的訪問將導(dǎo)致段錯(cuò)誤發(fā)生。
系統(tǒng)調(diào)用msync()
int msync ( void * addr , size_t len, int flags)
一般說來,進(jìn)程在映射空間的對共享內(nèi)容的改變并不直接寫回到磁盤文件中,往往在調(diào)用munmap()后才執(zhí)行該操作??梢酝ㄟ^調(diào)用msync()實(shí)現(xiàn)磁盤上文件內(nèi)容與共享內(nèi)存區(qū)的內(nèi)容一致。
3.2返回值解析
mmap函數(shù)的返回值也很關(guān)鍵,它能告訴我們映射操作是否成功 。當(dāng)mmap成功執(zhí)行時(shí),會返回被映射區(qū)的指針,我們可以通過這個(gè)指針來訪問映射的內(nèi)存區(qū)域 。例如:
#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字節(jié)
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 使用ptr訪問映射區(qū)域
//...
// 解除映射
if (munmap(ptr, length) == -1) {
perror("munmap");
}
close(fd);
return 0;
}
在上述代碼中,如果mmap函數(shù)成功,ptr就會指向映射區(qū)的起始地址,我們可以通過ptr來對映射區(qū)域進(jìn)行讀寫等操作 。而當(dāng)mmap函數(shù)失敗時(shí),會返回MAP_FAILED,其值為(void *)-1,同時(shí)errno會被設(shè)置為相應(yīng)的錯(cuò)誤代碼,以指示錯(cuò)誤的原因 。
常見的錯(cuò)誤原因包括:
- EACCES:表示訪問出錯(cuò),可能是因?yàn)闄?quán)限不足,比如我們嘗試以寫權(quán)限映射一個(gè)只讀文件時(shí),就會出現(xiàn)這個(gè)錯(cuò)誤 。
- EBADF:意味著fd不是有效的文件描述詞,可能是文件沒有正確打開或者文件描述符已經(jīng)被關(guān)閉 。
- EINVAL:表示一個(gè)或者多個(gè)參數(shù)無效,比如length為負(fù)數(shù),或者offset不是分頁大小的整數(shù)倍等 。
- ENOMEM:表示內(nèi)存不足,或者進(jìn)程已超出最大內(nèi)存映射數(shù)量,當(dāng)系統(tǒng)內(nèi)存緊張,無法為映射分配足夠的內(nèi)存時(shí),就會出現(xiàn)這個(gè)錯(cuò)誤 。
3.3mmap系統(tǒng)調(diào)用和直接使用IPC共享內(nèi)存之間的差異
mmap系統(tǒng)調(diào)用用于將文件映射到進(jìn)程的地址空間中,而共享內(nèi)存是一種不同的機(jī)制,用于進(jìn)程間通信。這兩種方法都用于數(shù)據(jù)共享和高效的內(nèi)存訪問,但它們有一些關(guān)鍵區(qū)別:
(1)數(shù)據(jù)源和持久化
- mmap: 通過 mmap 映射的數(shù)據(jù)通常來自文件系統(tǒng)中的文件。這意味著數(shù)據(jù)是持久化的——即使程序終止,文件中的數(shù)據(jù)依然存在。當(dāng)你通過映射的內(nèi)存區(qū)域修改數(shù)據(jù)時(shí),這些更改最終會反映到磁盤上的文件中。
- 共享內(nèi)存:共享內(nèi)存是一塊匿名的(或者有時(shí)與特定文件關(guān)聯(lián)的)內(nèi)存區(qū)域,它可以被多個(gè)進(jìn)程訪問。與 mmap 映射的文件不同,共享內(nèi)存通常是非持久的,即數(shù)據(jù)僅在計(jì)算機(jī)運(yùn)行時(shí)存在,一旦系統(tǒng)關(guān)閉或重啟,存儲在共享內(nèi)存中的數(shù)據(jù)就會丟失。
(2)使用場景
- mmap:mmap 特別適合于需要頻繁讀寫大文件的場景,因?yàn)樗梢詼p少磁盤 I/O 操作的次數(shù)。它也允許文件的一部分被映射到內(nèi)存中,這對于處理大型文件尤為有用。
- 共享內(nèi)存:共享內(nèi)存通常用于進(jìn)程間通信(IPC),允許多個(gè)進(jìn)程訪問相同的內(nèi)存區(qū)域,這樣可以非常高效地在進(jìn)程之間交換數(shù)據(jù)。
(3)性能和效率
- mmap:映射文件到內(nèi)存可以提高文件訪問的效率,尤其是對于隨機(jī)訪問或頻繁讀寫的場景。系統(tǒng)可以利用虛擬內(nèi)存管理和頁面緩存機(jī)制來優(yōu)化訪問。
- 共享內(nèi)存:共享內(nèi)存提供了一種非??焖俚臄?shù)據(jù)交換方式,因?yàn)樗械耐ㄐ哦荚趦?nèi)存中進(jìn)行,沒有文件 I/O 操作。
(4)同步和一致性
- mmap:使用 mmap 時(shí),必須考慮到文件內(nèi)容的同步問題。例如,使用 msync 調(diào)用來確保內(nèi)存中的更改被同步到磁盤文件中。
- 共享內(nèi)存:在共享內(nèi)存的環(huán)境中,進(jìn)程需要使用某種形式的同步機(jī)制(如信號量、互斥鎖)來避免競爭條件和數(shù)據(jù)不一致。
四、存儲映射I/O
在現(xiàn)在的項(xiàng)目中需要用到mmap建立內(nèi)存映射文件,順便把存儲映射I/O看了一下,這個(gè)東西還真是加載索引的良好工具,存儲映射I/O可以使一個(gè)磁盤文件與存儲空間中的一個(gè)緩沖區(qū)相映射,這樣可以從緩沖區(qū)中讀取數(shù)據(jù),就相當(dāng)于讀文件中的相應(yīng)字節(jié),而當(dāng)將數(shù)據(jù)存入緩沖區(qū)時(shí),最后相應(yīng)字節(jié)就自動(dòng)寫入文件中。
利用mmap建立內(nèi)存映射文件一般會分為兩條線:寫文件,讀文件,在分別介紹這兩條線之前首先將存儲映射I/O的常用函數(shù)介紹一下。
4.1存儲映射I/O基本函數(shù)
(1) mmap函數(shù), 這個(gè)函數(shù)會告訴內(nèi)核將一個(gè)給定的文件映射到一個(gè)存儲區(qū)域中,其函數(shù)原型為:
void* mmap(void *addr,size_t len,int prot,int flags,int fields,off_t off);
其中,參數(shù)addr用于指定存儲映射區(qū)的起始地址,通常設(shè)定為0,這表示由系統(tǒng)選擇該映射區(qū)的起始地址,參數(shù)len是指定映射的字節(jié)數(shù),參數(shù)port指定映射區(qū)的方式,如PROT_READ,PROT_WRITE,值得注意的是映射區(qū)的保護(hù)不能超過文件open模式訪問權(quán)限。參數(shù)flags是設(shè)置映射區(qū)的屬性,一般設(shè)為MAP_SHARED,這一標(biāo)志說明本進(jìn)程的存儲操作相當(dāng)于文件的write操作,參數(shù)fields是指定操作的文件描述符,參數(shù)off是要映射字節(jié)在文件中的起始偏移量。如果函數(shù)調(diào)用成功,函數(shù)的返回值是存儲映射區(qū)的起始地址;如果調(diào)用失敗,則返回MAP_FAILED。
(2) msync函數(shù),這個(gè)函數(shù)會將存儲映射區(qū)的修改沖洗到被映射的文件中,其函數(shù)原型為:
int msync(void *addr,size_t len,int flags)
其中,參數(shù)flags參數(shù)設(shè)定如何控制沖洗存儲區(qū),可以選擇MS_ASYNC,這表明是異步操作,函數(shù)調(diào)用立即返回,而選擇MS_SYNC,函數(shù)調(diào)用則等待寫操作完成后才會返回。
(3) munmap函數(shù),這個(gè)函數(shù)會解除文件和存儲映射區(qū)之間的映射。
int munmap(caddr_t addr,size_t len)
4.2寫入映射緩沖區(qū)
當(dāng)我們想向映射緩沖區(qū)中寫入數(shù)據(jù)時(shí),首先需要確定映射文件的大小,在打開文件后,可以利用修改文件大小的函數(shù)重新設(shè)定文件的大小,接下來就可以對該緩沖區(qū)進(jìn)行寫操作。
int fd = open(file_name,O_RDWR|O_CREAT);
ftruncate(fd,size);
mmap(0,size,PROT_WRITE,MAP_SHARED,fd,0);
4.3從映射緩沖區(qū)讀取
當(dāng)我們想從映射緩沖區(qū)中讀取數(shù)據(jù)時(shí),需要利用stat系列函數(shù)得到文件大小,進(jìn)行利用在映射存儲區(qū)中打開該文件。
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實(shí)例:用存儲映射 I/O 復(fù)制文件
#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);
}
五、內(nèi)存映射的應(yīng)用場景
5.1大文件處理
在當(dāng)今數(shù)據(jù)爆炸的時(shí)代,處理大文件是許多應(yīng)用場景中不可避免的挑戰(zhàn)。無論是數(shù)據(jù)分析、多媒體處理還是日志管理,大文件的讀寫操作都對系統(tǒng)性能提出了極高的要求 。傳統(tǒng)的文件I/O方式在面對大文件時(shí)往往顯得力不從心,頻繁的磁盤 I/O 操作會導(dǎo)致系統(tǒng)性能大幅下降,而內(nèi)存映射技術(shù)則為大文件處理提供了一種高效的解決方案 。
內(nèi)存映射之所以能顯著提升大文件處理效率,關(guān)鍵在于它減少了數(shù)據(jù)的讀寫次數(shù)。以一個(gè) 1GB 的日志文件為例,假設(shè)我們需要統(tǒng)計(jì)其中特定關(guān)鍵詞出現(xiàn)的次數(shù)。如果使用傳統(tǒng)的read函數(shù)逐塊讀取文件內(nèi)容到用戶空間進(jìn)行處理,每讀取一次都涉及數(shù)據(jù)從磁盤到內(nèi)核緩沖區(qū),再到用戶空間的拷貝過程 。
而采用內(nèi)存映射,文件直接被映射到進(jìn)程的虛擬地址空間,進(jìn)程可以像訪問內(nèi)存一樣直接讀取文件內(nèi)容,避免了數(shù)據(jù)在不同緩沖區(qū)之間的多次拷貝,大大減少了 I/O 操作的開銷 。并且,內(nèi)存映射還可以利用操作系統(tǒng)的頁緩存機(jī)制,當(dāng)進(jìn)程訪問映射區(qū)域中的數(shù)據(jù)時(shí),如果數(shù)據(jù)已經(jīng)在頁緩存中,就可以直接從內(nèi)存中讀取,無需再次訪問磁盤,進(jìn)一步提高了數(shù)據(jù)訪問的速度 。
5.2進(jìn)程間通信
在多進(jìn)程編程的世界里,進(jìn)程間通信(IPC)是實(shí)現(xiàn)不同進(jìn)程之間數(shù)據(jù)交互和協(xié)作的關(guān)鍵。內(nèi)存映射為進(jìn)程間通信提供了一種高效且直接的方式,通過映射同一文件或匿名內(nèi)存,不同進(jìn)程可以共享同一塊內(nèi)存區(qū)域,實(shí)現(xiàn)數(shù)據(jù)的實(shí)時(shí)共享和交互 。
以一個(gè)簡單的生產(chǎn)者 - 消費(fèi)者模型為例,生產(chǎn)者進(jìn)程和消費(fèi)者進(jìn)程需要共享一個(gè)數(shù)據(jù)緩沖區(qū)。我們可以通過內(nèi)存映射創(chuàng)建一個(gè)共享的內(nèi)存區(qū)域,生產(chǎn)者進(jìn)程將數(shù)據(jù)寫入這個(gè)共享區(qū)域,消費(fèi)者進(jìn)程則從該區(qū)域讀取數(shù)據(jù) 。在這個(gè)過程中,內(nèi)存映射利用了操作系統(tǒng)的虛擬內(nèi)存機(jī)制,使得不同進(jìn)程的虛擬地址可以映射到相同的物理內(nèi)存頁,從而實(shí)現(xiàn)數(shù)據(jù)的共享 。
并且,為了保證數(shù)據(jù)的一致性和同步性,通常會結(jié)合信號量、互斥鎖等同步機(jī)制來協(xié)調(diào)不同進(jìn)程對共享內(nèi)存的訪問 。比如,生產(chǎn)者在向共享內(nèi)存寫入數(shù)據(jù)前,先獲取互斥鎖,防止其他進(jìn)程同時(shí)寫入;寫入完成后,釋放互斥鎖,并發(fā)送信號量通知消費(fèi)者有新數(shù)據(jù)可用 。這樣,通過內(nèi)存映射和同步機(jī)制的配合,不同進(jìn)程可以高效、安全地進(jìn)行數(shù)據(jù)共享和通信 。
5.3動(dòng)態(tài)庫加載
在 Linux 系統(tǒng)中,動(dòng)態(tài)庫是一種重要的代碼共享機(jī)制,它允許多個(gè)程序共享同一份代碼和數(shù)據(jù),從而節(jié)省內(nèi)存空間和磁盤空間 。內(nèi)存映射在動(dòng)態(tài)庫加載過程中扮演著至關(guān)重要的角色,它將動(dòng)態(tài)庫的代碼段和數(shù)據(jù)段映射到進(jìn)程的虛擬地址空間,使得進(jìn)程能夠高效地訪問動(dòng)態(tài)庫中的函數(shù)和變量 。
當(dāng)一個(gè)可執(zhí)行程序依賴于某個(gè)動(dòng)態(tài)庫時(shí),在程序啟動(dòng)階段,系統(tǒng)會通過內(nèi)存映射將動(dòng)態(tài)庫加載到內(nèi)存中。具體來說,動(dòng)態(tài)鏈接器(如ld.so)會首先解析可執(zhí)行文件的依賴關(guān)系,找到需要加載的動(dòng)態(tài)庫 。然后,它使用內(nèi)存映射將動(dòng)態(tài)庫的代碼段映射到進(jìn)程的虛擬地址空間,并設(shè)置相應(yīng)的權(quán)限,如代碼段通常設(shè)置為只讀和可執(zhí)行權(quán)限 。對于動(dòng)態(tài)庫的數(shù)據(jù)段,也會根據(jù)其屬性進(jìn)行映射,如全局變量所在的數(shù)據(jù)段可能設(shè)置為可讀寫權(quán)限 。
在映射過程中,動(dòng)態(tài)鏈接器還會處理動(dòng)態(tài)庫中的符號表,將可執(zhí)行文件中的符號引用與動(dòng)態(tài)庫中的實(shí)際函數(shù)和變量地址進(jìn)行綁定,確保程序在運(yùn)行時(shí)能夠正確地調(diào)用動(dòng)態(tài)庫中的功能 。通過內(nèi)存映射加載動(dòng)態(tài)庫,不僅提高了程序的啟動(dòng)速度,還實(shí)現(xiàn)了代碼的共享,多個(gè)進(jìn)程可以共享同一個(gè)動(dòng)態(tài)庫的內(nèi)存映射,減少了內(nèi)存的占用 。
六、實(shí)戰(zhàn)演練:代碼中的內(nèi)存映射
6.1簡單文件讀寫示例
下面是一個(gè)使用mmap函數(shù)進(jìn)行文件讀寫的簡單示例代碼:
#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;
}
// 將文件映射到內(nèi)存
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;
}
// 現(xiàn)在可以像操作普通內(nèi)存一樣訪問文件內(nèi)容
printf("File contents before modification:\n%s\n", mapped);
// 修改文件內(nèi)容
strcpy(mapped, "Hello, mmap!");
// 解除映射
if (munmap(mapped, sb.st_size) == -1) {
perror("munmap");
}
// 關(guān)閉文件
close(fd);
return EXIT_SUCCESS;
}
代碼解釋:
- 打開文件:使用open函數(shù)打開名為example.txt的文件,O_RDWR標(biāo)志表示以讀寫模式打開文件。如果打開失敗,通過perror函數(shù)打印錯(cuò)誤信息并返回。
- 獲取文件大小:利用fstat函數(shù)獲取文件的相關(guān)信息,包括文件大小,將結(jié)果存儲在sb結(jié)構(gòu)體中。若獲取失敗,同樣打印錯(cuò)誤信息并關(guān)閉文件返回。
- 內(nèi)存映射:調(diào)用mmap函數(shù)將文件映射到內(nèi)存中。NULL表示讓系統(tǒng)自動(dòng)選擇映射的起始地址,sb.st_size指定映射的長度為文件的大小,PROT_READ | PROT_WRITE表示映射區(qū)域具有可讀可寫權(quán)限,MAP_SHARED表示映射區(qū)域的修改會同步到文件,fd是前面打開文件返回的文件描述符,0表示從文件開頭開始映射。如果映射失敗,打印錯(cuò)誤信息并關(guān)閉文件返回。
- 訪問和修改映射區(qū)域:通過mapped指針可以像操作普通內(nèi)存一樣訪問和修改文件內(nèi)容。這里使用strcpy函數(shù)將字符串Hello, mmap!復(fù)制到映射區(qū)域,從而修改了文件的內(nèi)容。
- 解除映射:使用munmap函數(shù)解除內(nèi)存映射,參數(shù)為映射的起始地址mapped和映射長度sb.st_size。如果解除失敗,打印錯(cuò)誤信息。
- 關(guān)閉文件:最后使用close函數(shù)關(guān)閉文件。
6.2進(jìn)程間通信示例
以下是通過內(nèi)存映射實(shí)現(xiàn)進(jìn)程間通信的示例代碼,這里以父子進(jìn)程為例:
#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;
// 創(chuàng)建一個(gè)臨時(shí)文件用于內(nèi)存映射
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;
}
// 寫入一個(gè)字節(jié),使文件大小達(dá)到SHM_SIZE
if (write(fd, "", 1) != 1) {
perror("write");
close(fd);
return EXIT_FAILURE;
}
// 將文件映射到內(nèi)存
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;
}
// 創(chuàng)建子進(jìn)程
pid = fork();
if (pid == -1) {
perror("fork");
munmap(shared_memory, SHM_SIZE);
close(fd);
return EXIT_FAILURE;
} else if (pid == 0) {
// 子進(jìn)程
strcpy(shared_memory, "Hello from child!");
_exit(EXIT_SUCCESS);
} else {
// 父進(jìn)程
wait(NULL);
printf("Data read from shared memory: %s\n", shared_memory);
wait(NULL);
}
// 解除映射
if (munmap(shared_memory, SHM_SIZE) == -1) {
perror("munmap");
}
// 關(guān)閉文件
close(fd);
// 刪除臨時(shí)文件
if (unlink("temp_file") == -1) {
perror("unlink");
}
return EXIT_SUCCESS;
}
關(guān)鍵步驟分析:
- 創(chuàng)建臨時(shí)文件并拓展大?。菏褂胦pen函數(shù)創(chuàng)建一個(gè)名為temp_file的臨時(shí)文件,O_RDWR | O_CREAT | O_TRUNC標(biāo)志表示以讀寫模式創(chuàng)建文件,如果文件存在則截?cái)唷H缓笸ㄟ^lseek函數(shù)將文件指針移動(dòng)到SHM_SIZE - 1的位置,再使用write函數(shù)寫入一個(gè)字節(jié),使文件大小達(dá)到SHM_SIZE,為后續(xù)的內(nèi)存映射做準(zhǔn)備。
- 內(nèi)存映射:調(diào)用mmap函數(shù)將臨時(shí)文件映射到內(nèi)存中,得到一個(gè)指向共享內(nèi)存區(qū)域的指針shared_memory。
- 創(chuàng)建子進(jìn)程:使用fork函數(shù)創(chuàng)建子進(jìn)程。子進(jìn)程和父進(jìn)程會共享這個(gè)內(nèi)存映射區(qū)域。
- 子進(jìn)程操作:在子進(jìn)程中,使用strcpy函數(shù)將字符串Hello from child!復(fù)制到共享內(nèi)存區(qū)域,然后調(diào)用_exit函數(shù)退出子進(jìn)程。
- 父進(jìn)程操作:父進(jìn)程通過wait函數(shù)等待子進(jìn)程結(jié)束,然后從共享內(nèi)存區(qū)域讀取數(shù)據(jù)并打印。
- 清理資源:最后,父進(jìn)程解除內(nèi)存映射,關(guān)閉文件,并刪除臨時(shí)文件,釋放相關(guān)資源。
6.3分塊內(nèi)存映射處理大文件示例
(1)內(nèi)存映射文件可以用于3個(gè)不同的目的:
- 系統(tǒng)使用內(nèi)存映射文件,以便加載和執(zhí)行. exe和DLL文件。這可以大大節(jié)省頁文件空間和應(yīng)用程序啟動(dòng)運(yùn)行所需的時(shí)間。
- 可以使用內(nèi)存映射文件來訪問磁盤上的數(shù)據(jù)文件。這使你可以不必對文件執(zhí)行I/O操作,并且可以不必對文件內(nèi)容進(jìn)行緩存。
- 可以使用內(nèi)存映射文件,使同一臺計(jì)算機(jī)上運(yùn)行的多個(gè)進(jìn)程能夠相互之間共享數(shù)據(jù)。Windows確實(shí)提供了其他一些方法,以便在進(jìn)程之間進(jìn)行數(shù)據(jù)通信,但是這些方法都是使用內(nèi)存映射文件來實(shí)現(xiàn)的,這使得內(nèi)存映射文件成為單個(gè)計(jì)算機(jī)上的多個(gè)進(jìn)程互相進(jìn)行通信的最有效的方法。
(2)使用內(nèi)存映射數(shù)據(jù)文件
若要使用內(nèi)存映射文件,必須執(zhí)行下列操作步驟:
- 1) 創(chuàng)建或打開一個(gè)文件內(nèi)核對象,該對象用于標(biāo)識磁盤上你想用作內(nèi)存映射文件的文件。
- 2) 創(chuàng)建一個(gè)文件映射內(nèi)核對象,告訴系統(tǒng)該文件的大小和你打算如何訪問該文件。
- 3) 讓系統(tǒng)將文件映射對象的全部或一部分映射到你的進(jìn)程地址空間中。
當(dāng)完成對內(nèi)存映射文件的使用時(shí),必須執(zhí)行下面這些步驟將它清除:
- 1) 告訴系統(tǒng)從你的進(jìn)程的地址空間中撤消文件映射內(nèi)核對象的映像。
- 2) 關(guān)閉文件映射內(nèi)核對象。
- 3) 關(guān)閉文件內(nèi)核對象。
文件操作是應(yīng)用程序最為基本的功能之一,Win32 API和MFC均提供有支持文件處理的函數(shù)和類,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile類等。一般來說,以上這些函數(shù)可以滿足大多數(shù)場合的要求,但是對于某些特殊應(yīng)用領(lǐng)域所需要的動(dòng)輒幾十GB、幾百GB、乃至幾TB的海量存儲,再以通常的文件處理方法進(jìn)行處理顯然是行不通的。所以可以使用內(nèi)存文件映射來處理數(shù)據(jù),網(wǎng)上也有鋪天蓋地的文章,但是映射大文件的時(shí)候又往往會出錯(cuò),需要進(jìn)行文件分塊內(nèi)存映射,這里就是這樣的一個(gè)例子,教你如何把文件分塊映射到內(nèi)存。
//
// 該函數(shù)用于讀取從CCD攝像頭采集來的RAW視頻數(shù)據(jù)當(dāng)中的某一幀圖像,
// RAW視頻前596字節(jié)為頭部信息,可以從其中讀出視頻總的幀數(shù),
// 幀格式為1024*576*8
/*
參數(shù):
pszPath:文件名
dwFrame: 要讀取第幾幀,默認(rèn)讀取第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的倍數(shù) DWORD的倍數(shù) 這里是 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;
}
//構(gòu)造Bitmap文件頭BITMAPFILEHEADER
bitmapFileHeader.bfType = 0x4d42; // 很重要的標(biāo)志位 BM 標(biāo)識
bitmapFileHeader.bfSize = (DWORD)(bitmapFileHeader.bfOffBits + height * widthbytes); //bmp文件長度
bitmapFileHeader.bfReserved1 = 0;
bitmapFileHeader.bfReserved2 = 0;
//構(gòu)造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;
//創(chuàng)建BMP內(nèi)存映像,寫入位圖頭部
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);
//構(gòu)造調(diào)色板 , 當(dāng)像素大于8位時(shí),就沒有調(diào)色板了。
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);
}
}
//
// 文件映射,從文件中查找圖像的數(shù)據(jù)
//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 ; // 標(biāo)準(zhǔn)塊大小
DWORD dwFrameSize = height*width; // 計(jì)算一幀圖像的數(shù)據(jù)量,不包括頭部信息
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; //內(nèi)存緩沖剩余的字節(jié)
if ( dwTmp >= dwFrameSize )
{
if(dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwFrameSize);
bLoop = FALSE;
}
curr2 += dwFrameSize;
}else //內(nèi)存中不夠一幀數(shù)據(jù)
{
DWORD dwTmp2 = dwFrameSize - dwTmp; // 一副完整的幀還需要dwTmp2字節(jié)
if (dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwTmp);
curr += dwTmp;
}
//1、首先計(jì)算文件的偏移位置
qwFileOffset += dwBytesInBlock;