共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式,因為進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據: 一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢為止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,采用共享內存的通信方式效率是非常高的。
一. 傳統文件訪問
UNIX訪問文件的傳統方法是用open打開它們, 如果有多個進程訪問同一個文件, 則每一個進程在自己的地址空間都包含有該
文件的副本,這不必要地浪費了存儲空間. 下圖說明了兩個進程同時讀一個文件的同一頁的情形. 系統要將該頁從磁盤讀到高
速緩沖區中, 每個進程再執行一個存儲器內的復制操作將數據從高速緩沖區讀到自己的地址空間.
二. 共享存儲映射
現在考慮另一種處理方法: 進程A和進程B都將該頁映射到自己的地址空間, 當進程A第一次訪問該頁中的數據時, 它生成一
個缺頁中斷. 內核此時讀入這一頁到內存並更新頁表使之指向它.以後, 當進程B訪問同一頁面而出現缺頁中斷時, 該頁已經在
內存, 內核只需要將進程B的頁表登記項指向次頁即可. 如下圖所示:
三、mmap()及其相關系統調用
mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪
問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。
mmap函數把一個文件或一個Posix共享內存區對象映射到調用進程的地址空間。使用該函數有三個目的:
(1)使用普通文件以提供內存映射I/O;
(2)使用特殊文件以提供匿名內存映射;
(3)使用shm_open以提供無親緣關系進程間的Posix共享內存區。
#include <sys/mman.h> void *mmap(void *addr,size_t len,int prot,int flags,int fd,off_t off);
其中addr可以指定描述符fd應被映射到的進程內空間的起始地址。它通常被指定為一個空指針,這樣告訴內核自己去選擇起始地址。無論哪種情況下,該函數的返回值都是描述符fd所映射到內存區的起始地址。
注意:fd指定要被映射文件的描述符,在映射該文件到一個地址空間之前,先要打開該文件。
同時,fd可以指定為-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關系的進程間通信)。
len是映射到調用進程地址空間中的字節數,它從被映射文件開頭起第off個字節處開始算。off通常設置為0.
內存映射區得保護由prot參數指定,它使用如下的常值。該參數的常見值是代表讀寫訪問的PROT_READ | PROT_WRITE。
對指定映射區的prot參數指定,不能超過文件open模式訪問權限。例如:若該文件是只讀打開的,那麼對映射存儲區就不能指定PROT_WRITE。
flags使用如下的常值指定。MAP_SHARED或MAP_PRIVATE這兩個標志必須指定一個,並可有選擇的或上MAP_FIXED。如果指定了MAP_PRIVATE,那麼調用進程被映射數據所作的修改只對該進程可見,而不改變其底層支撐對象(或者是一個文件獨享,或者是一個共享內存區對象)。如果指定了MAP_SHARED,那麼調用進程對被映射數據所作的修改對於共享該對象的所有進程都可見,而且確實改變了其底層支撐對象。
mmap成功返回後,fd參數可以關閉。該操作對於由mmap建立的映射關系沒有影響。
為從某個進程的地址空間刪除一個映射關系,我們調用munmap。
#include <sys/mman.h> int munmap(void *addr,szie_t len);
其中addr參數由mmap返回的地址,len是映射區的大小。再次訪問這些地址將導致向調用進程產生一個SIGSEGV信號(當然這裡假設以後的mmap調用並不重用這部分地址空間)。
如果被映射區是使用MAP_PRIVATE標志映射的,那麼調用進程對它所作的變動都會被丟棄掉,即不會同步到文件中。
注意:進程終止時,或調用munmap之後,存儲映射區就被自動解除映射。關閉文件描述符fd並不解除,munmap不會影響被映射的對象,在解除了映射之後,對於MAP_PRIVATE存儲區的修改被丟棄。
內核的虛擬內存算法保持內存映射文件(一般在硬盤上)與內存映射區(在內存中)的同步,前提是它是一個MAP_SHARED內存區。這就是說,如果我們修改了處於內存映射到某個文件的內存區中某個位置的內容,那麼內核將在稍後的某個時刻相應的更新文件。然而有時候我們希望確信硬盤上的文件內容與內存映射區中的內容一致,於是調用msync來執行這種同步。
#include <sys/mman.h> int msync(void *addr,size_t len,int flags);
其中addr和len參數通常指代內存中的整個內存映射區,不過也可以指定該內存區的一個子集。flags參數如下所示的各常值的組合。
MS_ASYNC和MS_SYNC這兩個常值中必須指定一個,但不能都指定。他們的差別是,一旦寫操作已由內核排入隊列,MS_ASYNC即返回,而MS_SYNC則要等到寫操作完成後才返回。如果指定了MS_INVALIDATE,那麼與其最終副本不一致的文件數據的所有內存中副本都失效。後續的引用將從文件中取得數據。
為何使用mmap
到此為止就mmap的描述符間接說明了內存映射文件:我們open它之後調用mmap把它映射到調用進程地址空間的某個文件。使用內存映射文件得到的奇妙特性是,所有的I/O都在內核的掩蓋下完成,我們只需編寫存取內存映射區中各個值得代碼。我們決不調用read,write或lseek。這麼一來往往可以簡化我們的代碼。
然而需要了解以防誤解的說明是,不是所有文件都能進行內存映射。例如,試圖把一個訪問終端或套接字的描述符映射到內存將導致mmap返回一個錯誤。這些類型的描述符必須使用read和write(或者他們的變體)來訪問。
mmap的另一個用途是在無親緣關系的進程間提供共享內存區。這種情形下,所映射文件的實際內容成了被共享內存區的初始內容,而且這些進程對該共享內存區所作的任何變動都復制回所映射的文件(以提供隨文件系統的持續性)。這裡假設指定了MAP_SHARED標志,它是進程間共享內存所需求的。
示例代碼:
1 通過共享映射的方式修改文件
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 將文件映射至進程的地址空間 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) { perror("mmap"); } /* 映射完後, 關閉文件也可以操縱內存 */ close(fd); printf("%s", mapped); /* 修改一個字符,同步到磁盤文件 */ mapped[20] = '9'; if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) { perror("msync"); } /* 釋放存儲映射區 */ if ((munmap((void *)mapped, sb.st_size)) == -1) { perror("munmap"); } return 0; }
注釋掉44-46與沒有注釋,運行結果都為:
huangcheng@ubuntu:~$ cat data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff huangcheng@ubuntu:~$ ./a.out data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff huangcheng@ubuntu:~$ cat data.txt aaaaaaaaaaaa bbbbbbb9bbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff
2 私有映射無法修改文件
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 將文件映射至進程的地址空間,這裡是私有映射 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) { perror("mmap"); } /* 映射完後, 關閉文件也可以操縱內存 */ close(fd); printf("%s", mapped); /* 修改一個字符,同步到磁盤文件 */ mapped[20] = '9'; if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) { perror("msync"); } /* 釋放存儲映射區 */ if ((munmap((void *)mapped, sb.st_size)) == -1) { perror("munmap"); } return 0; }
運行結果:
huangcheng@ubuntu:~$ cat data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff huangcheng@ubuntu:~$ ./a.out data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff huangcheng@ubuntu:~$ cat data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff
3.兩個進程中通信
兩個程序映射同一個文件到自己的地址空間, 進程A先運行, 每隔兩秒讀取映射區域, 看是否發生變化. 進程B後運行, 它修改映射區域, 然後推出, 此時進程A能夠觀察到存儲映射區的變化。
進程A的代碼:
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 將文件映射至進程的地址空間 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) { perror("mmap"); } /* 文件已在內存, 關閉文件也可以操縱內存 */ close(fd); /* 每隔兩秒查看存儲映射區是否被修改 */ while (1) { printf("%s\n", mapped); sleep(2); } return 0; }
進程B的代碼:
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 將文件映射至進程的地址空間 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) { perror("mmap"); } /* 映射完後, 關閉文件也可以操縱內存 */ close(fd); /* 修改一個字符 */ mapped[20] = '9'; return 0; }
運行結果:
huangcheng@ubuntu:~$ ./a data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff aaaaaaaaaaaa bbbbbbb9bbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff aaaaaaaaaaaa bbbbbbb9bbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff ............
如果進程B中的映射設置為私有映射,運行結果:
huangcheng@ubuntu:~$ ./a data.txt aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff aaaaaaaaaaaa bbbbbbbbbbbb cccccccccccc dddddddddddd eeeeeeeeeeee ffffffffffff ............
4. 匿名映射實現父子進程通信
#include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 100 int main(int argc, char** argv) { char *p_map; /* 匿名映射,創建一塊內存供父子進程通信 */ p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if(fork() == 0) { sleep(1); printf("child got a message: %s\n", p_map); sprintf(p_map, "%s", "hi, dad, this is son"); munmap(p_map, BUF_SIZE); //實際上,進程終止時,會自動解除映射。 exit(0); } sprintf(p_map, "%s", "hi, this is father"); sleep(2); printf("parent got a message: %s\n", p_map); return 0; }
四、對mmap地址的訪問
#include <sys/mman.h> void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset); int munmap(void *start, size_t length);
mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。
mmap()必須以PAGE_SIZE()為單位進行映射,而內存也只能以頁為單位進行映射,若要映射非PAGE_SIZE整數倍的地址范圍,要先進行內存對齊,強行以PAGE_SIZE的倍數大小進行映射。
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
mmap操作提供了一種機制,讓用戶程序直接訪問設備內存,這種機制,相比較在用戶空間和內核空間互相拷貝數據,效率更高。在要求高性能的應用中比較常用。mmap映射內存必須是頁面大小的整數倍,面向流的設備不能進行mmap,mmap的實現和硬件有關。
內存映射一個普通文件時,內存中映射區的大小(mmap的第二個參數)通常等於該文件的大小,然而文件的大小和內存的映射區大小可以不同。
我們要展示的第一種情形的前提是:文件大小等於內存映射區大小,但這個大小不是頁面大小的倍數。
#include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <sys/mman.h> #define max(A,B) (((A)>(B))?(A):(B)) int main(int argc,char ** argv) { int fd,i; char *ptr; size_t filesize,mmapsize,pagesize; if(argc != 4) printf("usage:tes1 <pathname> <filesname> <mmapsize>\n"); filesize = atoi(argv[2]); mmapsize = atoi(argv[3]); /*open file:create or truncate;set file size*/ fd = open(argv[1],O_RDWR | O_CREAT | O_TRUNC,0777); lseek(fd,filesize-1,SEEK_SET); write(fd," ",1); ptr = mmap(NULL,mmapsize,PROT_READ |PROT_WRITE,MAP_SHARED,fd,0); close(fd); pagesize = sysconf(_SC_PAGESIZE); printf("PAGESIZE = %ld\n",(long)pagesize); for(i = 0;i < max(filesize,mmapsize); i += pagesize) { printf("ptr[%d] = %d\n",i,ptr[i]); ptr[i] = 1; printf("ptr[%d] = %d\n",i + pagesize - 1,ptr[i + pagesize - 1]); ptr[i + pagesize - 1] = 1; } printf("ptr[%d] = %d\n",i,ptr[i]); exit(0); }
命令行參數
16-19 命令行參數有三個,分別指定即將創建並映射到內存的文件的路徑名,該文件將被設置成的大小以及內存映射區得大小。
創建,打開並截斷文件;設置文件大小
22-24 待打開的文件若不存在則創建之,若已存在則把它的大小截短成0.接著把該文件的大小設置成由命令行參數指定的大小,辦法是把文件讀寫指針移動到這個大小減去1的字節位置,然後寫1個字節。
內存映射文件
25-26 使用作為最後一個命令行參數指定的大小對該文件進行內存映射。其描述符隨後被關閉。
輸出頁面大小
28-29 使用sysconf獲取系統實現的頁面大小並將其輸出。
讀出和存入內存映射區
31-38 讀出內存映射區中每個頁面的首字節和尾字節,並輸出他們的值。我們預期這些值全為0.同時把每個頁面的這兩個字節設置為1,。我們預期某個引用會最終引發一個信號,它將終止程序。當for循環結束時,我們輸出下一頁的首字節,並預期這會失敗。
運行結果:
huangcheng@ubuntu:~$ ls -l test ls: 無法訪問test: 沒有那個文件或目錄 huangcheng@ubuntu:~$ ./a.out test 5000 5000 PAGESIZE = 4096 ptr[0] = 0 ptr[4095] = 0 ptr[4096] = 0 ptr[8191] = 0 段錯誤 huangcheng@ubuntu:~$ ls -l test -rwxr-xr-x 1 huangcheng huangcheng 5000 2013-07-09 15:48 test huangcheng@ubuntu:~$ od -b -A d test 0000000 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 0000016 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004080 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 001 0004096 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 0004112 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004992 000 000 000 000 000 000 000 040 0005000
頁面大小為4096字節,我們能夠讀完整的第2頁(下標為4096-8191),但是訪問第3頁時(下標為8192)引發SIGSEGV信號,shell將它輸出成"Segmentation Fault(分段障)"。盡管我們把ptr[8191]設置成1,它也不寫到test文件中,因而該文件的大小仍然是5000.內核允許我們讀寫最後一頁中映射區以遠部分(內核的內存保護是以頁面為單位的)。但是我們寫向這部分擴展區的任何內容都不會寫到test文件中。設置成1的其他3個字節(下標分別為0,4905和4906)復制回test文件,這一點可使用od命令來驗證。(-b選項指定以八進制數輸出各個字節,-A d選項指定以十進制數輸出地址。)
我們仍然能訪問內存映射區以遠部分,不過只能在邊界所在的那個內存頁面內(下標為5000-8191)。訪問ptr[8192]將引發SIGSEGV信號,這是我們預期的。
現在我們把內存映射區大小(15000字節)指定成大於文件大小(5000字節)。
huangcheng@ubuntu:~$ ./a.out test 5000 15000 PAGESIZE = 4096 ptr[0] = 0 ptr[4095] = 0 ptr[4096] = 0 ptr[8191] = 0 總線錯誤 huangcheng@ubuntu:~$ ls -l test -rwxr-xr-x 1 huangcheng huangcheng 5000 2013-07-09 16:00 test
其結果與先前那個文件大小等於內存映射區大小(都是5000字節)的例子類似。本例子引發SIGBUS信號(其shell輸出為"Bus Error(總線出錯)"),前一個例子則引發SIGSEGV信號。兩者的差別是,SIGBUS意味著我們是在內存映射區訪問的,但是已超出了底層支撐對象的大小。上一個例子中的SIGSEGV則意味著我們已在內存映射區以遠訪問。可以看出,內核知道被映射的底層支撐對象(本例子中為文件test)的大小,即使我們訪問不了該對象以遠的部分(最後一頁上該對象以遠的那些字節除外,他們的下標為5000-8191)。
注意:
huangcheng@ubuntu:~$ ./a.out test 5000 1000 PAGESIZE = 4096 ptr[0] = 0 ptr[4095] = 0 段錯誤 huangcheng@ubuntu:~$ od -b -A d test 0000000 001 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 0000016 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004080 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 001 //修改為1了 0004096 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 * 0004992 000 000 000 000 000 000 000 040 0005000
當文件長度為5000字節,映射1000字節時,但是1000字節不是PAGE_SIZE(4096)的整數倍,這樣會強制映射為PAGE_SIZE的整數倍,現在映射的是4096字節。所以對映射區裡面的第4096字節進行修改為1,會同步到文件中。
下面的程序展示了處理一個持續增長的文件的一種常用技巧:指定一個大於該文件大小的內存映射區大小,跟蹤該文件的當前大小(以確保不訪問當前文件尾以遠的部分),然後就讓該文件的大小隨著往其中每次寫入數據而增長。
#include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <sys/types.h> #define FILE "test.data" #define SIZE 32768 int main(int argc,char **argv) { int fd,i; char *ptr; /*open:create or truncate;then mmap file */ fd = open(FILE,O_RDWR | O_CREAT | O_TRUNC,0777); ptr = mmap(NULL,SIZE,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0); for(i = 4096;i <= SIZE;i += 4096) { printf("setting file size to %d\n",i); ftruncate(fd,i); printf("ptr[%d] = %d\n",i-1,ptr[i-1]); } exit(0); }
打開文件
15-17 打開一個文件,若不存在則創建之,若已存在則把它截短成大小為0.以32768字節的大小對該文件進行內存映射,盡管它當前的大小為0.
增長文件大小
19-24 通過調用ftruncate函數把文件的大小每次增長4096字節,然後取出現在是該文件最後一個字節的那個字節。
現在運行這個持續,我們看到隨著文件的大小的增長,我們能通過所建立的內存映射區訪問新的數據。
huangcheng@ubuntu:~$ ls -l test.data ls: 無法訪問test.data: 沒有那個文件或目錄 huangcheng@ubuntu:~$ ./a.out setting file size to 4096 ptr[4095] = 0 setting file size to 8192 ptr[8191] = 0 setting file size to 12288 ptr[12287] = 0 setting file size to 16384 ptr[16383] = 0 setting file size to 20480 ptr[20479] = 0 setting file size to 24576 ptr[24575] = 0 setting file size to 28672 ptr[28671] = 0 setting file size to 32768 ptr[32767] = 0 huangcheng@ubuntu:~$ ls -l test.data -rwxr-xr-x 1 huangcheng huangcheng 32768 2013-07-09 16:47 test.data
本例子表明,內核跟蹤著被內存映射的底層支撐對象(本例子中為文件test.data)的大小,而且我們總是能訪問在當前文件大小以內又在內存映射區以內的那些字節。