文章目錄
簡 述: 本篇講解另外一種進程間通信方式,內存映射區 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: //偏移文件的偏移量
- 當想要從文件的中間某處到結束區域,映射到內存中,就可以只用這個偏移
- addr:
- 返回值:
- *void : 開闢的那個區域的首地址,用指針傳出來。
- 映射區的首地址 – 調用成功
- 調用失敗, 返回 MAP_FAILED
- *void : 開闢的那個區域的首地址,用指針傳出來。
釋放內存映射區 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:是什麼 爲什麼 怎麼用, 其中文章開頭的一段拿來再描述一下,其餘則是本篇的側重點是用代碼來寫兩個例子,以及需要注意的一些坑,驗證和學習這個內存映射區。
下載地址:
歡迎 star 和 fork 這個系列 的 linux 學習,附學習由淺入深的目錄。