共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新,反之亦然。由於多個進程共享同一塊內存區域,必然需要某種同步機制,互斥鎖和信號量都可以。
采用共享內存通信的一個顯而易見的好處是效率高,因為進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據[1]:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據后就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢為止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,采用共享內存的通信方式效率是非常高的。
默認情況下通過fork派生的子進程並不與父進程共享內存區。通過一個程序來驗證,程序功能是讓父子進程都給一個名為count的全局變量加1操作,程序如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <semaphore.h> 6 #include <fcntl.h> 7 8 #define SEM_NAME "mysem" 9 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) 10 int count = 0; 11 12 int main(int argc,char* argv[]) 13 { 14 int i,nloop; 15 sem_t *mutex; 16 if(argc != 2) 17 { 18 printf("usage: incrl <#loops>"); 19 exit(0); 20 } 21 nloop = atoi(argv[1]); 22 //創建有名信號量 23 mutex = sem_open(SEM_NAME,O_RDWR|O_CREAT|O_EXCL,FILE_MODE,1); 24 sem_unlink(SEM_NAME); 25 //將stdout設置為非緩沖的 26 setbuf(stdout,NULL); 27 //子進程開始執行增加1 28 if(fork() == 0) 29 { 30 for(i = 0;i<nloop;++i) 31 { 32 sem_wait(mutex); 33 printf("child: %d\n",count++); 34 sem_post(mutex); 35 } 36 exit(0); 37 } 38 //父進程執行增加1操作 39 for(i = 0;i<nloop;++i) 40 { 41 sem_wait(mutex); 42 printf("parent: %d\n",count++); 43 sem_post(mutex); 44 } 45 //等待子進程退出 46 wait(NULL); 47 exit(0); 48 }
程序執行結果如下所示:
從結果可以看出父子進程都有各自的全局變量count的副本,每個進程都從該變量為0的初始值開始的,每次增加的對象是各自的變量的副本。
共享內存操作函數:
1、系統調用mmap()
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
mamap函數把一個文件或一個Posix共享內存區對象映射到調用進程的地址空間。參數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指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值為最后文件映射到進程空間的地址,進程可直接操作起始地址為該值的有效地址。
2、系統調用munmap()
int munmap( void * addr, size_t len )
munmap函數從某個進程的地址空間中刪除一個映射關系。addr是調用mmap()時返回的地址,len是映射區的大小。當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生。
3、系統調用msync()
int msync ( void * addr , size_t len, int flags)
一般說來,進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,往往在調用munmap()后才執行該操作。可以通過調用msync()實現磁盤上文件內容與共享內存區的內容一致。
現使用共享內存實現在內存映射文件中個計數器持續加1,程序如下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <semaphore.h> 6 #include <fcntl.h> 7 #include <sys/mman.h> 8 #include <errno.h> 9 10 #define SEM_NAME "mysem" 11 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) 12 13 int main(int argc,char* argv[]) 14 { 15 int fd,i,nloop,zero = 0; 16 int *ptr; 17 sem_t *mutex; 18 if(argc != 3) 19 { 20 printf("usage: incrl <#loops>"); 21 exit(0); 22 } 23 nloop = atoi(argv[2]); 24 //打開文件 25 fd = open(argv[1],O_RDWR|O_CREAT,FILE_MODE); 26 //向文件中寫入0值 27 write(fd,&zero,sizeof(int)); 28 //將文件映射到進程地址空間,返回被映射區的起始地址 29 ptr = mmap(NULL,sizeof(int),PROT_READ| PROT_WRITE,MAP_SHARED,fd,0); 30 if(ptr == MAP_FAILED) 31 { 32 perror("mmap() error"); 33 exit(0); 34 } 35 close(fd); 36 mutex = sem_open(SEM_NAME,O_RDWR|O_CREAT|O_EXCL,FILE_MODE,1); 37 sem_unlink(SEM_NAME); 38 setbuf(stdout,NULL); 39 if(fork() == 0) 40 { 41 for(i = -0;i<nloop;++i) 42 { 43 sem_wait(mutex); 44 printf("child: %d\n",(*ptr)++); 45 sem_post(mutex); 46 } 47 exit(0); 48 } 49 for(i = 0;i<nloop;++i) 50 { 51 sem_wait(mutex); 52 printf("parent: %d\n",(*ptr)++); 53 sem_post(mutex); 54 } 55 wait(NULL); 56 exit(0); 57 }
程序執行結果如下所示:
從結果可以看出父子進程共享內存區。可以將這個程序改成使用[osix基於內存的信號量,而不是一個Posix有名信號量,並把該信號量存放在共享內存中。程序如下所示:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <semaphore.h> 6 #include <fcntl.h> 7 #include <sys/mman.h> 8 #include <errno.h> 9 10 #define SEM_NAME "mysem" 11 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) 12 13 //共享內存結構 14 struct shared 15 { 16 sem_t mutex; //信號量 17 int count; //計數器 18 }shared; 19 20 int main(int argc,char* argv[]) 21 { 22 int fd,i,nloop; 23 struct shared *ptr; 24 if(argc != 3) 25 { 26 printf("usage: incrl <#loops>"); 27 exit(0); 28 } 29 nloop = atoi(argv[2]); 30 fd = open(argv[1],O_RDWR|O_CREAT,FILE_MODE); 31 write(fd,&shared,sizeof(struct shared)); 32 ptr = mmap(NULL,sizeof(struct shared),PROT_READ| PROT_WRITE,MAP_SHARED,fd,0); 33 if(ptr == MAP_FAILED) 34 { 35 perror("mmap() error"); 36 exit(0); 37 } 38 close(fd); 39 sem_init(&ptr->mutex,1,1); 40 setbuf(stdout,NULL); 41 if(fork() == 0) 42 { 43 for(i = -0;i<nloop;++i) 44 { 45 sem_wait(&ptr->mutex); 46 printf("child: %d\n",ptr->count++); 47 sem_post(&ptr->mutex); 48 } 49 exit(0); 50 } 51 for(i = 0;i<nloop;++i) 52 { 53 sem_wait(&ptr->mutex); 54 printf("parent: %d\n",ptr->count++); 55 sem_post(&ptr->mutex); 56 } 57 wait(NULL); 58 exit(0); 59 }
程序執行結果與上面的一致。從上面的程序發現,我們在進行文件映射的時候,當文件不存在的時候需要在文件系統中創建一個文件然后打開。4.4BSD提供匿名內存映射,避免了文件的創建和打開。其解決辦法是將mmap的flags的參數指定為MAP_SHARED | MAP_ANON,把fd參數指定為-1,offset參數則被忽略。這樣的內存區初始化為0。實現如下所示:
int *ptr; ptr = mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED | MAP_ANON,-1,0);
SVR4提供了/dev/zero設備文件,從該設備讀是返回的字節全為0,寫往該設備的任何字節被丟棄。實現如下所示:
int *ptr; fd = open("dev/zero",O_RDWR); ptr = mmap(NULL,sizeof(int),PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);