使用 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

    pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy