1. 程式人生 > >Linux程序間通訊--mmap共享記憶體(一)

Linux程序間通訊--mmap共享記憶體(一)

 共享記憶體可以說是最有用的程序間通訊方式,也是最快的IPC形式。兩個不同程序A、B共享記憶體的意思是,同一塊實體記憶體被對映到程序A、B各自的程序地址空間。程序A可以即時看到程序B對共享記憶體中資料的更新,反之亦然。由於多個程序共享同一塊記憶體區域,必然需要某種同步機制,互斥鎖和訊號量都可以。

採用共享記憶體通訊的一個顯而易見的好處是效率高,因為程序可以直接讀寫記憶體,而不需要任何資料的拷貝。對於像管道和訊息佇列等通訊方式,則需要在核心和使用者空間進行四次的資料拷貝,而共享記憶體則只拷貝兩次資料[1]:一次從輸入檔案到共享記憶體區,另一次從共享記憶體區到輸出檔案。實際上,程序之間在共享記憶體時,並不總是讀寫少量資料後就解除對映,有新的通訊時,再重新建立共享記憶體區域。而是保持共享區域,直到通訊完畢為止,這樣,資料內容一直儲存在共享記憶體中,並沒有寫回檔案。共享記憶體中的內容往往是在解除對映時才寫回檔案的。因此,採用共享記憶體的通訊方式效率是非常高的。

Linux的2.2.x核心支援多種共享記憶體方式,如mmap()系統呼叫,Posix共享記憶體,以及系統V共享記憶體。linux發行版本如Redhat 8.0支援mmap()系統呼叫及系統V共享記憶體,但還沒實現Posix共享記憶體,本文將主要介紹mmap()系統呼叫及系統V共享記憶體API的原理及應用。

2.mmap系統呼叫

mmap系統呼叫是的是的程序間通過對映同一個普通檔案實現共享記憶體.普通檔案被對映到程序地址空間後,程序可以向像訪問普通記憶體一樣對檔案進行訪問,不必再呼叫read,write等操作.與mmap系統呼叫配合使用的系統呼叫還有munmap,msync等.

實際上,mmap系統呼叫並不是完全為了用於共享記憶體而設計的.它本身提供了不同於一般對普通檔案的訪問方式,是程序可以像讀寫記憶體一樣對普通檔案操作.而Posix或System V的共享記憶體則是純粹用於共享記憶體的,當然mmap實現共享記憶體也是主要應用之一.

#include <sys/mman.h>  
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
各個引數的說明如下:
  • start:對映區的開始地址,設定為0時表示由系統決定對映區的起始地址.
  • length:對映區的長度.長度單位是以記憶體頁為單位.
  • prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突.是以下的某個值,可以通過or運算合理地組合在一起.
    • PROT_EXEC //頁內容可以被執行
    • PROT_READ //頁內容可以被讀取
    • PROT_WRITE //頁可以被寫入
    • PROT_NONE //頁不可訪問
  • flags:指定對映物件的型別,對映選項和對映頁是否可以共享.它的值可以是一個或者多個以下位的組合體.
    • MAP_FIXED //使用指定的對映起始地址,如果由start和len引數指定的記憶體區重疊於現存的對映空間,重疊部分將會被丟棄.如果指定的起始地址不可用,操作將會失敗.並且起始地址必須落在頁的邊界上.
    • MAP_SHARED //與其它所有對映這個物件的程序共享對映空間.對共享區的寫入,相當於輸出到檔案.直到msync()或者munmap()被呼叫,檔案實際上不會被更新.
    • MAP_PRIVATE //建立一個寫入時拷貝的私有對映.記憶體區域的寫入不會影響到原檔案.這個標誌和以上標誌是互斥的,只能使用其中一個.
    • MAP_DENYWRITE //這個標誌被忽略.
    • MAP_EXECUTABLE //同上
    • MAP_NORESERVE //不要為這個對映保留交換空間.當交換空間被保留,對對映區修改的可能會得到保證.當交換空間不被保留,同時記憶體不足,對對映區的修改會引起段違例訊號.
    • MAP_LOCKED //鎖定對映區的頁面,從而防止頁面被交換出記憶體.
    • MAP_GROWSDOWN //用於堆疊,告訴核心VM系統,對映區可以向下擴充套件.
    • MAP_ANONYMOUS //匿名對映,對映區不與任何檔案關聯.
    • MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用.
    • MAP_FILE //相容標誌,被忽略.
    • MAP_32BIT //將對映區放在程序地址空間的低2GB,MAP_FIXED指定時會被忽略.當前這個標誌只在x86-64平臺上得到支援。
    • MAP_POPULATE //為檔案對映通過預讀的方式準備好頁表.隨後對對映區的訪問不會被頁違例阻塞.
    • MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義.不執行預讀,只為已存在於記憶體中的頁面建立頁表入口.
  • fd:有效的檔案描述詞.一般是由open()函式返回,其值也可以設定為-1,此時需要指定flags引數中的MAP_ANON,表明進行的是匿名對映.
  • offset:被對映物件內容的起點.一般設為0,表示從檔案頭開始對映.
函式的返回值為最後檔案對映到程序空間的地址,程序可以直接操作起始地址為該值的有效地址.系統呼叫mmap用於共享記憶體時有下面兩種常用的方式:
  • 1)使用普通檔案提供的記憶體對映:適用於任何程序之間.此時,需要開啟或建立一個檔案,然後再呼叫mmap,這種方式有許多特點和要注意的地方,我們在後面或舉例子說明.
  • 2)使用特殊檔案提東匿名記憶體對映:適用於具有親緣關係的程序之間.由於父子程序特殊的親緣關係,在父程序中吊牌用mmap,然後呼叫fork.那麼在呼叫fork之後,子程序急促繼承父程序匿名對映後的地址空間,同樣也繼承mmap返回的地址,這樣,父子程序就可以通過對映區進行通訊了.注意,mmap返回的地址,需要由父程序共同維護.

對於任意的兩個程序可以使用第一種方式,而對於具有親緣關係的程序實現共享記憶體最好的方式應該是採用匿名記憶體對映的方式.此時,不必指定具體的檔案,只要設定相應的標誌即可,後面有相應的例子.

3.munmap系統呼叫

該系統呼叫在程序地址空間中解除一個對映關係,當對映關係解除後,對原來對映地址的訪問將導致段錯誤發生.其函式原型為:

int munmap(void *start, size_t length);  
其中,引數addr是呼叫mmap時返回的地址,len是對映區的大小.

4.msync系統呼叫

一般來說,程序在對映空間對共享內容的改變並不直接寫回到磁碟檔案中,往往在呼叫munmap後才執行該操作.可以通過呼叫msync實現磁碟上檔案內容與共享記憶體區的內容一致.該函式的原型如下:

int msync ( void * addr, size_t len, int flags);  
addr:檔案對映到程序空間的地址;
len:對映空間的大小;
flags:重新整理的引數設定,可以取值MS_ASYNC/MS_SYNC/MS_INVALIDATE
  • 取值為MS_ASYNC(非同步)時,呼叫會立即返回,不等到更新的完成;
  • 取值為MS_SYNC(同步)時,呼叫會等到更新完成之後返回;
  • 取MS_INVALIDATE(通知使用該共享區域的程序,資料已經改變)時,在共享內容更改之後,使得檔案的其他對映失效,從而使得共享該檔案的其他程序去重新獲取最新值.

5.應用例項

該例項包含兩個子程式,這兩個子程式編譯為mmap_read和mmap_write.兩個程式通過命令列引數指定痛一個檔案來實現共享記憶體方式的程序間通訊.mmap_write試圖開啟命令列引數指定的一個普通檔案,把該檔案對映到程序的地址空間,然後對對映後的地址空間進行寫操作.mmap_read把命令列引數指定的檔案對映到程序的地址空間,然後對對映的地址空間執行讀操作.這樣,兩個程序通過命令列引數指定同一個檔案來實現共享記憶體方式的程序間通訊.寫操作操作子程式原始碼如下:

/**************************************************************************************/
/*簡介:System V共享記憶體						 */
/*************************************************************************************/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct
{
	char name[4];
	int age;
} people;

int main(int argc, char** argv)
{
	int shm_id,i;
	people *p_map;
	char name[4];
	shm_id=shmget(IPC_PRIVATE,2048,0600);	
	if(shm_id<0)
	{
		perror("shmget error");
		return 1;
	}
	/*寫操作*/

	p_map=(people*)shmat(shm_id,0,0);
	if (p_map<0)
	{
		perror("shmmat error");
		return 1;
	}

	name[0]='a';
	name[1]='\0';
	for(i = 0;i<10;i++)
	{
		name[0]++;
		memcpy((*(p_map+i)).name,&name,sizeof(name));
		(*(p_map+i)).age=20+i;
	}
	if(shmdt(p_map)<0)
		perror(" detach error ");
	/*讀操作*/
	p_map = (people*)shmat(shm_id,NULL,0);
	for(i = 0;i<10;i++)
	{
		printf( "name:%s\n",(*(p_map+i)).name );
		printf( "age %d\n",(*(p_map+i)).age );
	}

	if(shmdt(p_map)<0)
		perror(" detach error ");

	return 0;
}
在上面的程式中,首先定義了一個people的資料格式(共享記憶體區的資料往往有固定的格式,由通訊的各個程序決定,採用結構的方式具有普遍代表性).mmap_write首先開啟或建立一個檔案,並把檔案的長度設為5個people結構大小.然後從mmap的返回地址開始,設定了10個people結構.然後程序睡眠10s,等待其他程序對映同一個檔案,最後解除對映.讀操作子程式的原始碼如下:
/**************************************************************************************/
/*簡介:mmap_read共享記憶體,讀操作子程式						 */
/*************************************************************************************/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
	char name[4];
	int  age;
}people;

int main(int argc, char** argv)
{
	int fd,i;
	people *p_map;
	if (argc != 2)
	{
		perror("usage: mmap_read <mmap file>");
		return 1;
	}
	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);
	close(fd);
	for(i = 0;i<10;i++)
	{
		printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
	}
	munmap( p_map,sizeof(people)*10 );
	return 0;
}
mmap_read只是簡單的對映一個檔案,並以people資料結構的格式從mmap返回的地址處讀取10個people結構,並輸出讀取的值,然後解除對映.下圖是執行結果. 檔案被對映後,呼叫mmap的程序對返回地址的訪問其實是對某一記憶體區域的訪問,暫時脫離了磁碟上檔案的影響.所有對mmap返回地址空間的操作只是在記憶體中才有意義,只有在呼叫了munmap或或者msync時,才把記憶體中的相應內容寫回磁碟檔案.