本文在這篇文章的基礎上,略做修改。由於本人經過實驗發現這篇文章存在些許錯誤。
原文連接: https://yq.aliyun.com/article...linux
mmap函數是unix/linux下的系統調用。
當存在客戶-服務程序中複製文件時候,其數據流以下,要經歷四次數據複製,開銷很大。git
若是採用共享內存的方式,那麼將大大優化IO操做,數據流變成了以下,數據只複製兩次:github
映射文件或設備到內存中,取消映射就是munmap函數。ubuntu
語法以下:app
void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset);函數
int munmap(void *addr, size_t length);性能
該函數主要用途有三個:測試
一、將普通文件映射到內存中,一般在須要對文件進行頻繁讀寫時使用,用內存讀寫取代I/O讀寫,以得到較高的性能;優化
二、將特殊文件進行匿名內存映射,爲關聯進程提供共享內存空間;spa
三、爲無關聯的進程間的Posix共享內存(SystemV的共享內存操做是shmget/shmat)
咱們來看下函數的入參選擇:
一、參數addr:
指向欲映射的內存起始地址,一般設爲 NULL,表明讓系統自動選定地址,映射成功後返回該地址。
二、參數length:
表明將文件中多大的部分映射到內存。
三、參數prot:
映射區域的保護方式。能夠爲如下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取
四、參數flags:
影響映射區域的各類特性。在調用mmap()時必需要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 若是參數start所指的地址沒法成功創建映射時,則放棄映射,不對地址作修正。一般不鼓勵用此。
MAP_SHARED對映射區域的寫入數據會複製迴文件內,並且容許其餘映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操做會產生一個映射文件的複製,即私人的「寫入時複製」(copy on write)對此區域做的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS創建匿名映射。此時會忽略參數fd,不涉及文件,並且映射區域沒法和其餘進程共享。
MAP_DENYWRITE只容許對映射區域的寫入操做,其餘對文件直接寫入的操做將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
五、參數fd:
要映射到內存中的文件描述符。若是使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設爲-1。
六、參數offset:
文件映射的偏移量,一般設置爲0,表明從文件最前方開始對應,offset必須是分頁大小的整數倍。以下圖內存映射文件的示例。
如下示例代碼,在ubuntu 16.04.4上測試經過。gcc版本:5.4.0
使用dd命令,能夠建立任意大小的測試文件。
好比:dd if=/dev/zero of=/tmp/hello.txt bs=4096 count=1
即在/tmp目錄下建立一個大小爲4096的文件。
1 共享映射
修改共享內存中的文件內容:
#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> int main (int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped; if ( argc <= 1 ) { printf("%s: Need file path! \n",argv[0]); exit(-1); } /* 打開文件 */ 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[0] = '0'; if ((msync ((void *) mapped, sb.st_size, MS_SYNC)) == -1) { perror ("msync"); } /* 釋放存儲映射區 */ if ((munmap ((void *) mapped, sb.st_size)) == -1) { perror ("munmap"); } return 0; }
編譯:
$ gcc -c lianxi1.c
連接:
$ gcc -o out1 lianxi1.o
執行, 參數爲一個文件全路徑。例如在/tmp下建立一個hello1.txt,存入hello單詞:
$ ./out1 /tmp/hello1.txt
執行完畢後發現文件中的第一個字母h變成了0。
程序將文件映射到了內存中,並將第一個字符進行了修改並同步到了磁盤文件中。
2 父子進程通訊
使用fork建立進程,父子進程分別往共享內存中寫入各自字符串,並讀出。
#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", "from u son"); munmap (p_map, BUF_SIZE); //實際上,進程終止時,會自動解除映射。 exit (0); } sprintf (p_map, "%s", "from u father"); sleep (2); //能夠修改此處睡眠時間,看看不一樣的輸出。 printf ("parent got a message: %s\n", p_map); return 0; }
編譯:
$ gcc -c lianxi2.c
連接:
$ gcc -o out2 lianxi2.o
執行:
$ ./out2
這段代碼,根據父子進程的sleep時間長短不一樣,輸出會有不一樣。
3 內存訪問溢出
linux採用的是頁式管理機制,使用mmap()映射普通文件後,進程會在本身的地址空間新增一塊空間,空間大小由mmap()的len參數指定。可是,進程並不必定可以對所有新增空間都能進行有效訪問。進程可以訪問的有效地址大小取決於文件被映射部分的大小。決定進程能訪問的大小是容納文件被映射部分的最小頁面數。以下圖。
#include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main (int argc, char **argv) { int fd; int pagesize, offset; int len, size; char *p_map; struct stat sb; /* 取得page size */ pagesize = sysconf (_SC_PAGESIZE); printf ("pagesize is %d\n", pagesize); /* 打開文件 */ fd = open (argv[1], O_RDWR, 00777); fstat (fd, &sb); len = pagesize * 2; size = sb.st_size; printf("映射內存長度: %d; 文件大小: %d\n", len, size); offset = 0; //映射兩頁內存 p_map = (char *) mmap (NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); close (fd); printf("開始測試\n"); printf("訪問超過映射大小內存地址\n"); p_map[len] = '9'; /* 致使段錯誤 */ if(size < pagesize) { //文件大小小於一頁大小 printf("文件大小小於一頁大小\n"); printf("訪問文件大小內存地址\n"); p_map[size] = '9'; /* 正常訪問 */ printf("訪問一頁大小內存地址\n"); p_map[pagesize - 1] = '9'; /* 正常訪問 */ printf("訪問超過一頁大小內存地址\n"); p_map[pagesize] = '9'; /* 致使總線錯誤 */ printf("訪問映射大小內存地址\n"); p_map[len - 1] = '9'; /* 致使總線錯誤 */ } else if(size > pagesize) { //文件大小大於一頁大小 printf("文件大小大於一頁大小\n"); printf("訪問文件大小內存地址\n"); p_map[size] = '9'; /* 正常訪問 */ printf("訪問一頁大小內存地址\n"); p_map[pagesize - 1] = '9'; /* 正常訪問 */ printf("訪問超過一頁大小內存地址\n"); p_map[pagesize] = '9'; /* 正常訪問 */ printf("訪問映射大小內存地址\n"); p_map[len - 1] = '9'; /* 正常訪問 */ } else { //文件大小等於一頁大小 printf("文件大小等於一頁大小\n"); printf("訪問文件大小內存地址\n"); p_map[size] = '9'; /* 致使總線錯誤 */ printf("訪問一頁大小內存地址\n"); p_map[pagesize - 1] = '9'; /* 正常訪問 */ printf("訪問超過一頁大小內存地址\n"); p_map[pagesize] = '9'; /* 致使總線錯誤 */ printf("訪問映射大小內存地址\n"); p_map[len - 1] = '9'; /* 致使總線錯誤 */ } munmap (p_map, len); return 0; }
編譯:
$ gcc -c lianxi3.c
連接:
$ gcc -o out3 lianxi3.o
執行, 參數爲一個文件全路徑。例如在/tmp下建立h三個文件,分別對應文件大小大於一頁內存大小的dayu.txt,文件大小小於一頁內存大小的xiaoyu.txt, 文件大小等於一頁內存大小的dengyu.txt:
$ ./out3 dayu.txt
$ ./out3 xiaoyu.txt
$ ./out3 dengyu.txt
修改代碼中的測試用例,屢次編譯、連接、執行。
本文實例代碼在github上: https://github.com/lansheng22...