下面將給出兩個使用mmap()的例子:例1說明兩個進程通過映射普通文件實現* * *內存通信;例2說明父子進程通過匿名映射實現* * *內存共享。系統調用mmap()有很多有趣的地方。下面是壹個用mmap()映射普通文件實現進程間通信的例子。我們舉例說明mmap()實現* * *內存共享的特點和註意事項。
例1:兩個進程通過映射普通文件實現* * *內存通信。
例1包含兩個子程序:map_normalfile1.c和map_normalfile2.c編譯兩個程序,可執行文件分別為map_normalfile1和map_normalfile2。兩個程序通過命令行參數指定同壹個文件,實現* * *內存共享模式下的進程間通信。Map_normalfile2嘗試打開命令行參數指定的普通文件,將文件映射到進程的地址空間,並寫入映射的地址空間。Map_normalfile1將命令行參數指定的文件映射到進程地址空間,然後讀取映射的地址空間。這樣,兩個進程通過命令行參數指定同壹個文件,實現* * *內存共享模式的進程間通信。
這裏有兩個程序代碼:
/* - map_normalfile1.c - */
# include & ltsys/mman . h & gt;
# include & ltsys/types . h & gt;
# include & ltfcntl.h & gt
# include & ltunistd.h & gt
typedef結構{
字符名稱[4];
int age
}人;
main(int argc,char** argv) //將普通文件映射為共享內存:
{
int fd,I;
人* p _ map
炭化溫度;
fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
lseek(fd,sizeof(people)*5-1,SEEK _ SET);
寫(fd,"",1);
p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
關閉(FD);
temp = ' a
for(I = 0;我& lt10;i++)
{
temp+= 1;
memcpy( ( *(p_map+i))。姓名& amp溫度,2);
(*(p_map+i))。年齡= 20+I;
}
printf(" initialize over \ n ");
睡眠(10);
munmap( p_map,sizeof(people)* 10);
printf(" umap ok \ n ");
}
/* - map_normalfile2.c - */
# include & ltsys/mman . h & gt;
# include & ltsys/types . h & gt;
# include & ltfcntl.h & gt
# include & ltunistd.h & gt
typedef結構{
字符名稱[4];
int age
}人;
main(int argc,char** argv) //將普通文件映射為共享內存:
{
int fd,I;
人* p _ map
fd=open( argv[1],O_CREAT|O_RDWR,00777);
p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
for(I = 0;我& lt10;i++)
{
printf(" name:% s age % d;\n",(*(p_map+i))。名字,(*(p_map+i))。年齡);
}
munmap( p_map,sizeof(people)* 10);
}
Map_normalfile1.c首先定義了壹個people數據結構,(這裏采用數據結構是因為* * *的內存區的數據往往有固定的格式,由各個通信進程決定,結構壹般具有代表性)。Map_normfile1首先打開或創建壹個文件,並將文件的長度設置為五個人的結構大小。然後從mmap()的返回地址開始,設置10個人結構。然後,進程休眠10秒,等待其他進程映射同壹個文件,最後取消映射。
Map_normfile2.c簡單映射壹個文件,從mmap()返回的地址中以people數據結構的格式讀取10個people結構,輸出讀取的值,然後取消映射。
將兩個程序分別編譯成可執行文件map_normalfile1和map_normalfile2,然後在終端上運行。/map_normalfile2 /tmp/test_shm,程序輸出結果如下:
重新初始化
umap正常
在從map_normalfile1輸出initialize over之後,在輸出umap ok之前,在另壹個終端上運行map_normalfile2 /tmp/test_shm將產生以下輸出(為了節省空間,輸出結果稍微排序):
姓名:b年齡20;姓名:c年齡21;姓名:d年齡22;姓名:e年齡23;姓名:f年齡24;
姓名:g年齡25;姓名:h年齡26;姓名:本人年齡27;姓名:j年齡28;姓名:k年齡29;
在map_normalfile1輸出umap ok之後,運行map_normalfile2會輸出以下結果:
姓名:b年齡20;姓名:c年齡21;姓名:d年齡22;姓名:e年齡23;姓名:f年齡24;
姓名:年齡0;姓名:年齡0;姓名:年齡0;姓名:年齡0;姓名:年齡0;
從程序運行結果中可以得出的結論
1.最終映射文件的內容長度不會超過文件本身的初始大小,即映射不能改變文件的大小;
2.可用於進程通信的有效地址空間的大小通常受映射文件大小的限制,但並不完全受文件大小的限制。打開的文件被截斷為五個人結構大小,10個人數據結構在map_normalfile1中初始化。在適當的時候調用map_normalfile2(在map_normalfile1初始化之後,umap ok輸出之前)會發現map_normalfile2會輸出10人結構的所有值。
註意:在linux中,內存保護是基於頁面的。即使映射的文件只有壹個字節大小,內核也會為映射分配壹個頁面大小的內存。當映射的文件小於頁面大小時,進程可以從mmap()的返回地址開始無錯誤地訪問頁面大小;但是,如果訪問頁面之外的地址空間,將會發生錯誤,這將在後面進壹步描述。因此,可用於進程間通信的有效地址空間的大小不會超過文件大小和壹個頁面大小的總和。
3.文件壹旦映射,調用mmap()的進程訪問返回地址到某個內存區域,暫時脫離文件在磁盤上的影響。所有將地址空間返回給mmap()的操作只有在內存中才有意義。只有在調用munmap()或msync()後,才能將內存中相應的內容寫回磁盤文件,寫入的內容仍然不能超過文件的大小。
示例2:父子進程通過匿名映射共享內存。
# include & ltsys/mman . h & gt;
# include & ltsys/types . h & gt;
# include & ltfcntl.h & gt
# include & ltunistd.h & gt
typedef結構{
字符名稱[4];
int age
}人;
main(int argc,char** argv)
{
int I;
人* p _ map
炭化溫度;
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
if(fork() == 0)
{
睡眠(2);
for(I = 0;我& lt5;i++)
printf("child read:這%d人的年齡是%d\n ",i+1,(*(p_map+i))。年齡);
(*p_map)。年齡= 100;
munmap(p_map,sizeof(people)* 10);//實際上,當進程終止時,它會自動取消映射。
exit();
}
temp = ' a
for(I = 0;我& lt5;i++)
{
temp+= 1;
memcpy((*(p_map+i))。姓名& amp溫度,2);
(*(p_map+i))。年齡= 20+I;
}
睡眠(5);
printf(" parent read:the first people,s age is %d\n ",(*p_map)。年齡);
printf(" umap \ n ");
munmap( p_map,sizeof(people)* 10);
printf(" umap ok \ n ");
}
檢查程序的輸出結果,我意識到父子流程享有匿名內存:
孩子讀:1人的年齡是20
子讀:兩個人的年齡是21
孩子讀:三個人的年齡都是22
孩子讀:這4個人的年齡是23歲
孩子讀:5個人的年齡是24
家長讀:第壹個人的年齡是100
umap
umap正常
回到頂端
4.訪問mmap()的返回地址
正如前面討論示例的運行結構時提到的,linux采用了頁面管理機制。對於mmap()映射的普通文件,進程會在自己的地址空間中增加壹個空間,空間大小由mmap()的len參數指定。請註意,該進程可能無法有效地訪問所有新添加的空間。進程可以訪問的有效地址大小取決於文件映射部分的大小。簡單來說,可以容納文件映射部分大小的最小頁數決定了進程可以從mmap()返回的地址有效訪問的地址空間。超過這個空間大小,內核將根據超出的嚴重程度返回給進程發送不同的信號。可以舉例如下:
註意:文件的映射部分,而不是整個文件,決定了進程可以訪問的空間大小。此外,如果指定文件的偏移部分,必須註意頁面大小的整數倍。以下是訪問進程映射地址空間的示例:
# include & ltsys/mman . h & gt;
# include & ltsys/types . h & gt;
# include & ltfcntl.h & gt
# include & ltunistd.h & gt
typedef結構{
字符名稱[4];
int age
}人;
main(int argc,char** argv)
{
int fd,I;
int pagesize,offset
人* p _ map
PAGESIZE = sysconf(_ SC _ PAGESIZE);
printf("pagesize是%d\n ",pagesize);
fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
lseek(fd,pagesize*2-100,SEEK _ SET);
寫(fd,"",1);
偏移= 0;//這裏offset = 0編譯成版本1;Offset =編譯為版本2的頁面大小。
p_map = (people*)mmap(NULL,pagesize*3,PROT _讀| PROT _寫,地圖_共享,fd,偏移量);
關閉(FD);
for(I = 1;我& lt10;i++)
{
(*(p _ map+pagesize/sizeof(people)* I-2))。年齡= 100;
printf("access page %d over\n ",I);
(*(p _ map+pagesize/sizeof(people)* I-1))。年齡= 100;
printf("訪問頁%d edge over,現在開始訪問頁%d\n ",I,I+1);
(*(p _ map+pagesize/sizeof(people)* I))。年齡= 100;
printf("access page %d over\n ",I+1);
}
munmap(p_map,sizeof(people)* 10);
}
正如程序中提到的,程序編譯成兩個版本,主要體現在文件的映射部分大小不同。文件大小在壹頁到兩頁之間(大小:pagesize*2-99),版本1的映射部分是整個文件,版本2的映射部分是文件大小減去壹頁的剩余部分,小於壹頁大小(大小:pagesize-99)。程序試圖訪問每壹個頁面邊界,兩個版本都試圖在進程空間中映射pagesize*3的字節數。
版本1的輸出如下:
頁面大小是4096
訪問1頁結束
訪問頁面1邊結束,現在開始訪問頁面2
訪問第2頁結束
訪問第2頁結束
訪問第2頁結束,現在開始訪問第3頁
總線錯誤//映射文件覆蓋了進程空間中的兩頁。此時,該進程嘗試訪問第三頁。
版本2的輸出如下:
頁面大小是4096
訪問1頁結束
訪問頁面1邊結束,現在開始訪問頁面2
總線錯誤//映射文件覆蓋了進程空間中的壹個頁面。此時,該進程嘗試訪問第二頁。
結論:使用系統調用mmap()實現進程間通信非常方便,應用層上的接口非常簡單。內部實現機制區涉及linux存儲管理和文件系統的內容,可以參考相關的重要數據結構加深理解。在本主題的後面部分,將介紹system v***內存共享的實現。
參考數據
[1]理解Linux內核,第二版,作者Daniel P. Bovet,Marco Cesati,重點介紹了每個主題,內容清晰。
[2] UNIX網絡編程第二卷:進程間通信,作者W.Richard Stevens,譯者:楊·,清華大學出版社。Mmap()詳細闡述。
[3]《Linux內核源代碼的場景分析(壹)》,毛·、胡·,浙江大學出版社,給出了mmap()相關的源代碼分析。
[4]mmap()手冊