一.概述
內存映射是在調用進程的虛擬地址空間創建一個新的內存映射。
內存映射分為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(執行異步文件寫入),此操作內核會先把內容寫到內核的緩沖區,某個合適的時候再寫到磁盤。
三.文件映射實例
/**
* @file mmap_file.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>
#define MMAP_FILE_NAME "a.txt"
#define MMAP_FILE_SIZE 10
void err_exit(const char *err_msg)
{
printf("error:%s\n", err_msg);
exit(1);
}
/* 信號處理器 */
void signal_handler(int signum)
{
if (signum == SIGSEGV)
printf("\nSIGSEGV handler!!!\n");
else if (signum == SIGBUS)
printf("\nSIGBUS handler!!!\n");
exit(1);
}
int main(int argc, const char *argv[])
{
if (argc < 2)
{
printf("usage:%s text\n", argv[0]);
exit(1);
}
char *addr;
int file_fd, text_len;
long int sys_pagesize;
/* 設置信號處理器 */
if (signal(SIGSEGV, signal_handler) == SIG_ERR)
err_exit("signal()");
if (signal(SIGBUS, signal_handler) == SIG_ERR)
err_exit("signal()");
if ((file_fd = open(MMAP_FILE_NAME, O_RDWR)) == -1)
err_exit("open()");
/* 系統分頁大小 */
sys_pagesize = sysconf(_SC_PAGESIZE);
printf("sys_pagesize:%ld\n", sys_pagesize);
/* 內存只讀 */
//addr = (char *)mmap(NULL, MMAP_FILE_SIZE, PROT_READ, MAP_SHARED, file_fd, 0);
/* 映射大於文件長度,且大於該文件分頁大小 */
//addr = (char *)mmap(NULL, sys_pagesize + 1, PROT_READ | PROT_WRITE, MAP_SHARED, file_fd, 0);
/* 正常分配 */
addr = (char *)mmap(NULL, MMAP_FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, file_fd, 0);
if (addr == MAP_FAILED)
err_exit("mmap()");
/* 原始數據 */
printf("old text:%s\n", addr);
/* 越界訪問 */
//addr += sys_pagesize + 1;
//printf("out of range:%s\n", addr);
/* 拷貝新數據 */
text_len = strlen(argv[1]);
memcpy(addr, argv[1], text_len);
/* 同步映射區數據 */
//if (msync(addr, text_len, MS_SYNC) == -1)
// err_exit("msync()");
/* 打印新數據 */
printf("new text:%s\n", addr);
/* 解除映射區域 */
if (munmap(addr, MMAP_FILE_SIZE) == -1)
err_exit("munmap()");
return 0;
}
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.匿名映射的使用跟上面的文件映射差不多。這裡不再給例子。