Linux中進程間的通信方式--內存映射區mmap()



簡 述: 本篇講解另外一種進程間通信方式,內存映射區 mmap(),以及對應的釋放內存映射區 munmap() ,。前面兩篇講解了進程間通信,使用有名管道匿名管道的方式進行 IPC,也是經常用到的,可以去接觸一下。

  • 對於有血緣關係的進程間通信:
    • 有名內存映射區
    • 匿名內存映射區(推薦)
  • 對於無血緣關係的進程間通信:
    • (只能用)有名內存映射區

編程環境:

💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3

💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0


mmap內存映射原理:

(一)進程啓動映射過程,並在虛擬地址空間中爲映射創建虛擬映射區域

1、進程在用戶空間調用庫函數mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前進程的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址

3、爲此虛擬區分配一個vm_area_struct結構,接着對這個結構的各個域進行了初始化

4、將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中

(二)調用內核空間的系統調用函數 mmap(不同於用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關係

5、爲映射分配了新的虛擬地址區域後,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護着和這個已打開文件相關各項信息。

6、通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型爲:int mmap(struct file *filp, struct vm_area_struct *vma),不同於用戶空間庫函數。

7、內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。

8、通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關係。此時,這片虛擬地址並沒有任何數據關聯到主存中。

(三)進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝

注:前兩個階段僅在於創建虛擬區間並完成地址映射,但是並沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。

9、進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因爲目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作後,內核發起請求調頁過程。

11、調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。

12、之後進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁盤地址,也即完成了寫入到文件的過程。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件裏了。


創建內存映射區 mmap():

作用: 將磁盤文件的數據映射到內存,用戶通過修改內存就能修改磁盤文件。

void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
  • 參數:
    • addr:
      • 映射區的首地址,傳 NULL ;系統會自動在虛擬地址空間的動態加載區,開闢一塊大小爲 len 的內存區域空間。
    • len: //映射區的大小
      • 必須是 4K 的整數倍,且不能夠爲 0。
      • 一般文件有多大,len 就有多大
    • prot: //映射區的權限
      • PROT_READ – 映射區必須要有讀權限
      • PROT_WRITE – 寫權限
    • **flags: //標誌位參數 **
      • MAP_SHARED 共享區域,開啓此權限,則內存中映射區域的內容,是和磁盤文件的內容保持一致
      • MAP_PRIVATR 內存區域的映射內容,是和磁盤文件的內容,不是時刻同步的。
    • fd: //文件描述符
      • 磁盤文件(想要映射到內存中的共享區)的那個文件的文件描述符
    • offset: //偏移文件的偏移量
      • 當想要從文件的中間某處到結束區域,映射到內存中,就可以只用這個偏移
  • 返回值:
    • *void 開闢的那個區域的首地址,用指針傳出來。
      • 映射區的首地址 – 調用成功
      • 調用失敗, 返回 MAP_FAILED

釋放內存映射區 munmap():

就像 malloc - free; new - delete; mmap - munmap 一樣,有開闢空間,就有釋放該內存區域

int munmap(void *addr, size_t len);

兩個參數,就是 mmap() 的第一個和第二個參數。


寫一個例子,驗證內存內容和磁盤文件會同步:

對於一個已有的文本文件 it.txt 進行映射,創建一個內存映射區,然後在內存映射區裏面修改文件的聶榮,再重新打開磁盤的文本文件查看內容是否同時發生了改變。顯示修改之前文件和使用內存映射區的內容後的文件內容後,發現磁盤裏面的內容的確是和同步的改變了。最後要記得使用 munmap() 關閉你使用 mmap() 創建的內存映射區的空間哦。

  • 代碼示例:

    #include <stdio.h>
    #include <unistd.h> 
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
    
    int main(int argc, char *argv[])
    {
        int fd = open("it.txt", O_RDWR);
    
        if (fd == -1) {
            perror("[open file] ");
            _exit(1);
        }
    
        int len = lseek(fd, 0, SEEK_END);
        void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  //創建內存映射區
        if (ptr == MAP_FAILED) {
            perror("[mmap fail] ");
            exit(1);
        }
    
        ((char *)ptr)[0] = 'a';
        ((char *)ptr)[1] = 'b';
        ((char *)ptr)[2] = 'c';
        
        printf("%s\n", (char*)ptr);  //釋放內存映射區
        munmap(ptr, len);
        close(fd);
    
        return 0;
    }
    
  • 運行效果:

    和預期的效果一直,在內存中改動內容,磁盤的文件的內容也隨之改變。


對於 mmap() 的一些思考:

  • 如果 mmap() 的返回值 (ptr)做++操作(ptr++),munmap是否能夠成功?
    • 不能,如果要做指針偏移的的話,可以 char* pt = ptr;
  • 如果 open() 時候 O_RDONLY,mmap 時 prot 參數指定 PROR_READ | PROT_WRITE 會怎樣?
    • mmap 會調用失敗
    • open() 文件指定權限應該大於等於 mmap() 的第三個參數 prot 指定的權限
  • 如果文件的偏移量爲 1000 會怎麼樣?
    • 會失敗,其必須是 4096 的整數倍
  • 如果不檢查 mmap() 的返回值會怎樣?
    • 也不會怎麼樣
  • mmap() 什麼時候會調用失敗?
    • 第二個參數 len = 0
    • 第三個必須要有 PROT_READ 權限;且 open()打開的權限要大於 mmap() 的 port 參數權限
  • 可以open()的時候,O_CREAT 一個新文件來創建映射區嗎?
    • 可以,但是需要做文件擴展
    • lseek()
    • truncate(path,length)
  • mmap 後關閉文件描述符,對 mmap 映射有沒有影響?
    • 文件被打開之後,就沒有影響了。
  • 對 ptr 越界操作會怎麼樣?
    • 這個取決於 ptr 越界後面的內存寫的是什麼。但是大概率的會遇到段錯誤

mmap 實現內存映射?

  • 必須要有一個文件
  • 文件數據什麼時候有用?
    • 單純的實現文件映射
    • 進行進程間通信,磁盤的文件數據時沒有用的。(在內存操作會更有效率,但是屬於非阻塞)

父子進程間永遠共享的東西?

  • 文件描述符
  • 內存映射區

例子實現父子進程間的通信:

通過改寫上面的例子,創建 anonMmap.cpp 文件,創建子進程,父進程對內存映射區進行修改內容,在首段使用 strcpy() 添加一段中文語句,然後在子進程裏面對複製進來的尾部’\0’進行覆蓋,再次修改一段內容,然後在子進程裏面間該短內容輸出到終端顯示。

  • 代碼實現:

    #include <stdio.h>
    #include <unistd.h> 
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
    
    int main(int argc, char *argv[])
    {
        int fd = open("it.txt", O_RDWR);
    
        if (fd == -1) {
            perror("[open file] ");
            _exit(1);
        }
    
        int len = lseek(fd, 0, SEEK_END);
        void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  //創建內存映射區
        if (ptr == MAP_FAILED) {
            perror("[mmap fail] ");
            exit(1);
        }
    
        pid_t pid = fork();
    
        if (pid > 0) {  //父進程
             strcpy((char *)ptr, "(我是父進程寫入數據到內存映射區內容)");  //下標0-53,一共 54 個,其中ptr[53]爲'\0'
             wait(NULL);         //回收子進程
        } else if (pid == 0) {  //子進程
            // sleep(2);
            ((char *)ptr)[53] = 'a';  //故意覆蓋掉'\0',方便打印出來後面文章
            ((char *)ptr)[54] = 'b';
            ((char *)ptr)[57] = 'c';
            printf("%s", (char *)ptr);
        }
    
        munmap(ptr, len);
        close(fd);
    
        return 0;
    }
    
  • 運行效果:


創建匿名內存映射區:

上面寫的例子,都是對於有血緣關係的父子進程之間的通信例子,通過磁盤文件使用 mmap() 創建的是 (有名)內存映射區 ; 但是改一下 mmap() 創建的倒數第二個參數,且不需要 open() 磁盤文件,創建出來的就是 (匿名)內存映射區

但是匿名內存映射區只能夠適用於有血緣關係之間的進程通信。而有名內存映射區,可以使用與在有有血緣的進程和無血緣的進程之間的通信,都可以。

匿名內存映射區(有血緣關係進程通信):

  • (匿名)內存映射區 代碼例子:

    int main(int argc, char *argv[])
    {
        int len = 4096;
        void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);  //創建匿名內存映射區,只需要修改倒數 2、3 兩個闡述即可
        if (ptr == MAP_FAILED) {
            perror("[mmap fail] ");
            exit(1);
        }
    
        pid_t pid = fork();
    
        if (pid > 0) {  //父進程
             strcpy((char *)ptr, "this is parent process");
             wait(NULL);         //回收子進程
        } else if (pid == 0) {  //子進程
            // sleep(2);
            ((char *)ptr)[0] = 'a';  //故意覆蓋掉'\0',打印出來後面文章
            ((char *)ptr)[1] = 'b';
            ((char *)ptr)[2] = 'c';
            printf("%s", (char *)ptr);
        }
    
        munmap(ptr, len);
    
        return 0;
    }
    
    
  • 運行效果:


有名內存映射區(無血緣關係進程通信):

而對於無血緣關係的進程間通信,只需要都打開同一個磁盤文件,各自的進程會按照這個順序, 磁盤文件名 --> (各自的進程虛擬地址空間的)內存映射區 --> (共用一份的)物理內存的區域, 然後都可以修改和讀取這一段內存區域,從而實現進程間通信。

創建 aProcess.cpp 生成 a 進程,創建 bProcess.cpp 生成 b 進程;a 進程先對 c.txt 文件改寫添加 “abc”,然後 b 進程再對 c.txt 文件改寫添加 “ABC”,然後輸出到終端顯示。

  • 代碼顯示:

    實現僞代碼如下,詳細的源碼見下面下載鏈接

    //aProcess.cpp
    int main(int argc, char *argv[])
    {
        int fd = open("c.txt", O_RDWR);
     		...
        int len = lseek(fd, 0, SEEK_END);
        void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        ((char *)ptr)[0] = 'a';
        ((char *)ptr)[1] = 'b';
        ((char *)ptr)[2] = 'c';
        
        munmap(ptr, len);
    		...
    }
    
    
    //bProcess.cpp
    int main(int argc, char *argv[])
    {
        int fd = open("c.txt", O_RDWR);
     		...
        int len = lseek(fd, 0, SEEK_END);
        void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        ((char *)ptr)[3] = 'A';
        ((char *)ptr)[4] = 'B';
        ((char *)ptr)[5] = 'C';
        
        munmap(ptr, len);
    		...
    }
    
  • 運行效果:


借鑑博客與總結:

發現一篇講解的很棒的博客,更多的是理論和概念上面的分析 mmap() 的原理:認真分析mmap:是什麼 爲什麼 怎麼用, 其中文章開頭的一段拿來再描述一下,其餘則是本篇的側重點是用代碼來寫兩個例子,以及需要注意的一些坑,驗證和學習這個內存映射區。


下載地址:

13_mmap

歡迎 star 和 fork 這個系列 的 linux 學習,附學習由淺入深的目錄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章