目錄

廣告 AD

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

發現手邊有 Shared memory 的 code

就整理整理出來做個紀錄

廣告 AD

Process 之間的通訊方式有很多種,Shared memory 就是其中一種,且是速度最快的一種,但事情沒有兩好,Shared memory 內的資料維護要使用者去做維護,可以使用 semaphore 或其他的同步物件來同步資料。

Shared memory 用途:

  1. 傳遞訊息 IPC。
  2. 在不同的 process 上減少讀取重複的檔案。

以下就來介紹 Shared memory 流程中會用到的 function 吧 ~

POSIX 全名為 Portable Operating System Interface of UNIX,在現今有各式各樣的 UNIX 的衍伸版本,但如果他們的 API 都不一樣是不是都要一直重寫很麻煩,於是在 1988 年開始,IEEE 定義了一系列的 API,正式名稱為 IEEE Std 1003,或有人為了方便記憶稱作 POSIX。所以我們等等要介紹的 Linux 的 shared memory 就是使用 POSIX 的 API。

  • shm_open: 創建 POSIX shared memory object
  • shm_unlink: 移除 POSIX shared memory object

Cpp

#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

Cpp

#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:


在創建完 shared memory object 之後,預設的檔案大小為 0,我們可以使用 ftruncate 來調整檔案大小。

  • truncate: 傳入檔案路徑調整檔案大小。
  • ftruncate: 傳入 file descriptor (fd) 調整檔案大小。

Cpp

#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

Cpp

#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:


接著我們要把這個 shared memory object 映射到當前 process 的 virtual memory,這樣當前的 process 才可以直接存取。

  • mmap: 在當前的 process 創建一個新的 mapping。
  • munmap: 刪除 mapping。

Cpp

#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:

mmapmunmap 錯誤都會回傳 -1,成功的話,mmap 回傳指向的被映射的區域,munmap 回傳 0。

Cpp

#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:


全部組一起的話就會是下面這樣,其他的 process 就只要打開相同的文件,映射到自己 process 的 virtual memory 上就可以讀取了 ~

Cpp

#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 查看。

bash

gcc test.cpp -o test.exe -lrt

Windows 的部分我們這邊列出使用 window.h 的操作方式,流程與 Linux 是一樣的,差在呼叫的 function 不同。

為某個文件創建 file mapping object。

Cpp

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,則依照 dwMaximumSizeHighdwMaximumSizeLow 所設定的大小決定 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) DWORD
  • dwMaximumSizeLow: file mapping object 的最大大小的高位元的 (low-order) DWORD
  • lpName: file mapping object 的名稱,如果要在不同的 terminal server sessions 中與其他 process 溝通的話,前面要加上 Global\

Return Value:

回傳新創建的 file mapping object 的 handle。

Cpp

#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 即可

Cpp

HANDLE OpenFileMappingA(
  [in] DWORD  dwDesiredAccess,
  [in] BOOL   bInheritHandle,
  [in] LPCSTR lpName
);
  • dwDesiredAccess: 期望的權限。
  • bInheritHandle: 是否可以被繼承,一般都是否。
  • lpName: file mapping object 的名稱。

Return Value:

回傳打開的 file mapping object 的 handle。

Cpp

#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: 映射檔案到當前 process 的空間上。
  • UnmapViewOfFile: 取消映射檔案到當前 process 的空間上。

Cpp

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

Cpp

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);

例子可自微軟官網觀看 ~ LINK

廣告 AD