C/C++: Shared Memory,不同 Process 資料交換的方式

發現手邊有 Shared memory 的 code
就整理整理出來做個紀錄
Shared memory
Process 之間的通訊方式有很多種,Shared memory 就是其中一種,且是速度最快的一種,但事情沒有兩好,Shared memory 內的資料維護要使用者去做維護,可以使用 semaphore 或其他的同步物件來同步資料。
Shared memory 用途:
- 傳遞訊息 IPC。
- 在不同的 process 上減少讀取重複的檔案。
以下就來介紹 Shared memory 流程中會用到的 function 吧 ~
Linux
POSIX 全名為 Portable Operating System Interface of UNIX,在現今有各式各樣的 UNIX 的衍伸版本,但如果他們的 API 都不一樣是不是都要一直重寫很麻煩,於是在 1988 年開始,IEEE 定義了一系列的 API,正式名稱為 IEEE Std 1003,或有人為了方便記憶稱作 POSIX。所以我們等等要介紹的 Linux 的 shared memory 就是使用 POSIX 的 API。
shm_open / shm_unlink
shm_open
: 創建 POSIX shared memory objectshm_unlink
: 移除 POSIX shared memory object
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
name
: shared memory object 的名稱,(不是路徑,只能是名稱),為了更好的移植性,建議用/
開頭,例如:/myshm。實測在 FreeBSD 上一定要用
/
開頭,但 Ubuntu 不用。oflag
: 讀取和寫入的權限、創建的方式,使用 bit mask 的方式輸入,例如O_RDWR | O_CREAT | O_EXCL
,相關定義在<fcntl.h>
O_RDONLY
: 只能讀取O_RDWR
: 讀寫都可以O_CREAT
: 如果 shared memory object 不存在,則新增一個新的O_EXCL
: 在使用O_CREAT
時,如果事先有存在同樣名稱的檔案,則回傳錯誤O_TRUNC
: 如果事先有存在同樣名稱的檔案,則截短至 0 byte (刪光光)
mode
: 用O_CREAT
新增的時候的檔案權限,就是 Linux 中檔案的權限,例如:0600,定義在<sys/stat.h>
Return Value:
shm_open
成功的時候會回傳 file descriptor (非負整數),shm_unlink
成功的時候會回傳 0,兩者失敗的時候都會回傳 -1,錯誤訊息在 errno
。
#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
const char* SM_NAME = "/SHARED_MEMORY_TEST";
// 建立 POSIX shared memory 物件
int fd = shm_open(
SM_NAME, // shared memory 物件名稱
O_CREAT | O_RDWR, // 自動創建及讀寫權限
0777); // 檔案權限
// 檢查是否建立成功
if(fd < 0){
printf("shm_open failed!, %s\n", strerror(errno));
return 1;
}
// 刪除 shared memory 物件
if(shm_unlink(SM_NAME) != 0){
printf("shm_unlink failed!, %s\n", strerror(errno));
return 1;
}
Reference:
ftruncate
在創建完 shared memory object 之後,預設的檔案大小為 0,我們可以使用 ftruncate
來調整檔案大小。
truncate
: 傳入檔案路徑調整檔案大小。ftruncate
: 傳入 file descriptor (fd) 調整檔案大小。
#include <unistd.h>
int truncate(const char *path, off_t length);
int ftruncate(int fildes, off_t length);
path
: 檔案路徑fildes
: 檔案指標 (file descriptor)length
: 目標的檔案大小,單位為 byte,如果目標檔案大小比實際的還小,則會刪掉多餘部分的資料,反之則會補 0 填補,另外要確保檔案可寫。
Return Value:
成功回傳 0,反之回傳 -1,錯誤訊息皆放置在 errno
。
#include <unistd.h>
#include <errno.h>
#include <string.h>
// 調整物件大小
int r = ftruncate(fd, sizeof(int));
// 檢查是否調整成功
if(r < 0){
printf("ftruncate failed!, %s\n", strerror(errno));
return 1;
}
Reference:
mmap
接著我們要把這個 shared memory object 映射到當前 process 的 virtual memory,這樣當前的 process 才可以直接存取。
mmap
: 在當前的 process 創建一個新的 mapping。munmap
: 刪除 mapping。
#include <sys/mman.h>
void* mmap(void* addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void* addr, size_t length);
addr
: mapping 到的位置,傳入NULL
時,會由 OS 決定位置,若不為NULL
,OS 會將之當作 hint 決定位置,通常傳入NULL
即可。length
: mapping 的大小,單位為 byte。prot
: memory 的讀寫權限。PROT_EXEC
: Memory 的 pages 可執行。PROT_READ
: Memory 的 pages 可讀。PROT_WRITE
: Memory 的 pages 可寫。PROT_NONE
: Memory 的 pages 不能存取。
flags
: 設定對 memory 的修改是否對其他 process 可見。MAP_SHARED
: 所有修改與其他 process 共享。MAP_PRIVATE
: 修改只在當前 process 可見,要修改時會複製該 page,再複製上進行更改,稱作 copy-on-write mapping。MAP_ANONYMOUS
: 因為是匿名的,map 的時候不使用 file descriptor,通常用於fork
後 parent 和 child 之間的溝通,因為 child 會繼承 parent 的 mapping。
fd
: file descriptor。offset
: 對 file descriptor 的 offset。
Return Value:
mmap
和 munmap
錯誤都會回傳 -1,成功的話,mmap
回傳指向的被映射的區域,munmap
回傳 0。
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
void *buf = mmap(
NULL, // 讓 OS 自動決定地址
sizeof(int), // map 的大小
PROT_READ | PROT_WRITE, // pages 可讀可寫
MAP_SHARED, // 將所有修改與其他 process 共享
fd, // file descriptor
0); // 不要做位移,從 fd 的頭開始
// 檢查是否映射成功
if(buf == MAP_FAILED){
printf("mmap failed!, %s\n", strerror(errno));
return 1;
}
// 刪除映射
if(munmap(buf, sizeof(int)) != 0){
printf("munmap failed!, %s\n", strerror(errno));
return 1;
}
一般來說,memory mapping 是將檔案映射到 virtual memory 上,因此要傳入 file descriptor,但其實也可以不要使用 file,就是使用 anonymous memory mapping,應用上透過匿名映射,parent process 可以和 child process 溝通。
除了進程之間的溝通,linux 的 malloc
也和 memory mapping 有關係,MMAP_THRESHOLD
是一個閥值,當 malloc 要配置的大小大於 MMAP_THRESHOLD
,則會使用 memory mapping,並且是 private anonymous memory mapping,反之則是透過 process 的 heap 來提供,這樣做的好處是當你釋放了 malloc 所配置的空間後,使用 memory mapping 的空間會直接歸還給系統,反之 heap 的空間就算你 free 了之後,大部分的系統會將分配到的 pages 繼續放在該 process,之後可以直接 reuse,如此一來各個 process 可以更靈活地充分使用 memory。
提一下:private anonymous memory mapping 的話並不是馬上分配空間給你,而是你第一次 access 的時候才分配空間給你,因此第一次存取的時間會比較久,且 kernel 必須要將內容清 0 之後再給你使用,避免洩漏其他 process 的資訊。
Reference:
Reference:
- Document - mmap/munmap
- Document - ftruncate
- 30-05 之應用層的 I/O 加速 - 零拷貝 ( Zero Copy )
- How to use shared memory with Linux in C
Example
全部組一起的話就會是下面這樣,其他的 process 就只要打開相同的文件,映射到自己 process 的 virtual memory 上就可以讀取了 ~
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
int main(){
const char* shared_memory_name = "SHARED_MEMORY_TEST";
size_t bufSize = sizeof(int);
// 創建 shared memory object
int fd = shm_open(
shared_memory_name,
O_CREAT | O_RDWR,
0777);
if(fd < 0){
printf("shm_open failed!, %s\n", strerror(errno));
return 1;
}
// 調整大小
int r = ftruncate(fd, bufSize);
if(r < 0){
printf("ftruncate failed!, %s\n", strerror(errno));
return 1;
}
// memory mapping
void *buf = mmap(
NULL,
sizeof(int),
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0);
// 檢查是否映射成功
if(buf == MAP_FAILED){
printf("mmap failed!, %s\n", strerror(errno));
return 1;
}
int* integer = ((int*)buf);
*integer = 1;
// 刪除映射
if(munmap(buf, sizeof(int)) != 0){
printf("munmap failed!, %s\n", strerror(errno));
return 1;
}
// 刪除 shared memory object
if(shm_unlink(shared_memory_name) != 0){
printf("shm_unlink failed!, %s\n", strerror(errno));
return 1;
}
return 0;
}
編譯的時候記得加上 -lrt
,rt 是 POSIX Realtime Extension,可以打 man librt 查看。
gcc test.cpp -o test.exe -lrt
Windows
Windows 的部分我們這邊列出使用 window.h
的操作方式,流程與 Linux 是一樣的,差在呼叫的 function 不同。
CreateFileMapping / OpenFileMapping
為某個文件創建 file mapping object。
HANDLE CreateFileMapping(
[in] HANDLE hFile,
[in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
[in] DWORD flProtect,
[in] DWORD dwMaximumSizeHigh,
[in] DWORD dwMaximumSizeLow,
[in, optional] LPCSTR lpName
);
hFile
: mapping 的 file handle,如果設定為INVALID_HANDLE_VALUE
,則依照dwMaximumSizeHigh
和dwMaximumSizeLow
所設定的大小決定 file mapping object 的大小。lpFileMappingAttributes
: 決定是否 file mapping object 可以被 child process 繼承,如果設為 NULL,則 child process 不能繼承,且設為預設的安全設定。flProtect
: file mapping object 的 page 保護設定。PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_EXECUTE_WRITECOPY
PAGE_READONLY
PAGE_READWRITE
PAGE_WRITECOPY
dwMaximumSizeHigh
: file mapping object 的最大大小的高位元的 (high-order) DWORDdwMaximumSizeLow
: file mapping object 的最大大小的高位元的 (low-order) DWORDlpName
: file mapping object 的名稱,如果要在不同的 terminal server sessions 中與其他 process 溝通的話,前面要加上Global\
。
Return Value:
回傳新創建的 file mapping object 的 handle。
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
TCHAR szName[] = TEXT("Local\\TESTTESTTEST");
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)
szName); // name of mapping object
if (hMapFile == NULL){
_tprintf(TEXT("Could not create file mapping object (%d).\n"),
GetLastError());
return 1;
}
可以選擇 Global 和 Local 來指定物件建立的空間,一般狀況用 Local 即可
HANDLE OpenFileMappingA(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] LPCSTR lpName
);
dwDesiredAccess
: 期望的權限。bInheritHandle
: 是否可以被繼承,一般都是否。lpName
: file mapping object 的名稱。
Return Value:
回傳打開的 file mapping object 的 handle。
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
TCHAR szName[] = TEXT("Global\\TESTTESTTEST");
HANDLE hMapFile;
LPCTSTR pBuf;
hMapFile = OpenFileMapping(
FILE_MAP_ALL_ACCESS, // read/write access
FALSE, // do not inherit the name
szName); // name of mapping object
if (hMapFile == NULL){
_tprintf(TEXT("Could not open file mapping object (%d).\n"),
GetLastError());
return 1;
}
MapViewOfFile / UnmapViewOfFile
MapViewOfFile
: 映射檔案到當前 process 的空間上。UnmapViewOfFile
: 取消映射檔案到當前 process 的空間上。
LPVOID MapViewOfFile(
[in] HANDLE hFileMappingObject,
[in] DWORD dwDesiredAccess,
[in] DWORD dwFileOffsetHigh,
[in] DWORD dwFileOffsetLow,
[in] SIZE_T dwNumberOfBytesToMap
);
BOOL UnmapViewOfFile(
[in] LPCVOID lpBaseAddress
);
hFileMappingObject
: 要映射的檔案的 handle。dwDesiredAccess
: 映射檔案的存取權限。FILE_MAP_ALL_ACCESS
FILE_MAP_READ
FILE_MAP_WRITE
FILE_MAP_COPY
FILE_MAP_EXECUTE
FILE_MAP_LARGE_PAGES
FILE_MAP_TARGETS_INVALID
dwFileOffsetHigh
: 映射檔案的位移的高位元的 (high-order) DWORD。dwFileOffsetLow
: 映射檔案的位移的高位元的 (low-order) DWORD。dwNumberOfBytesToMap
: 映射的檔案大小,單位為 byte。lpBaseAddress
: 要取消映射的起始地址。
Return Value:
- 成功: 回傳映射的空間位置
- 失敗: 回傳
NULL
pBuf = (LPTSTR) MapViewOfFile(hMapFile, // handle to map object
FILE_MAP_ALL_ACCESS, // read/write permission
0,
0,
BUF_SIZE);
if (pBuf == NULL){
_tprintf(TEXT("Could not map view of file (%d).\n"),
GetLastError());
// 記得關掉先前創建的 file mapping object
CloseHandle(hMapFile);
return 1;
}
UnmapViewOfFile(pBuf);
Example
例子可自微軟官網觀看 ~ LINK。
Reference
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪