前言
最近在學習GNU/Linux內核,看到mmap的時候書上說:
mmap/munmap接口函數是用戶最常用的兩個系統調用接口,無論是在用戶程序中分配內存、讀寫大文件、鏈接動態庫文件,還是多進程間共享內存,都可以看到mmap/munmap的身影。
這句話說的很正確,雖然我們日常沒有直接使用mmap,但是其實我們都間接地使用了mmap/mumap函數。
舉個例子,我們使用動態鏈接庫的時候,我們都知道,動態鏈接不會把動態庫中的代碼整合到目標文件中,相反,動態庫跟目標文件獨立。
那爲什麼運行時,程序能夠獲得指定的符號鏈接?這正是mmap的力量,程序運行時,他將保存在物理內存的動態庫的內容(如果物理內存中沒有,則先加載如內存)映射到自身的進程地址空間,這樣符號所對應的指令數據便存在了。
(通過
ldd
可獲取所依賴的動態庫,Linux怎麼獲取到的,我懷疑跟ELF文件格式有關,待解答)
mmap
mmap是內存映射文件的方法
mmap將一個文件或者其它對象映射進內存。mmap在用戶空間映射調用系統中作用很大。
mmap()必須以PAGE_SIZE
爲單位進行映射,而內存也只能以頁爲單位進行映射,若要映射非PAGE_SIZE
整數倍的地址範圍,要先進行內存對齊,強行以PAGE_SIZE
的倍數大小進行映射。
頭文件
<sys mman.h>
函數原型
void* mmap(void* addr, size_t length,int prot,int flags,int fd,off_t offset); int munmap(void* start,size_t length);
- addr: 用於指定映射到進程地址的起始地址,爲了應用程序的可移植性,一般設爲nullptr;
- length: 表示映射到進程地址空間的大小;
- prot: 表示設置內存映射區域的讀寫屬性等;
- flags: 用於設置內存映射的屬性,例如共享映射、私有映射、匿名映射等;
- fd: 文件句柄,如果不是文件映射,則置爲0;
- offset: 文件映射時的文件偏移量。
重複一遍:由於GNU/Linux中,內存分配是以頁爲單位的,所以length長度不足1頁(默認4KB),則按1頁來處理。
prot
參數detail:
參數 | 解釋 |
---|---|
PROT_EXEC | 頁內容可以被執行 |
PROT_READ | 頁內容可以被讀取 |
PROT_WRITE | 頁可以被寫入 |
PROT_NONE | 頁不可訪問 |
flags
參數detail:
參數 | 解釋 |
---|---|
MAP_FIXED | 使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。 |
MAP_SHARED | 與其它所有映射這個對象的進程共享映射空間。對共享區的寫入,相當於輸出到文件。直到msync()或者munmap()被調用,文件實際上不會被更新。 |
MAP_PRIVATE | 建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標誌和MAP_SHARED標誌是互斥的,只能使用其中一個。 |
MAP_ANONYMOUS | 匿名映射,映射區不與任何文件關聯。可用於分配內存 |
MAP_POPULATE | 對於文件映射來說,會提前預讀文件內容到映射區域,該特性只支持私有映射 |
共享內存
共享內存Shared Memory,顧名思義就是允許兩個不相關的進程訪問同一個邏輯內存,共享內存是兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。
具體來說,共享內存就是一段真實存在的物理內存,不同進程通過訪問和修改該段物理內存,最終達到共享內存的目的。
PS: 共享內存不提供數據同步機制,如一個進程在寫過程中,另一個進程可以進行讀操作和寫操作,所以需要通過信號量來解決同步問題。
創建共享內存
函數介紹
函數原型如下:
#include <sys mman.h>
#include <sys stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
Link with -lrt.
shm_open()
創建並打開一個新的或現有的POSIX
共享內存對象。POSIX
共享內存對象實際上是一個句柄,它可以被與mmap
共享內存相同區域的無關進程使用。
shm_unlink()
函數的作用是:刪除先前由shm_open()
創建的對象。
函數用法
int fd = shm_open("/shm01", O_CREAT | O_RDWR, 0777);
當oflag使用O_CREAT
時,mode表示創建文件時的權限,權限寫法跟命令chmod一樣(https://www.runoob.com/linux/linux-comm-chmod.html)。
open與shm_open區別
或許你會問:如果是創建一個文件何必使用shm_open呢?爲什麼不使用open?
理論上來說兩個方法可以相互替換。
但是,很重要的一點是:shm_open
會將文件創建在/dev/shm
目錄下,該目錄下掛載的文件系統格式是tmpfs
,該目錄下的文件也只存儲在內存(主存)中。如果你使用open
方法在該目錄下創建和打開文件其實兩者就沒有本質區別了。
如圖所示,/dev/shm是tmpfs文件系統
進程間通訊實例
本測試用例需要依賴
googletest
測試框架,當然讀者可以通過分別建立兩個項目來實現下述實例;
實例原理
如圖所示,共享內存創建後保存在真實的物理內存中,通過mmap函數我們將該物理內存地址映射到進程地址空間中,最終實現操作共享內存的功能。
實例功能
服務端:創建共享內存,並將內存映射到進程地址空間中,然後進行定時修改內存內容。
客戶端:映射共享內存到進程地址空間中,定時獲取內存內容。
實例代碼
服務端代碼
#include <gtest gtest.h>
#include <iostream>
#include <fcntl.h>
#include <sys mman.h>
TEST(shm_test, server) {
int fd = shm_open("/shm01", O_CREAT | O_RDWR, 0777);
// int fd = open("/dev/shm/shm01", O_CREAT | O_RDWR);
if (fd < 0) {
std::cout << "error to create or open" << std::endl;
return;
}
std::cout << "create or open ok" << std::endl;
ftruncate(fd, 4096);// 分配4KB內存
char *ptr = (char *)mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
char a[] = "0\0";
for (char k = 0; k < 26; ++k) {
memset(a, 'a' + k, 1 *sizeof(char));
strcpy(ptr, a);
sleep(1);
}
strcpy(ptr, a);
mumap((void *)ptr, 4096); // 關閉映射
exit(0);
}
客戶端代碼
#include <gtest gtest.h>
#include <iostream>
#include <fcntl.h>
#include <sys mman.h>
TEST(test, client) {
int fd = shm_open("/shm01", O_RDWR, 0777);
// int fd = open("/dev/shm/shm01", O_CREAT | O_RDWR);
if (fd < 0) {
std::cout << "error to open" << std::endl;
return;
}
char *ptr = (char *)mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
while(true) {
std::cout << ptr << std::endl;
sleep(1);
}
exit(0);
}
執行結果
開頭的z是之前保存在共享內存中的數據
可以看到客戶端輸出了英語字母。
遇到的問題
- 找不到
shm_open
的符號鏈接
答:在編譯時鏈接rt
庫
- 使用
googletest
時,缺失pthread
答:鏈接pthread
庫
總結
本篇文章,我們知道了共享內存,通過共享內存和mmap我們能很輕易地完成進程間通訊(IPC Inter-Process Communication),進程間的semaphore(信號量)實現方式也是通過mmap和共享變量實現的。