一.概述 記憶體映射是在調用進程的虛擬地址空間創建一個新的記憶體映射。記憶體映射分為2種:1.文件映射:將一個普通文件的全部或者一部分映射到進程的虛擬記憶體中。映射後,進程就可以直接在對應的記憶體區域操作文件內容!2.匿名映射:匿名映射沒有對應的文件或者對應的文...
一.概述
記憶體映射是在調用進程的虛擬地址空間創建一個新的記憶體映射。
記憶體映射分為2種:
1.文件映射:將一個普通文件的全部或者一部分映射到進程的虛擬記憶體中。映射後,進程就可以直接在對應的記憶體區域操作文件內容!
2.匿名映射:匿名映射沒有對應的文件或者對應的文件時虛擬文件(如:/dev/zero),映射後會把記憶體分頁全部初始化為0。
當多個進程映射了同一個記憶體區域時,它們會共用物理記憶體的相同分頁。通過fork()創建的子進程也會繼承父進程的映射副本!!!
如果多個進程都會同一個記憶體區域操作時,會根據映射的特性,會有不同的行為。映射特征可分為私有映射和共用映射:
1.私有映射:映射的內容對其他進程不可見。對於文件映射來說,某一個進程在映射記憶體中改變文件的內容不會反映到被映射的底層文件中。內核會使用copy-on-write(寫時複製)技術來解決這個問題:只要有一個進程修改了分頁中的內容,內核會為該進程重新創建一個新的分頁,並將需要修改的內容複製到新分頁中。
2.共用映射:某一個進程對共用的記憶體區域操作都對其他進程可見!!!對於文件映射,操作的內容會反映到底層文件中。
註意:進程執行exec()調用後,先前的記憶體映射會丟失,而fork()創建的子進程會繼承父進程的,映射的特征(私有和共用)也會被繼承。
異常信號:
1.當映射記憶體的屬性設置只讀時,如果進行寫操作會產生SIGSEGV信號。
2.當映射記憶體的位元組數大於被映射文件的大小,且大於該文件當前的記憶體分頁大小時。如果訪問的區域超過了該文件分頁大小,會產生SIGBUS信號。
有點繞口,舉個簡單的例子:假設內核維護的記憶體分頁是4k(一般都是4k,4096位元組),一個普通文件a.txt的大小是10位元組。如果創建一個映射記憶體為4097位元組,並映射該文件。此時,因為a.txt的大小用一個分頁就可以完全映射,10位元組遠小於一個分頁的4096位元組,所以內核只會給它一個分頁。記憶體地址是從0開始,0-9區間的內容對應a.txt文件的數據,我們也是可以訪問10-4095的區間。但如果訪問4096區間時,已經超過一個分頁的大小了,此時會產生SIGBUS信號!!!
等會我們用個簡單的例子演示下這2個異常。
二.函數介面
1.創建映射
1 #include <sys/mman.h> 2 3 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:映射後要存放的虛擬記憶體地址。如果是NULL,內核會自動幫你選擇。
length:映射記憶體的位元組數。
prot:許可權保護:PROT_NONE(無法訪問),PROT_READ(可讀),PROT_WRITE(可寫),PROT_EXEC(可執行)。
flags:映射特征:MAP_PRIVATE(私有),MAP_SHARED(共用),MAP_ANONYMOUS。還有一些其他的可查詢man手冊。
fd:要映射的文件描述符。
offset:文件的偏移量,如果為0,且length為文件長度,代表映射整個文件。
2.解除映射
1 #include <sys/mman.h> 2 3 int munmap(void *addr, size_t length);
addr:要解除記憶體的起始地址。如果addr不在剛剛映射區域的開始位置,解除一部分後記憶體區域可能會分成兩半!!!
length:要解除的位元組數。
3.同步映射區
1 #include <sys/mman.h> 2 3 int msync(void *addr, size_t length, int flags);
addr:要同步的記憶體起始地址。
length:要同步的位元組長度。
flags:MS_SYNC(執行同步文件寫入),此操作內核會把內容直接寫到磁碟。MS_ASYNC(執行非同步文件寫入),此操作內核會先把內容寫到內核的緩衝區,某個合適的時候再寫到磁碟。
三.文件映射實例
1 /** 2 * @file mmap_file.c 3 */ 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <signal.h> 10 #include <unistd.h> 11 #include <sys/mman.h> 12 13 #define MMAP_FILE_NAME "a.txt" 14 #define MMAP_FILE_SIZE 10 15 16 void err_exit(const char *err_msg) 17 { 18 printf("error:%s\n", err_msg); 19 exit(1); 20 } 21 22 /* 信號處理器 */ 23 void signal_handler(int signum) 24 { 25 if (signum == SIGSEGV) 26 printf("\nSIGSEGV handler!!!\n"); 27 else if (signum == SIGBUS) 28 printf("\nSIGBUS handler!!!\n"); 29 exit(1); 30 } 31 32 int main(int argc, const char *argv[]) 33 { 34 if (argc < 2) 35 { 36 printf("usage:%s text\n", argv[0]); 37 exit(1); 38 } 39 40 char *addr; 41 int file_fd, text_len; 42 long int sys_pagesize; 43 44 /* 設置信號處理器 */ 45 if (signal(SIGSEGV, signal_handler) == SIG_ERR) 46 err_exit("signal()"); 47 if (signal(SIGBUS, signal_handler) == SIG_ERR) 48 err_exit("signal()"); 49 50 if ((file_fd = open(MMAP_FILE_NAME, O_RDWR)) == -1) 51 err_exit("open()"); 52 53 /* 系統分頁大小 */ 54 sys_pagesize = sysconf(_SC_PAGESIZE); 55 printf("sys_pagesize:%ld\n", sys_pagesize); 56 57 /* 記憶體只讀 */ 58 //addr = (char *)mmap(NULL, MMAP_FILE_SIZE, PROT_READ, MAP_SHARED, file_fd, 0); 59 60 /* 映射大於文件長度,且大於該文件分頁大小 */ 61 //addr = (char *)mmap(NULL, sys_pagesize + 1, PROT_READ | PROT_WRITE, MAP_SHARED, file_fd, 0); 62 63 /* 正常分配 */ 64 addr = (char *)mmap(NULL, MMAP_FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, file_fd, 0); 65 if (addr == MAP_FAILED) 66 err_exit("mmap()"); 67 68 /* 原始數據 */ 69 printf("old text:%s\n", addr); 70 71 /* 越界訪問 */ 72 //addr += sys_pagesize + 1; 73 //printf("out of range:%s\n", addr); 74 75 /* 拷貝新數據 */ 76 text_len = strlen(argv[1]); 77 memcpy(addr, argv[1], text_len); 78 79 /* 同步映射區數據 */ 80 //if (msync(addr, text_len, MS_SYNC) == -1) 81 // err_exit("msync()"); 82 83 /* 列印新數據 */ 84 printf("new text:%s\n", addr); 85 86 /* 解除映射區域 */ 87 if (munmap(addr, MMAP_FILE_SIZE) == -1) 88 err_exit("munmap()"); 89 90 return 0; 91 }
1.首先創建一個10位元組的文件:
1 $:dd if=/dev/zero of=a.txt bs=1 count=10
2.把程式編譯運行後,依次執行2寫入:
可以看到本機的分頁大小是4096位元組。第一次寫入9個位元組,原來用dd命令創建的文件為空,old text為空。第二次寫入4個位元組,只覆蓋了最前面的1234。
3.驗證可訪問現有分頁的記憶體。寫入超過10位元組的數據:
上面我們寫入了17個位元組,雖然64行的mmap()映射了MMAP_FILE_SIZE=10位元組。但從輸入new text可以看出,我們依然可以訪問10位元組後面的記憶體,因為該數據都在一個分頁(4096)裡面。cat查看a.txt後,只有前10個位元組寫入了a.txt。
4.驗證SIGSEGV信號。把64行註釋調,58行打開,設置映射屬性為只讀,編譯後訪問:
設置只讀屬性後,第77行有寫操作。我們自定義的信號處理器就捕捉到了該信號。如果沒有自定義信號處理器,終端就會輸出Segmentation fault
5.驗證SIGBUS信號。用61行的方法來映射記憶體。映射了一個分頁大小再加1位元組的記憶體,並放開72,73行的代碼,讓指針指向一個分頁後的區域。編譯後運行:
SIGBUS信號被自定義處理器捕捉到了。如果沒有自定義信號處理器,終端就會輸出Bus error
四.匿名映射
匿名映射有2種方式:
1.指定mmap()的flags參數為MAP_ANONYMOUS,在linux上當指定這個值後會忽略fd參數的值。不過在有的UNIX上還需要把fd指定為-1。
2.把/dev/zero當做文件描述符打開,從/dev/zero讀取數據時它會給你提供無窮無盡的0,向它寫數據,它會丟棄。丟棄這點跟/dev/null一樣,只是/dev/null不跟你提供數據。
3.匿名映射的使用跟上面的文件映射差不多。這裡不再給例子。