使用 C++ 和 Node.js 在樹莓派上透過共享記憶體實現跨進程通訊

2024/11/01 C++

前言

本文將詳細介紹如何在樹莓派上使用 C++ 實現共享記憶體的讀寫,並透過 Node.js 從共享記憶體中讀取數據,實現跨語言、跨進程的數據交互。我們將利用 POSIX 共享記憶體(shm_open 和 mmap)來實現進程間通訊,並使用 Node.js 的 ffi-napi 和 ref-napi 模組呼叫 C 標準庫函數,從共享記憶體中讀取數據。

  • [任務一] 使用 C++ 讀寫數值:在這一部分,我們將撰寫兩個 C++ 程式,一個負責將浮點數寫入共享記憶體,另一個負責從共享記憶體讀取浮點數。

  • [任務二] 使用 Node.js 讀取數值:接著,我們將撰寫一個 Node.js 程式,通過呼叫 C 標準庫函數,從共享記憶體中讀取浮點數,實現跨語言的數據交互。

[Task1] 使用C++讀寫數值

在樹莓派上撰寫兩個 C++ 程式:一個負責將浮點數寫入共享記憶體,另一個負責從共享記憶體讀取浮點數。在 Linux 系統(如樹莓派)上,可以使用 POSIX 共享記憶體(shm_open 和 mmap)來實現進程間通訊。

寫入程式(shared_memory_writer.cpp)

#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h>    /* For O_* constants */
#include <unistd.h>
#include <cstring>

int main() {
    const char *name = "/my_shared_memory"; // 共享記憶體名稱
    const size_t SIZE = sizeof(float);      // 共享記憶體大小

    // 創建共享記憶體對象
    int shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        std::cerr << "共享記憶體創建失敗" << std::endl;
        return 1;
    }

    // 設置共享記憶體大小
    if (ftruncate(shm_fd, SIZE) == -1) {
        std::cerr << "設定共享記憶體大小失敗" << std::endl;
        return 1;
    }

    // 映射共享記憶體
    void *ptr = mmap(0, SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        std::cerr << "共享記憶體映射失敗" << std::endl;
        return 1;
    }

    float data = 3.14159f; // 要寫入的浮點數

    // 寫入共享記憶體
    memcpy(ptr, &data, sizeof(float));

    std::cout << "已寫入浮點數:" << data << std::endl;

    // 解除映射
    munmap(ptr, SIZE);

    // 關閉共享記憶體對象
    close(shm_fd);

    return 0;
}

讀取程式(shared_memory_reader.cpp)

#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h>    /* For O_* constants */
#include <unistd.h>
#include <cstring>

int main() {
    const char *name = "/my_shared_memory"; // 與寫入程式相同的共享記憶體名稱
    const size_t SIZE = sizeof(float);      // 共享記憶體大小

    // 打開共享記憶體對象
    int shm_fd = shm_open(name, O_RDONLY, 0666);
    if (shm_fd == -1) {
        std::cerr << "共享記憶體打開失敗" << std::endl;
        return 1;
    }

    // 映射共享記憶體
    void *ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
    if (ptr == MAP_FAILED) {
        std::cerr << "共享記憶體映射失敗" << std::endl;
        return 1;
    }

    float data;

    // 從共享記憶體讀取
    memcpy(&data, ptr, sizeof(float));

    std::cout << "讀取到的浮點數:" << data << std::endl;

    // 解除映射
    munmap(ptr, SIZE);

    // 關閉共享記憶體對象
    close(shm_fd);

    // 刪除共享記憶體對象
    shm_unlink(name);

    return 0;
}

編譯與執行步驟

1. 編譯程式

在終端中,進入程式所在的目錄,然後執行以下命令:

g++ -o  shared_memory_writer shared_memory_writer.cpp
g++ -o  shared_memory_reader shared_memory_reader.cpp

如果發生編譯失敗(特別是出現與 shm_open 或 POSIX 實時函數相關的錯誤),可以在編譯命令後加上 -lrt,如下所示:

g++ -o  shared_memory_writer shared_memory_writer.cpp -lrt
g++ -o  shared_memory_reader shared_memory_reader.cpp -lrt

為什麼需要 -lrt?

  • POSIX 實時庫-lrt 用於鏈接 POSIX 實時庫,其中包含了一些特殊的 POSIX 實時功能,如 shm_open 和 shm_unlink 等函數。
  • 系統差異:是否需要 -lrt 取決於系統的特性、編譯器版本,以及標準函式庫的配置。一些新的 Linux 發行版(如 Debian、Ubuntu、Raspbian 等)的標準 C 庫(glibc)已經內建支援實時函數,無需額外指定 -lrt。例如筆者在 Jetson nano orin 上面就必須加。

2. 執行程式

首先運行寫入程式:

./shared_memory_writer

輸出應該會顯示:

3.14159

然後運行讀取程式:

./shared_memory_reader

輸出應該會顯示:

3.14159

注意事項:

  • 共享記憶體名稱: 在 shm_open 中使用的名稱必須以斜杠 / 開頭,例如 /my_shared_memory。
  • 權限設置: 在創建共享記憶體時,權限設置為 0666,表示所有用戶都可讀寫。如果需要更嚴格的權限,請根據需要進行調整。
  • 刪除共享記憶體: 在讀取程式中,使用 shm_unlink 刪除共享記憶體對象,這是為了防止資源洩漏。如果您需要多次讀取,請在適當的時機進行刪除。
  • 錯誤處理: 在實際應用中,建議對每個系統呼叫都進行錯誤檢查,並適當處理錯誤。
  • 共享記憶體刪除:執行讀取程式後,該程式會將共享記憶體對象(/my_shared_memory)刪除。因此,再次執行 shared_memory_reader 時會出現「共享記憶體打開失敗」的錯誤,這是正常的。
  • 避免自動刪除:如果您不想每次讀取後都刪除共享記憶體,可以將讀取程式最後一行的 shm_unlink(name); 註解掉。
  • 共享記憶體的存儲位置:共享記憶體存儲在 RAM 中,系統重新啟動後會自動清空。

[Task2] 使用 Node.js 讀取數值

接下來要撰寫一個 Node.js 程式,用於從 C++ 程式寫入的共享記憶體中讀取浮點數值。在 Node.js 中,沒有內建的方法直接訪問 POSIX 共享記憶體。但是,我們可以使用 ffi-napi 和 ref-napi 模組來呼叫 C 標準庫函數,如 shm_open、mmap 等,以實現從共享記憶體中讀取數據。

步驟 1:安裝必要的 Node.js 模組

首先,需要在專案目錄中中安裝 ffi-napi 和 ref-napi:

npm install ffi-napi ref-napi

步驟 2:撰寫 Node.js 程式

創建一個名為 index.js 的檔案,並添加以下內容:

// ldd --version > (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35
const ffi = require('ffi-napi');
const ref = require('ref-napi');

// 定義 C 標準類型和函數
const voidPtr = ref.refType(ref.types.void);
const off_t = ref.types.long;
const size_t = ref.types.size_t;
const int = ref.types.int;

// 載入 libc 庫
const libc = ffi.Library(null, {
    shm_open: [int, ['string', int, int]],
  mmap: [voidPtr, [voidPtr, size_t, int, int, int, off_t]],
  munmap: [int, [voidPtr, size_t]],
  close: [int, [int]],
  shm_unlink: [int, ['string']]
});


// 定義常量
const O_RDONLY = 0;         // 只讀模式
const PROT_READ = 0x1;      // 頁面可被讀取
const MAP_SHARED = 0x01;    // 與其他所有映射該對象的進程共享

const name = '/my_shared_memory'; // 與 C++ 程式中相同的共享記憶體名稱
const SIZE = ref.types.float.size; // float 的大小(通常為 4 個字節)

// 打開共享記憶體
const shm_fd = libc.shm_open(name, O_RDONLY, 0o666);
if (shm_fd === -1) {
  console.error('共享記憶體打開失敗');
  process.exit(1);
}

// 映射共享記憶體
const ptr = libc.mmap(
  null,
  SIZE,
  PROT_READ,
  MAP_SHARED,
  shm_fd,
  0
);

if (ptr.address() === 0 || ptr.isNull()) {
  console.error('共享記憶體映射失敗');
  libc.close(shm_fd);
  process.exit(1);
}

// 將指標轉換為 Buffer
const buffer = ref.reinterpret(ptr, SIZE);

// 讀取浮點數(小端模式)
const data = buffer.readFloatLE(0);

console.log('讀取到的浮點數:', data);

// 解除映射
libc.munmap(ptr, SIZE);

// 關閉共享記憶體文件描述符
libc.close(shm_fd);

// 刪除共享記憶體對象(如果需要)
libc.shm_unlink(name);
  • 引入模組: 使用 ffi-napi 來呼叫 C 標準庫函數,ref-napi 用於處理指針和數據類型。
  • 定義函數: 使用 ffi.Library 加載 C 標準庫中的函數,如 shm_open、mmap、munmap 等。
  • 設置常量: 定義需要的標誌和權限常量。這些常量的值可能根據系統不同而有所變化,請根據您的系統進行調整。
  • 打開共享記憶體: 使用 shm_open 打開共享記憶體對象。
  • 映射共享記憶體: 使用 mmap 將共享記憶體映射到進程的地址空間。
  • 讀取數據: 使用 ref 模組從共享記憶體中讀取浮點數值。
  • 清理資源: 解除映射,關閉文件描述符,並根據需要刪除共享記憶體對象。

步驟 3:運行程式

確保 C++ 寫入程式已經運行: 首先,運行編譯好的 shared_memory_writer.cpp,以創建並寫入共享記憶體。

./shared_memory_writer

運行 Node.js 讀取程式

node index.js

應該會看到類似以下的輸出:

讀取到的浮點數:3.14159

如果執行 Node.js 程式失敗,請回想是否在 C++ 編譯時曾遇到需要加上 -lrt 才成功編譯的情況。這可能表示 Node.js 程式也需要額外連接 POSIX 庫,才能呼叫所需的函數。詳細的程式碼實作範例,請參考 GitHub

範例程式可以從GitHub中取得!

鼓勵持續創作,支持化讚為賞!透過下方的 Like 拍手👏,讓創作者獲得額外收入~
版主10在2020年首次開設YouTube頻道,嘗試拍攝程式教學。想要了解更多的朋友歡迎關注我的頻道,您的訂閱就是最大的支持~如果想學其他什麼內容也歡迎許願XD
https://www.youtube.com/channel/UCSNPCGvMYEV-yIXAVt3FA5A

Search

    Table of Contents