# 共享記憶體 - File Mapping 在同一個專案下,如果想在多個`cpp`中,使用同一個`int`變數,我們也許會宣告一個全域變數(global variable),讓不同`cpp`都能存取到該變數。 但開發大型系統時,通常不會只有單一個專案,而是有很多個`exe`或`dll`,而在這些分開執行的`行程(process)`中,也經常需要共用一些變數或資料。 然而,每個`process`在記憶體中,都會被分配到一塊屬於自己的記憶體空間,並與其它`process`互相隔離、互不干擾,所以在一個`process`宣告的全域變數,並不能讓其它`process`使用。 若想在不同`process`之間,共享同一份資料,其中一個方式就是使用 ++**File Mapping**++。 ## File Mapping (檔案對映) > File Mapping(檔案對映)可以用來讀取大型檔案,作法是在記憶體中創建一塊空間,並對映到硬碟中的檔案。若是不對映任何檔案,就能利用這塊記憶體空間,實作共享記憶體的功能。 ![image](https://hackmd.io/_uploads/SkTuVge3Jx.png) :::success ##### 💡 你可能也會查到「Memory-Mapped File (記憶體對映檔)」,中國稱「內存映射」,是一樣的。 ::: 以下範例,須建立兩個++各自獨立的專案++`ProcessA`和`ProcessB` : - 在`ProcessA`++建立記憶體對映檔++並寫入資料。 - 在`ProcessB`++讀取記憶體對映檔++的資料,達到共享資料的作用。 ### ✔程式碼 - ProcessA >[!Tip] 關鍵步驟 >#### 1. CreateFileMapping : 在實體記憶體,建立`Memory-Mapped File`(記憶體對映檔) >#### 2. MapViewOfFile : 將記憶體對映檔,對映(map)到此`行程(Process)`的虛擬記憶體 >#### 3. UnmapViewOfFile : 取消對映 >#### 4. CloseHandle : 釋放記憶體 ```cpp= void ProcessAFunction() { char szMsg[256] = "Message from ProcessA."; //要共享給ProcessB的資料 char szErr[256]; //記錄error訊息 HANDLE hMapFile; LPCTSTR pBuf; //建立記憶體對映檔(實體記憶體) hMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, // use paging file NULL, // default security PAGE_READWRITE, // read/write access 0, // maximum object size (high-order DWORD) BUF_SIZE, // maximum object size (low-order DWORD) "MyFileMappingObject"); // name of mapping object if(hMapFile == NULL) { sprintf(szErr, "Could not create file mapping object (%d).", GetLastError()); return; } //將對映檔,對映(map)到此行程的虛擬記憶體 pBuf = (LPTSTR) MapViewOfFile(hMapFile, // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, 0, BUF_SIZE); if(pBuf == NULL) { sprintf(szErr, "Could not map view of file (%d).", GetLastError()); CloseHandle(hMapFile); return; } //將資料寫入對映檔 CopyMemory((PVOID)pBuf, szMsg, 256); //關閉虛擬記憶體的對映 UnmapViewOfFile(pBuf); //須在別處釋放對映檔 //CloseHandle(hMapFile); return; } ``` #### ✅程式說明 - 第10~16行,使用`CreateFileMapping()`,在++實體記憶體++中建立`Memory-Mapped File`(記憶體對映檔),並回傳對映檔的`handle`。 📍`CreateFileMapping(檔案控制代碼, 安全屬性, 存取權限, 對映檔大小, 對映檔大小, 對映檔名稱);` - `INVALID_HANDLE_VALUE`,代表沒有跟任何檔案有關聯,因為File Mapping其實有其它用途,這裡只單純用來建立一個對映檔的空間。 - `NULL`,代表預設安全屬性。 - `PAGE_READWRITE`,代表建立的對映檔可以被讀/寫。 - `0, BUF_SIZE`,第一個固定填0,第二個填共享資料的大小。 - `"MyFileMappingObject"`,其它`process`可以用這個名稱,找到相同的記憶體對映檔。 - 第18~21行,判斷對映檔是否建立失敗,可以用`GetLastError()`取得錯誤代碼。 - 第24~28行,使用`MapViewOfFile()`將對映檔,對映到此`process`的++虛擬記憶體++,並回傳對映檔的++起始位址(address)++。 📍`MapViewOfFile(對映檔控制代碼, 所需權限, 檔案讀取偏移, 檔案讀取偏移, 讀取大小);` - `hMapFile`,要使用的對映檔`handle`。 - `FILE_MAP_ALL_ACCESS`,代表可讀/寫。 - `0, 0`,代表讀取位置不偏移。 - `BUF_SIZE`,要讀取對映檔內容的大小。 - 第30~34行,判斷對映檔是否對映(讀取)失敗,可以用`GetLastError()`取得錯誤代碼。 - 第37行,將要共享的資料,複製到對映檔。 - 第39行,使用`UnmapViewOfFile()`取消此`process`虛擬記憶體中的對映。 📍`UnmapViewOfFile(對映檔起始位址);` - 參數必須是`MapViewOfFile()`回傳的位址。 - 實體記憶體的對映檔++不會++消失。 ### ✔程式碼 - ProcessB >[!Tip] 關鍵步驟 >#### 1. OpenFileMapping: 取得`Memory-Mapped File`(記憶體對映檔) >#### 2. MapViewOfFile : 將記憶體對映檔,對映到此`行程(Process)`的虛擬記憶體 >#### 3. UnmapViewOfFile : 取消對映 >#### 4. CloseHandle : 釋放記憶體 ```cpp= void ProcessBFunction() { char szMsg[256]; char szErr[256]; HANDLE hMapFile; LPCTSTR pBuf; //開啟記憶體對映檔(實體記憶體) hMapFile = OpenFileMapping( FILE_MAP_ALL_ACCESS, // read/write access FALSE, // do not inherit the name "MyFileMappingObject"); // name of mapping object if(hMapFile == NULL) { sprintf(szErr, "Could not open file mapping object (%d).", GetLastError()); return; } //將對映檔,對映(map)到此行程的虛擬記憶體 pBuf = (LPTSTR) MapViewOfFile(hMapFile, // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, 0, BUF_SIZE); if(pBuf == NULL) { sprintf(szErr, "Could not map view of file (%d).", GetLastError()); CloseHandle(hMapFile); return; } //使用對映檔內的資料 sprintf(szMsg, "%s", pBuf); //關閉虛擬記憶體的對映 UnmapViewOfFile(pBuf); //釋放實體記憶體的對映檔 CloseHandle(hMapFile); return; } ``` #### ✅程式說明 - 第10~13行,使用`OpenFileMapping()`開啟記憶體對映檔,並回傳對映檔的`handle`。 📍`OpenFileMapping(存取權限, 繼承權限, 對映檔名稱);` - `FILE_MAP_ALL_ACCESS`,代表可讀/寫。 - `FALSE`,固定填false。 - `"MyFileMappingObject"`,要開啟的對映檔名稱。 - 第15~18行,判斷對映檔是否開啟失敗,可以用`GetLastError()`取得錯誤代碼。 - 第21~25行,使用`MapViewOfFile()`將對映檔,對映到此`process`的++虛擬記憶體++,並回傳對映檔的++起始位址(address)++。 - 第27~31行,判斷對映檔是否對映(讀取)失敗,可以用`GetLastError()`取得錯誤代碼。 - 第34行,印出對映檔的內容。 - 第36行,使用`UnmapViewOfFile()`取消此`process`虛擬記憶體中的對映。 - 第38行,使用`CloseHandle()`釋放實體記憶體中的對映檔`handle`(實體記憶體的對映檔++會++消失)。 📍`CloseHandle(對映檔控制代碼);` - 若有其它`process`正在使用對映檔,則對映檔不會被釋放,也不會跳出錯誤。 ### 📌其它說明 >[!Caution] 以下是不負責任說明,若有錯誤請務必指正。 - **File Mapping**在記憶體中,是怎麼運作的? 1. `A`使用`CreateFileMapping`後,會在++實體++記憶體創建一個`File Mapping Object(檔案對映物件)`,並且回傳這個物件的`handle(控制代碼)`。 2. `A`使用`MapViewOfFile`,將這個`File Mapping Object`對映到`A`的++虛擬++記憶體中,並且回傳虛擬記憶體中,此區塊的指標,並利用此指標,將要共享的資料寫入記憶體中。 3. `A`使用完畢後,須使用`UnmapViewOfFile`釋放虛擬記憶體的區塊。 4. `B`使用`OpenFileMapping`,來取得指定`File Mapping Object`的`handle`。 5. `B`也使用`MapViewOfFile()`,將`File Mapping Object`對映到`B`的++虛擬++記憶體中,並且回傳虛擬記憶體中,此區塊的指標,並利用此指標,讀取到`A`寫入的資料。 6. `B`使用完畢後,也須使用`UnmapViewOfFile`釋放虛擬記憶體的區塊。 📍==當所有操作結束後,`A`和`B`都必須呼叫一次`CloseHandle`釋放`File Mapping Object`。== - 雖然`File Mapping Object`,只有當`A`呼叫`CreateFileMapping`時,在記憶體被建立一次。 - 但++建立`File Mapping Object`後,會產生一個「計數器」,記錄這個`File Mapping Object`被引用幾次++。 - 若`A`先用`CloseHandle`釋放`File Mapping Object`,但是`B`還沒有釋放,則`File Mapping Object`並不會真的被釋放,只會讓`A`的`handle`失效而已。 - 當所有引用該`File Mapping Object`的`process`都釋放`handle`(計數器歸零),`File Mapping Object`才會真正從記憶體中被釋放。 - `ProcessA`使用`CloseHandle`後,如何再次操作`File Mapping Object`? - 若`File Mapping Object`還沒被釋放,只需使用`OpenFileMapping`取得`handle`即可。 - 為何不同`process`,取得同一個`File Mapping Object`的`handle`值不同? - `CreateFileMapping`和`OpenFileMapping`回傳的`handle`是不一樣的,即使這兩個`handle`都指向同一個`File Mapping Object`。 - 這是因為每個`process`都有自己的`handle表單`,只有產生該`handle`的`process`知道是指向哪個`object`。 - 每次`process`呼叫`OpenFileMapping`,也會重新從表單取出新的`handle`。 - 可以簡單快速地說明,每個函數的用途嗎? ![image](https://hackmd.io/_uploads/HylUVDbhyg.png) ![image](https://hackmd.io/_uploads/HyMADwb3ke.png) - 在實體記憶體中,被建立的`File Mapping Object`,就是`Memory-Mapped File(記憶體對映檔)`,是實際存放共享資料的地方。 - `對映(map)`是一個動詞,指的是將記憶體對映檔,投射到`process`虛擬記憶體中的動作。 - 當記憶體對映檔,被投射到`process`虛擬記憶體中的某個區塊後,`process`彷彿就能透過這個區塊看到實際的對映檔(可視化),這個區塊就稱作`view`。 - 可以在哪裡看到自己建立的`File Mapping Object`? - 在`【工作管理員】->【效能】->【資源監視器】`,在`【處理程序】`勾選自己的程式。 - 然後在`【關聯控制代碼】`,搜尋`CreateFileMapping`指定的對映檔名稱。 ![image](https://hackmd.io/_uploads/H1UE2HK2yx.png) - 有些人會在對映檔名稱前,加上`Global\`,有什麼差別? - 在Windows切換不同使用者,系統會幫每個使用者都建立一個`Session`。 - 名稱加上`Global\`,可以在切換使用者後,用同一個對映檔名稱,找到同一個對映檔。 - 想在不同`process`之間,共享同一份資料的其它方式? - DLL共享區間 ## 參考資料 [File Mapping](https://learn.microsoft.com/en-us/windows/win32/memory/file-mapping "官方文件") [Creating Named Shared Memory](https://learn.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory "官方文件") [Windows API 之 CreateFile、CreateFileMapping 、MapViewOfFile](https://www.cnblogs.com/predator-wang/p/4902793.html) [Windows10系统FileMapping跨进程共享需要权限](https://blog.csdn.net/qq_31042143/article/details/121754159) [进程间通信:使用file-mapping kernel object(文件映射内核对象)](http://www.cppblog.com/canmeng50401/archive/2012/04/09/file-mapping-kernel-object.html)