C++ `mmap` 与共享内存:进程间高性能数据交换

好的,各位观众老爷们,欢迎来到今天的“C++ mmap与共享内存:进程间高性能数据交换”专场。我是你们的老朋友,BUG终结者,代码界的段子手。今天咱们不讲虚的,直接上干货,聊聊在C++的世界里,如何用mmap和共享内存这两个大杀器,实现进程间的数据高速公路。

第一部分:进程通信那些事儿,为什么需要共享内存?

首先,咱们得明白一个道理:进程之间默认是“老死不相往来”的。每个进程都有自己独立的内存空间,就像一座座孤岛,彼此之间无法直接访问。如果想让两个进程交流,就得想办法打破这种隔离。这就是进程间通信(Inter-Process Communication,IPC)的意义所在。

传统的IPC方式有很多,比如:

  • 管道 (Pipe): 像水管一样,只能单向流动,效率较低。
  • 命名管道 (Named Pipe): 比管道好一点,但还是单向的,而且涉及到文件系统操作。
  • 消息队列 (Message Queue): 像邮局一样,可以双向通信,但需要进行数据拷贝,效率不高。
  • 套接字 (Socket): 像电话一样,可以跨网络通信,但开销比较大。

这些方法各有优缺点,但在高性能数据交换的场景下,它们都有一个共同的缺点:需要数据拷贝

想象一下,你要把一个GB级别的大文件从一个进程传到另一个进程,如果每次都拷贝一份,那得多慢啊!这就像你要把一车货物从一个仓库搬到另一个仓库,难道要把所有货物都重新装卸一遍吗?

这时候,共享内存就闪亮登场了!它就像在两个仓库之间架起了一座桥梁,货物可以直接在两个仓库之间流通,而无需重新装卸。

共享内存的原理:

共享内存允许多个进程访问同一块物理内存。这意味着,一个进程对共享内存的修改,可以立即被其他进程看到。这样就避免了数据拷贝,大大提高了进程间通信的效率。

第二部分:mmap:内存映射的魔法

mmap (Memory Map) 是一个系统调用,它可以将一个文件或者其他对象映射到进程的地址空间中。简单来说,就是把文件或者其他对象当成内存来操作。

mmap和共享内存的关系:

mmap 可以用来创建共享内存区域。通过mmap,我们可以将一个文件或者匿名内存区域映射到多个进程的地址空间中,从而实现共享内存。

mmap的用法:

mmap函数的原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

  • addr: 映射区的首地址。通常设置为NULL,让系统自动选择。
  • length: 映射区的长度,必须是页大小的整数倍。
  • prot: 映射区的保护方式,指定映射区的访问权限。
    • PROT_READ: 可读
    • PROT_WRITE: 可写
    • PROT_EXEC: 可执行
    • PROT_NONE: 不可访问
  • flags: 映射区的标志,影响映射区的行为。
    • MAP_SHARED: 共享映射,对映射区的修改会反映到文件中,并且可以被其他进程看到。
    • MAP_PRIVATE: 私有映射,对映射区的修改不会反映到文件中,并且其他进程看不到。
    • MAP_ANONYMOUS: 匿名映射,用于创建共享内存,不需要文件支持。
  • fd: 文件描述符,如果是匿名映射,则设置为-1
  • offset: 映射区的偏移量,必须是页大小的整数倍。

返回值:

  • 成功:返回映射区的首地址。
  • 失败:返回MAP_FAILED

munmap:解除映射

使用完mmap之后,需要使用munmap来解除映射,释放资源。

int munmap(void *addr, size_t length);

参数说明:

  • addr: 映射区的首地址。
  • length: 映射区的长度。

返回值:

  • 成功:返回0
  • 失败:返回-1

第三部分:实战演练:用mmap创建共享内存

光说不练假把式,现在咱们来写一个简单的例子,演示如何使用mmap创建共享内存,实现进程间的数据交换。

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>

const char* SHARED_MEMORY_NAME = "/my_shared_memory";
const int SHARED_MEMORY_SIZE = 4096; // 4KB

int main() {
    // 1. 创建或打开共享内存对象
    int fd = shm_open(SHARED_MEMORY_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        return 1;
    }

    // 2. 设置共享内存对象的大小
    if (ftruncate(fd, SHARED_MEMORY_SIZE) == -1) {
        perror("ftruncate failed");
        close(fd);
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    // 3. 映射共享内存对象到进程的地址空间
    void* shared_memory = mmap(NULL, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_memory == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    // 4. 关闭文件描述符
    close(fd);

    // 5. 判断是生产者还是消费者
    std::cout << "Are you producer (p) or consumer (c)? ";
    char role;
    std::cin >> role;

    if (role == 'p') {
        // 生产者
        std::cout << "Enter the message to share: ";
        std::string message;
        std::cin.ignore(); // Consume the newline character
        std::getline(std::cin, message);

        // 将消息写入共享内存
        std::strcpy((char*)shared_memory, message.c_str());
        std::cout << "Message sent: " << message << std::endl;
    } else if (role == 'c') {
        // 消费者
        // 等待一段时间,确保生产者已经写入数据
        sleep(2);

        // 从共享内存读取消息
        std::cout << "Message received: " << (char*)shared_memory << std::endl;
    } else {
        std::cout << "Invalid role." << std::endl;
    }

    // 6. 解除映射
    if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1) {
        perror("munmap failed");
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    // 7. 删除共享内存对象 (通常只在最后一个使用共享内存的进程中执行)
    //shm_unlink(SHARED_MEMORY_NAME); //取消注释来删除共享内存

    return 0;
}

代码解释:

  1. shm_open() 创建或打开一个共享内存对象。 SHARED_MEMORY_NAME是共享内存对象的名字,就像文件的路径一样。O_CREAT | O_RDWR表示如果共享内存对象不存在,则创建它,并以读写方式打开。0666是权限设置,表示所有用户都可以读写。
  2. ftruncate() 设置共享内存对象的大小。必须设置大小,否则mmap会失败。
  3. mmap() 将共享内存对象映射到进程的地址空间。MAP_SHARED表示共享映射,对共享内存的修改会被其他进程看到。
  4. close() 关闭文件描述符。映射完成后,文件描述符就可以关闭了。
  5. 生产者/消费者逻辑: 程序会询问你是生产者还是消费者。生产者会将你输入的消息写入共享内存,消费者会从共享内存读取消息。
  6. munmap() 解除映射,释放资源。
  7. shm_unlink() 删除共享内存对象。注意: 这个操作通常只在最后一个使用共享内存的进程中执行。如果所有进程都使用完共享内存后,没有删除它,那么共享内存对象会一直存在,直到系统重启。

编译和运行:

g++ shared_memory_example.cpp -o shared_memory_example -lrt

运行的时候,先运行一个进程选择p (生产者),输入要发送的消息,然后再运行另一个进程选择c (消费者),就可以看到消费者收到了生产者发送的消息。

注意事项:

  • 权限问题: 确保你的程序有足够的权限来创建和访问共享内存对象。
  • 同步问题: 由于共享内存允许多个进程同时访问,因此需要注意同步问题。可以使用互斥锁、信号量等同步机制来保护共享内存的并发访问。
  • 大小限制: 共享内存的大小受到系统限制。可以使用sysconf(_SC_PAGE_SIZE)来获取系统的页大小,然后计算出最大可以使用的共享内存大小。
  • 错误处理: 在实际开发中,需要对shm_open, ftruncate, mmap, munmap等函数的返回值进行检查,并进行适当的错误处理。

第四部分:共享内存的同步机制:锁与信号量

共享内存虽然高效,但也带来了一个新的问题:并发访问。多个进程同时读写共享内存,可能会导致数据竞争,最终结果不可预测。为了解决这个问题,我们需要使用同步机制来保证共享内存的正确访问。

常用的同步机制有:

  • 互斥锁 (Mutex): 互斥锁就像一把锁,同一时刻只允许一个进程访问共享资源。
  • 信号量 (Semaphore): 信号量允许多个进程同时访问共享资源,但限制了访问的数量。

使用互斥锁保护共享内存:

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h> // for pthread_mutex_*

const char* SHARED_MEMORY_NAME = "/my_shared_memory_mutex";
const int SHARED_MEMORY_SIZE = 4096; // 4KB

struct SharedData {
    pthread_mutex_t mutex;
    char message[SHARED_MEMORY_SIZE - sizeof(pthread_mutex_t)]; // leave space for mutex
};

int main() {
    int fd = shm_open(SHARED_MEMORY_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        return 1;
    }

    if (ftruncate(fd, SHARED_MEMORY_SIZE) == -1) {
        perror("ftruncate failed");
        close(fd);
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    SharedData* shared_data = (SharedData*)mmap(NULL, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_data == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    close(fd);

    // Initialize the mutex.  Important to do this *before* any other process
    // attaches to the shared memory, or you'll have a race condition on
    // mutex initialization.
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); // allow mutex to be shared between processes
    pthread_mutex_init(&shared_data->mutex, &attr);
    pthread_mutexattr_destroy(&attr);

    std::cout << "Are you producer (p) or consumer (c)? ";
    char role;
    std::cin >> role;

    if (role == 'p') {
        std::cout << "Enter the message to share: ";
        std::string message;
        std::cin.ignore();
        std::getline(std::cin, message);

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

        // Write the message to shared memory
        std::strcpy(shared_data->message, message.c_str());
        std::cout << "Message sent: " << message << std::endl;

        // Unlock the mutex
        pthread_mutex_unlock(&shared_data->mutex);
    } else if (role == 'c') {
        sleep(2);

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

        // Read the message from shared memory
        std::cout << "Message received: " << shared_data->message << std::endl;

        // Unlock the mutex
        pthread_mutex_unlock(&shared_data->mutex);
    } else {
        std::cout << "Invalid role." << std::endl;
    }

    pthread_mutex_destroy(&shared_data->mutex); // Destroy the mutex
    if (munmap(shared_data, SHARED_MEMORY_SIZE) == -1) {
        perror("munmap failed");
        shm_unlink(SHARED_MEMORY_NAME);
        return 1;
    }

    // shm_unlink(SHARED_MEMORY_NAME); // Uncomment to delete shared memory

    return 0;
}

代码解释:

  1. SharedData结构体: 定义了一个结构体,包含了互斥锁mutex和用于存储消息的message。 互斥锁必须放在共享内存中,这样才能被多个进程共享。
  2. pthread_mutex_init() 初始化互斥锁。 pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) 非常重要,它告诉系统这个互斥锁是进程间共享的。必须先初始化互斥锁,再让其他进程访问共享内存,否则会发生竞争条件。
  3. pthread_mutex_lock() 加锁。在访问共享内存之前,需要先加锁,防止其他进程同时访问。
  4. pthread_mutex_unlock() 解锁。访问完共享内存之后,需要解锁,让其他进程可以访问。
  5. pthread_mutex_destroy() 销毁互斥锁。在程序结束时,需要销毁互斥锁,释放资源。

编译和运行:

g++ shared_memory_mutex_example.cpp -o shared_memory_mutex_example -lrt -lpthread

第五部分:mmap 与文件:持久化数据

mmap 的一个强大之处在于它可以将文件映射到内存。这意味着你可以像操作内存一样操作文件,而无需进行显式的读写操作。这对于处理大型文件非常有用,可以提高效率。

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>

const char* FILE_NAME = "my_file.txt";
const int FILE_SIZE = 4096; // 4KB

int main() {
    // 1. 打开文件
    int fd = open(FILE_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // 2. 设置文件大小
    if (ftruncate(fd, FILE_SIZE) == -1) {
        perror("ftruncate failed");
        close(fd);
        return 1;
    }

    // 3. 映射文件到内存
    char* mapped_data = (char*)mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped_data == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return 1;
    }

    // 4. 关闭文件描述符
    close(fd);

    // 5. 操作映射的内存
    std::strcpy(mapped_data, "Hello, mmap!");
    std::cout << "Data written to file: " << mapped_data << std::endl;

    // 6. 解除映射
    if (munmap(mapped_data, FILE_SIZE) == -1) {
        perror("munmap failed");
        return 1;
    }

    return 0;
}

代码解释:

  1. open() 打开文件。O_CREAT | O_RDWR表示如果文件不存在,则创建它,并以读写方式打开。
  2. ftruncate() 设置文件大小。
  3. mmap() 将文件映射到内存。MAP_SHARED表示共享映射,对映射区的修改会反映到文件中。
  4. close() 关闭文件描述符。
  5. 操作映射的内存: 可以直接对mapped_data进行读写操作,就像操作内存一样。
  6. munmap() 解除映射。

运行之后,你会发现my_file.txt文件中已经写入了 "Hello, mmap!"。

第六部分:mmap 的优势与劣势

优势:

  • 高性能: 避免了数据拷贝,提高了进程间通信的效率。
  • 方便: 可以像操作内存一样操作文件,简化了文件读写操作。
  • 持久化: 可以将数据持久化到文件中。

劣势:

  • 同步问题: 需要使用同步机制来保证并发访问的正确性。
  • 错误处理: 需要对mmap相关函数的返回值进行检查,并进行适当的错误处理。
  • 地址空间限制: 受限于进程的地址空间大小。

总结:

mmap 和共享内存是 C++ 中进行进程间高性能数据交换的利器。 掌握它们,可以让你在多进程编程中如虎添翼。 但是,也要注意同步问题和错误处理,才能保证程序的稳定性和可靠性。

好了,今天的讲座就到这里。希望大家有所收获。 记住,代码的世界是充满乐趣的,只要你肯学习,肯实践,就能成为一名真正的编程高手!下次再见!

发表回复

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