mmap詳解

原文地址:

http://kenby.iteye.com/blog/1164700

共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式, 因爲進程可以直接讀寫內存,而不需要任何

數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則

只拷貝兩次數據: 一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內

存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直

到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映

射時才寫回文件的。因此,採用共享內存的通信方式效率是非常高的。

一. 傳統文件訪問

UNIX訪問文件的傳統方法是用open打開它們, 如果有多個進程訪問同一個文件, 則每一個進程在自己的地址空間都包含有該

文件的副本,這不必要地浪費了存儲空間. 下圖說明了兩個進程同時讀一個文件的同一頁的情形. 系統要將該頁從磁盤讀到高

速緩衝區中, 每個進程再執行一個存儲器內的複製操作將數據從高速緩衝區讀到自己的地址空間.


二. 共享存儲映射

現在考慮另一種處理方法: 進程A和進程B都將該頁映射到自己的地址空間, 當進程A第一次訪問該頁中的數據時, 它生成一

個缺頁中斷. 內核此時讀入這一頁到內存並更新頁表使之指向它.以後, 當進程B訪問同一頁面而出現缺頁中斷時, 該頁已經在

內存, 內核只需要將進程B的頁表登記項指向次頁即可. 如下圖所示: 

三、mmap()及其相關係統調用

mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪

問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。

 

mmap()系統調用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset ) 

mmap的作用是映射文件描述符fd指定文件的 [off,off + len]區域至調用進程的[addr, addr + len]的內存區域, 如下圖所示:

參數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指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函

數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。


四. mmap的兩個例子
範例中使用的測試文件 data.txt:

    aaaaaaaaa  
    bbbbbbbbb  
    ccccccccc  
    ddddddddd  
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;  
    }  
2 私有映射無法修改文件

/* 將文件映射至進程的地址空間 */
if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | 
                    PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) {
    perror("mmap");
}

五. 使用共享映射實現兩個進程之間的通信
兩個程序映射同一個文件到自己的地址空間, 進程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_PRIVATE, fd, 0)) == (void *)-1) {  
            perror("mmap");  
        }  
      
        /* 映射完後, 關閉文件也可以操縱內存 */  
        close(fd);  
      
        /* 修改一個字符 */  
        mapped[20] = '9';  
       
        return 0;  
    }  

六. 通過匿名映射實現父子進程通信

#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()返回地址的訪問
linux採用的是頁式管理機制。對於用mmap()映射普通文件來說,進程會在自己的地址空間新增一塊空間,空間大
小由mmap()的len參數指定,注意,進程並不一定能夠對全部新增空間都能進行有效訪問。進程能夠訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,能夠容納文件被映射部分大小的最少頁面個數決定了進程從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,i;  
        int pagesize,offset;  
        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);  
        printf("file size is %zd\n", (size_t)sb.st_size);  
      
        offset = 0;   
        p_map = (char *)mmap(NULL, pagesize * 2, PROT_READ|PROT_WRITE,   
                MAP_SHARED, fd, offset);  
        close(fd);  
          
        p_map[sb.st_size] = '9';  /* 導致總線錯誤 */  
        p_map[pagesize] = '9';    /* 導致段錯誤 */  
      
        munmap(p_map, pagesize * 2);  
      
        return 0;  
    }  




發佈了28 篇原創文章 · 獲贊 9 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章