C++实现内存数据共享:利用共享内存与操作系统API实现跨进程数据交换

好的,我们开始今天的讲座,主题是C++实现内存数据共享:利用共享内存与操作系统API实现跨进程数据交换。

一、引言:进程间通信的重要性

在多任务操作系统中,不同的进程拥有独立的内存空间,彼此之间无法直接访问对方的内存。然而,在许多应用场景下,我们需要让不同的进程能够共享数据,协同工作。例如,一个进程负责数据采集,另一个进程负责数据处理,或者多个进程共同维护一个数据库。这时,就需要用到进程间通信(IPC)机制。

共享内存是IPC的一种重要方式,它允许多个进程访问同一块物理内存区域,从而实现数据的快速共享。由于进程直接访问内存,避免了数据复制的开销,因此共享内存通常具有很高的效率。

二、共享内存的基本原理

共享内存的实现依赖于操作系统提供的API。其基本原理是:

  1. 创建共享内存区域: 由一个进程(通常是服务器进程)向操作系统申请一块共享内存区域,并指定其大小和访问权限。
  2. 映射共享内存区域: 其他进程(通常是客户端进程)将该共享内存区域映射到自己的地址空间。
  3. 访问共享内存: 映射完成后,所有映射了该共享内存区域的进程都可以像访问普通内存一样访问该区域,从而实现数据的共享。
  4. 同步与互斥: 由于多个进程可以同时访问共享内存,因此需要使用同步机制(如互斥锁、信号量)来保证数据的一致性和完整性。
  5. 解除映射与销毁: 当进程不再需要访问共享内存时,需要解除映射。当所有进程都解除映射后,可以销毁共享内存区域。

三、Windows平台上的共享内存实现

在Windows平台上,可以使用以下API来实现共享内存:

  • CreateFileMapping():创建或打开一个命名的文件映射对象,该对象可以用来表示共享内存区域。
  • MapViewOfFile():将文件映射对象映射到进程的地址空间。
  • UnmapViewOfFile():解除文件映射对象的映射。
  • CloseHandle():关闭文件映射对象。

下面是一个简单的示例,演示如何在Windows上创建和使用共享内存:

// Server process (writer)
#include <iostream>
#include <windows.h>

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    HANDLE hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,   // Use paging file
        NULL,                   // Default security
        PAGE_READWRITE,         // Read/write access
        0,                      // Max. object size (high DWORD)
        SHARED_MEMORY_SIZE,     // Max. object size (low DWORD)
        SHARED_MEMORY_NAME);    // Name of mapping object

    if (hMapFile == NULL) {
        std::cerr << "Could not create file mapping object (" << GetLastError() << ")." << std::endl;
        return 1;
    }

    LPVOID lpBaseAddress = MapViewOfFile(
        hMapFile,               // Handle to map object
        FILE_MAP_ALL_ACCESS,    // Read/write access
        0,                      // File offset high DWORD
        0,                      // File offset low DWORD
        SHARED_MEMORY_SIZE);    // Number of bytes to map

    if (lpBaseAddress == NULL) {
        std::cerr << "Could not map view of file (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMapFile);
        return 1;
    }

    // Write data to shared memory
    char* pData = (char*)lpBaseAddress;
    strcpy_s(pData, SHARED_MEMORY_SIZE, "Hello from server!");
    std::cout << "Server: Wrote data to shared memory." << std::endl;

    // Wait for a while to allow the client to read
    Sleep(5000);

    // Unmap and close
    UnmapViewOfFile(lpBaseAddress);
    CloseHandle(hMapFile);

    return 0;
}

// Client process (reader)
#include <iostream>
#include <windows.h>

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,   // Read/write access
        FALSE,                 // Do not inherit the name
        SHARED_MEMORY_NAME);    // Name of mapping object

    if (hMapFile == NULL) {
        std::cerr << "Could not open file mapping object (" << GetLastError() << ")." << std::endl;
        return 1;
    }

    LPVOID lpBaseAddress = MapViewOfFile(
        hMapFile,               // Handle to map object
        FILE_MAP_ALL_ACCESS,    // Read/write access
        0,                      // File offset high DWORD
        0,                      // File offset low DWORD
        SHARED_MEMORY_SIZE);    // Number of bytes to map

    if (lpBaseAddress == NULL) {
        std::cerr << "Could not map view of file (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMapFile);
        return 1;
    }

    // Read data from shared memory
    char* pData = (char*)lpBaseAddress;
    std::cout << "Client: Read data from shared memory: " << pData << std::endl;

    // Unmap and close
    UnmapViewOfFile(lpBaseAddress);
    CloseHandle(hMapFile);

    return 0;
}

代码解释:

  • Server Process (Writer):

    • CreateFileMapping() 创建了一个名为 "MySharedMemory" 的共享内存区域。INVALID_HANDLE_VALUE 表示使用页面文件(系统管理的虚拟内存)。PAGE_READWRITE 指定了读写权限。
    • MapViewOfFile() 将该共享内存区域映射到服务器进程的地址空间。
    • 服务器进程将字符串 "Hello from server!" 写入共享内存。
    • Sleep(5000) 暂停 5 秒,给客户端进程足够的时间来读取数据。
    • UnmapViewOfFile() 解除映射,CloseHandle() 关闭文件映射对象。
  • Client Process (Reader):

    • OpenFileMapping() 打开了已存在的共享内存区域 "MySharedMemory"。
    • MapViewOfFile() 将该共享内存区域映射到客户端进程的地址空间。
    • 客户端进程从共享内存读取数据并打印到控制台。
    • UnmapViewOfFile() 解除映射,CloseHandle() 关闭文件映射对象。

编译和运行:

  1. 将上面的代码分别保存为 server.cppclient.cpp
  2. 使用 C++ 编译器(例如 Visual Studio)编译这两个文件。
  3. 先运行 server.exe,再运行 client.exe

你应该看到客户端进程成功读取了服务器进程写入的数据。

四、Linux平台上的共享内存实现

在Linux平台上,可以使用以下API来实现共享内存:

  • shmget():创建或获取一个共享内存标识符。
  • shmat():将共享内存段连接到进程的地址空间。
  • shmdt():将共享内存段从进程的地址空间分离。
  • shmctl():控制共享内存段。

下面是一个简单的示例,演示如何在Linux上创建和使用共享内存:

// Server process (writer)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>

const int SHARED_MEMORY_SIZE = 1024;
const key_t SHM_KEY = 1234; // Unique key for the shared memory

int main() {
    // Create the shared memory segment
    int shmid = shmget(SHM_KEY, SHARED_MEMORY_SIZE, IPC_CREAT | 0666);
    if (shmid < 0) {
        std::cerr << "shmget error" << std::endl;
        return 1;
    }

    // Attach the shared memory segment to our address space
    char* shared_memory = (char*)shmat(shmid, NULL, 0);
    if (shared_memory == (char*)-1) {
        std::cerr << "shmat error" << std::endl;
        return 1;
    }

    // Write data to shared memory
    strcpy(shared_memory, "Hello from server!");
    std::cout << "Server: Wrote data to shared memory." << std::endl;

    // Wait for a while to allow the client to read
    sleep(5);

    // Detach from shared memory
    if (shmdt(shared_memory) == -1) {
        std::cerr << "shmdt error" << std::endl;
        return 1;
    }

    // Destroy the shared memory segment
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        std::cerr << "shmctl error" << std::endl;
        return 1;
    }

    return 0;
}

// Client process (reader)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>

const int SHARED_MEMORY_SIZE = 1024;
const key_t SHM_KEY = 1234; // Unique key for the shared memory

int main() {
    // Locate the shared memory segment
    int shmid = shmget(SHM_KEY, SHARED_MEMORY_SIZE, 0666);
    if (shmid < 0) {
        std::cerr << "shmget error" << std::endl;
        return 1;
    }

    // Attach the shared memory segment to our address space
    char* shared_memory = (char*)shmat(shmid, NULL, 0);
    if (shared_memory == (char*)-1) {
        std::cerr << "shmat error" << std::endl;
        return 1;
    }

    // Read data from shared memory
    std::cout << "Client: Read data from shared memory: " << shared_memory << std::endl;

    // Detach from shared memory
    if (shmdt(shared_memory) == -1) {
        std::cerr << "shmdt error" << std::endl;
        return 1;
    }

    return 0;
}

代码解释:

  • Server Process (Writer):

    • shmget() 创建了一个共享内存段,SHM_KEY 是一个唯一的键值,用于标识该共享内存段。IPC_CREAT | 0666 表示如果共享内存段不存在则创建,并设置读写权限。
    • shmat() 将该共享内存段连接到服务器进程的地址空间。
    • 服务器进程将字符串 "Hello from server!" 写入共享内存。
    • sleep(5) 暂停 5 秒,给客户端进程足够的时间来读取数据。
    • shmdt() 将共享内存段从服务器进程的地址空间分离。
    • shmctl(shmid, IPC_RMID, NULL) 销毁共享内存段。
  • Client Process (Reader):

    • shmget() 获取已存在的共享内存段的标识符。
    • shmat() 将该共享内存段连接到客户端进程的地址空间。
    • 客户端进程从共享内存读取数据并打印到控制台。
    • shmdt() 将共享内存段从客户端进程的地址空间分离。

编译和运行:

  1. 将上面的代码分别保存为 server.cppclient.cpp
  2. 使用 C++ 编译器(例如 g++)编译这两个文件:
    g++ server.cpp -o server
    g++ client.cpp -o client
  3. 先运行 server,再运行 client

你应该看到客户端进程成功读取了服务器进程写入的数据。

五、共享内存的同步与互斥

由于多个进程可以同时访问共享内存,因此需要使用同步机制来保证数据的一致性和完整性。常用的同步机制包括:

  • 互斥锁(Mutex): 用于保护共享资源,防止多个进程同时访问。
  • 信号量(Semaphore): 用于控制对共享资源的访问数量。
  • 条件变量(Condition Variable): 用于在进程之间发送信号,通知对方某个条件已经满足。

Windows平台上的互斥锁:

// Server process (writer)
#include <iostream>
#include <windows.h>

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const char* MUTEX_NAME = "MyMutex";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    // Create a mutex
    HANDLE hMutex = CreateMutex(
        NULL,              // Default security attributes
        FALSE,             // Initially not owned
        MUTEX_NAME);        // Mutex name

    if (hMutex == NULL) {
        std::cerr << "Could not create mutex (" << GetLastError() << ")." << std::endl;
        return 1;
    }

    HANDLE hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,   // Use paging file
        NULL,                   // Default security
        PAGE_READWRITE,         // Read/write access
        0,                      // Max. object size (high DWORD)
        SHARED_MEMORY_SIZE,     // Max. object size (low DWORD)
        SHARED_MEMORY_NAME);    // Name of mapping object

    if (hMapFile == NULL) {
        std::cerr << "Could not create file mapping object (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMutex);
        return 1;
    }

    LPVOID lpBaseAddress = MapViewOfFile(
        hMapFile,               // Handle to map object
        FILE_MAP_ALL_ACCESS,    // Read/write access
        0,                      // File offset high DWORD
        0,                      // File offset low DWORD
        SHARED_MEMORY_SIZE);    // Number of bytes to map

    if (lpBaseAddress == NULL) {
        std::cerr << "Could not map view of file (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMapFile);
        CloseHandle(hMutex);
        return 1;
    }

    // Acquire the mutex
    WaitForSingleObject(hMutex, INFINITE);

    // Write data to shared memory
    char* pData = (char*)lpBaseAddress;
    strcpy_s(pData, SHARED_MEMORY_SIZE, "Hello from server!");
    std::cout << "Server: Wrote data to shared memory." << std::endl;

    // Release the mutex
    ReleaseMutex(hMutex);

    // Wait for a while to allow the client to read
    Sleep(5000);

    // Unmap and close
    UnmapViewOfFile(lpBaseAddress);
    CloseHandle(hMapFile);
    CloseHandle(hMutex);

    return 0;
}

// Client process (reader)
#include <iostream>
#include <windows.h>

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const char* MUTEX_NAME = "MyMutex";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    // Open the mutex
    HANDLE hMutex = OpenMutex(
        SYNCHRONIZE,        // Request synchronize access
        FALSE,              // Handle not inheritable
        MUTEX_NAME);        // Object name

    if (hMutex == NULL) {
        std::cerr << "Could not open mutex (" << GetLastError() << ")." << std::endl;
        return 1;
    }

    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,   // Read/write access
        FALSE,                 // Do not inherit the name
        SHARED_MEMORY_NAME);    // Name of mapping object

    if (hMapFile == NULL) {
        std::cerr << "Could not open file mapping object (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMutex);
        return 1;
    }

    LPVOID lpBaseAddress = MapViewOfFile(
        hMapFile,               // Handle to map object
        FILE_MAP_ALL_ACCESS,    // Read/write access
        0,                      // File offset high DWORD
        0,                      // File offset low DWORD
        SHARED_MEMORY_SIZE);    // Number of bytes to map

    if (lpBaseAddress == NULL) {
        std::cerr << "Could not map view of file (" << GetLastError() << ")." << std::endl;
        CloseHandle(hMapFile);
        CloseHandle(hMutex);
        return 1;
    }

    // Acquire the mutex
    WaitForSingleObject(hMutex, INFINITE);

    // Read data from shared memory
    char* pData = (char*)lpBaseAddress;
    std::cout << "Client: Read data from shared memory: " << pData << std::endl;

    // Release the mutex
    ReleaseMutex(hMutex);

    // Unmap and close
    UnmapViewOfFile(lpBaseAddress);
    CloseHandle(hMapFile);
    CloseHandle(hMutex);

    return 0;
}

Linux平台上的互斥锁:

// Server process (writer)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
#include <pthread.h>

const int SHARED_MEMORY_SIZE = 1024;
const key_t SHM_KEY = 1234; // Unique key for the shared memory

struct SharedData {
    pthread_mutex_t mutex;
    char data[SHARED_MEMORY_SIZE];
};

int main() {
    // Create the shared memory segment
    int shmid = shmget(SHM_KEY, sizeof(SharedData), IPC_CREAT | 0666);
    if (shmid < 0) {
        std::cerr << "shmget error" << std::endl;
        return 1;
    }

    // Attach the shared memory segment to our address space
    SharedData* shared_data = (SharedData*)shmat(shmid, NULL, 0);
    if (shared_data == (SharedData*)-1) {
        std::cerr << "shmat error" << std::endl;
        return 1;
    }

    // Initialize the mutex
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); // Make mutex process-shared
    pthread_mutex_init(&shared_data->mutex, &attr);
    pthread_mutexattr_destroy(&attr);

    // Lock the mutex
    pthread_mutex_lock(&shared_data->mutex);

    // Write data to shared memory
    strcpy(shared_data->data, "Hello from server!");
    std::cout << "Server: Wrote data to shared memory." << std::endl;

    // Unlock the mutex
    pthread_mutex_unlock(&shared_data->mutex);

    // Wait for a while to allow the client to read
    sleep(5);

    // Detach from shared memory
    if (shmdt(shared_data) == -1) {
        std::cerr << "shmdt error" << std::endl;
        return 1;
    }

    // Destroy the shared memory segment
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        std::cerr << "shmctl error" << std::endl;
        return 1;
    }

    return 0;
}

// Client process (reader)
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
#include <pthread.h>

const int SHARED_MEMORY_SIZE = 1024;
const key_t SHM_KEY = 1234; // Unique key for the shared memory

struct SharedData {
    pthread_mutex_t mutex;
    char data[SHARED_MEMORY_SIZE];
};

int main() {
    // Locate the shared memory segment
    int shmid = shmget(SHM_KEY, sizeof(SharedData), 0666);
    if (shmid < 0) {
        std::cerr << "shmget error" << std::endl;
        return 1;
    }

    // Attach the shared memory segment to our address space
    SharedData* shared_data = (SharedData*)shmat(shmid, NULL, 0);
    if (shared_data == (SharedData*)-1) {
        std::cerr << "shmat error" << std::endl;
        return 1;
    }

    // Lock the mutex
    pthread_mutex_lock(&shared_data->mutex);

    // Read data from shared memory
    std::cout << "Client: Read data from shared memory: " << shared_data->data << std::endl;

    // Unlock the mutex
    pthread_mutex_unlock(&shared_data->mutex);

    // Detach from shared memory
    if (shmdt(shared_data) == -1) {
        std::cerr << "shmdt error" << std::endl;
        return 1;
    }

    return 0;
}

代码解释:

  • Windows:

    • CreateMutex() 创建一个互斥锁对象。WaitForSingleObject() 等待互斥锁变为可用,ReleaseMutex() 释放互斥锁。
    • 客户端和服务器端都必须使用相同的互斥锁名称。
  • Linux:

    • pthread_mutexattr_init() 初始化互斥锁属性。
    • pthread_mutexattr_setpshared() 设置互斥锁为进程间共享。
    • pthread_mutex_init() 初始化互斥锁。
    • pthread_mutex_lock() 锁定互斥锁,pthread_mutex_unlock() 解锁互斥锁。

六、共享内存的优缺点

特性 优点 缺点
速度 速度快,进程间直接访问共享内存,无需数据复制。 需要进行同步和互斥,否则可能导致数据不一致。
实现难度 相对简单,操作系统提供了相应的API。 需要考虑不同操作系统之间的API差异。
数据类型 可以共享任何类型的数据,包括基本类型、结构体、类等。 需要手动管理内存的生命周期,防止内存泄漏。
同步 需要使用同步机制(如互斥锁、信号量)来保证数据的一致性和完整性。 同步机制使用不当可能导致死锁。
安全性 如果一个进程恶意修改共享内存,可能会影响其他进程的运行。 共享内存的安全性较低,容易受到攻击。

七、共享内存的应用场景

  • 高性能计算: 多个进程并行处理数据,并将结果写入共享内存。
  • 数据库系统: 多个进程共享数据库缓存,提高查询效率。
  • 图像处理: 一个进程负责图像采集,另一个进程负责图像处理。
  • 实时系统: 多个进程共享实时数据,保证数据的及时性。
  • 游戏开发: 多个进程共享游戏状态,实现多人在线游戏。

八、高级主题:Boost.Interprocess库

Boost.Interprocess 是一个C++库,提供了跨平台的进程间通信机制,包括共享内存、消息队列、互斥锁、信号量等。使用 Boost.Interprocess 可以简化共享内存的实现,并提高代码的可移植性。

下面是一个使用 Boost.Interprocess 实现共享内存的示例:

// Server process (writer)
#include <iostream>
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <cstring>

using namespace boost::interprocess;

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const char* MUTEX_NAME = "MyMutex";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    try {
        // Remove shared memory on construction and destruction
        struct remover {
            remover() { shared_memory_object::remove(SHARED_MEMORY_NAME); }
            ~remover() { shared_memory_object::remove(SHARED_MEMORY_NAME); }
        } remover;

        // Create a shared memory object.
        shared_memory_object shm(create_only, SHARED_MEMORY_NAME, read_write);

        // Set size
        shm.truncate(SHARED_MEMORY_SIZE);

        // Map the whole shared memory in this process.
        mapped_region region(shm, read_write);

        // Get the address of the mapped region.
        void* addr = region.get_address();

        // Construct the shared structure in memory
        char* data = new (addr) char[SHARED_MEMORY_SIZE];

        // Create a named mutex.
        named_mutex mutex(create_only, MUTEX_NAME);

        // Lock the mutex
        mutex.lock();

        // Write data to shared memory
        strcpy(data, "Hello from server using Boost.Interprocess!");
        std::cout << "Server: Wrote data to shared memory." << std::endl;

        // Unlock the mutex
        mutex.unlock();

        // Wait for a while to allow the client to read
        sleep(5);

    } catch (const interprocess_exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    }

    return 0;
}

// Client process (reader)
#include <iostream>
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <cstring>

using namespace boost::interprocess;

const char* SHARED_MEMORY_NAME = "MySharedMemory";
const char* MUTEX_NAME = "MyMutex";
const int SHARED_MEMORY_SIZE = 1024;

int main() {
    try {
        // Open a shared memory object.
        shared_memory_object shm(open_only, SHARED_MEMORY_NAME, read_write);

        // Map the whole shared memory in this process.
        mapped_region region(shm, read_write);

        // Get the address of the mapped region.
        void* addr = region.get_address();

        // Obtain the shared structure
        char* data = (char*)addr;

        // Open a named mutex.
        named_mutex mutex(open_only, MUTEX_NAME);

        // Lock the mutex
        mutex.lock();

        // Read data from shared memory
        std::cout << "Client: Read data from shared memory: " << data << std::endl;

        // Unlock the mutex
        mutex.unlock();

    } catch (const interprocess_exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    }

    return 0;
}

编译和运行:

  1. 确保安装了 Boost 库。
  2. 使用 C++ 编译器编译这两个文件,并链接 Boost 库。
  3. 先运行 server,再运行 client

九、总结:共享内存的实践价值

共享内存是一种高效的进程间通信机制,可以用于实现数据的快速共享。 在Windows和Linux平台上,操作系统提供了相应的API来实现共享内存。 为了保证数据的一致性和完整性,需要使用同步机制(如互斥锁、信号量)。 Boost.Interprocess 库提供了一种跨平台的共享内存实现方式,可以简化开发并提高代码的可移植性。 共享内存在高性能计算、数据库系统、图像处理、实时系统等领域有着广泛的应用。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注