C++进程间通信之共享内存

简介: C++进程间通信之共享内存

前言

C++共享内存是一种用于多进程或多线程之间进行数据交换的机制。它允许不同的进程或线程在同一块内存空间中共享数据,从而实现高效的通信和协作。共享内存在数据密集型应用程序中具有重要的作用和价值。


共享内存的主要作用之一是提高程序的性能和效率。由于共享内存是直接访问内存中的数据,而不需要复制或传输数据,因此可以避免不必要的数据拷贝和通信开销。这样,在多个进程或线程之间进行数据共享和交换时,共享内存可以大大减少时间和资源消耗,提高应用程序的运行效率。


共享内存还具有提供更方便的数据共享和同步机制的重要性。通过共享内存,不同的进程或线程可以直接读取和写入共享内存中的数据,而无需通过中间介质进行数据传输。这使得不同的进程或线程可以更容易地进行数据交换和共享,提高系统的并发性和吞吐量。此外,共享内存还可以通过使用同步机制(如信号量、互斥锁等)来确保在并发访问共享数据时的正确性和一致性。

一、共享内存是什么?

共享内存是一种特殊的内存区域,在多个进程或线程之间共享数据的机制。它允许不同的进程或线程在同一块内存空间中访问和操作共享的数据,而无需进行数据拷贝或传输。共享内存提供了一种高效的通信和协作方式,可以在多个并发执行的进程或线程之间快速交换数据。


在共享内存中,多个进程或线程可以将某个特定的内存区域映射到它们各自的地址空间中,使得它们可以直接访问这块内存中的数据,就像它们是在操作本地内存一样。这意味着一个进程或线程对共享内存中数据的修改,可以被其他进程或线程立即看到。


此外,共享内存还可以用于高性能计算、大数据处理、消息传递等领域。

二、它是如何实现共享的?

当多个进程或线程使用共享内存时,它们可以将同一块内存区域映射到各自的地址空间中。

如下图所示

        +------------------+
        |   Process 1      |
        |                  |
        |   +------------+ |
        |   | Shared     | |
        |   |  Memory    | |
        |   |            | |
        |   +------------+ |
        |                  |
        +------------------+
        +------------------+
        |   Process 2      |
        |                  |
        |   +------------+ |
        |   | Shared     | |
        |   |  Memory    | |
        |   |            | |
        |   +------------+ |
        |                  |
        +------------------++---

这里有两个进程(Process 1和Process 2),它们都将同一块内存(Shared Memory)映射到各自的地址空间中。这块内存区域可以用来存储共享的数据。


多个进程或线程可以通过访问共享内存中的数据来共享信息。例如,Process 1可以将一些数据写入共享内存,然后Process 2可以读取这些数据。由于它们都指向同一块内存区域,因此数据可以在两个进程之间共享,而无需进行数据拷贝或传输。

三、从各个层面考虑它的实现原理又是什么?

共享内存的实现原理涉及虚拟内存和物理内存的概念以及操作系统的支持。


1. 虚拟内存和物理内存:在现代操作系统中,每个进程都有自己的虚拟内存空间,它是一个抽象的地址空间。虚拟内存空间由连续的虚拟地址组成,而实际的数据存储在物理内存中。操作系统负责将虚拟地址映射到物理内存中的对应地址。


2. 共享内存的实现:共享内存是在虚拟内存中创建的一块特殊区域,它被多个进程或线程映射到各自的虚拟地址空间中。这意味着多个进程或线程可以通过它们各自的虚拟地址来访问同一块物理内存,实现数据的共享。


3. 操作系统支持:实现共享内存需要操作系统提供相应的系统调用和机制。具体实现方式可能因操作系统而异。通常,操作系统会提供一些特定的系统调用,允许进程或线程创建共享内存区域,并将其映射到虚拟内存空间中。


4. 共享内存的创建和映射:在使用共享内存之前,进程或线程需要通过系统调用创建共享内存区域,并获得一个唯一的标识符。此后,它们可以通过系统调用将这个共享内存区域映射到各自的虚拟地址空间中的某个地址。


5. 数据的读写和同步:一旦共享内存映射完成,进程或线程可以通过访问共享内存中的数据来进行读写操作。由于多个进程或线程可以同时访问共享内存,因此需要使用适当的同步机制(如信号量、互斥锁等)来确保数据的正确修改和一致性。


6. 物理内存的管理:物理内存是实际存储数据的地方。操作系统负责管理物理内存的分配和释放,以满足进程或线程的共享内存需求。当进程或线程创建共享内存时,操作系统会为其分配一块物理内存,并将其映射到所有相关的进程或线程的虚拟地址空间中。


总之,共享内存是通过将一块物理内存映射到多个进程或线程的虚拟地址空间中实现的。通过操作系统提供的系统调用和机制,进程或线程可以创建共享内存区域并映射到自己的虚拟地址空间中。这样,它们可以直接访问共享内存中的数据进行读写操作,通过适当的同步机制保证数据的正确和一致。操作系统负责管理物理内存的分配和释放,以满足共享内存的需求

1.C++代码简单示例

进程1 ( 写入数据 ):

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
    // 创建共享内存的key
    key_t key = ftok("shared_mem_example", 1234);
    // 创建共享内存区域
    int shmid = shmget(key, sizeof(int), IPC_CREAT | 0666);
    if (shmid == -1) {
        std::cerr << "Failed to create shared memory segment." << std::endl;
        return 1;
    }
    // 将共享内存映射到进程的地址空间
    int* shared_data = (int*)shmat(shmid, NULL, 0);
    if (shared_data == (int*)-1) {
        std::cerr << "Failed to attach shared memory segment." << std::endl;
        return 1;
    }
    // 写入数据到共享内存
    *shared_data = 42;
    // 断开与共享内存的连接
    if (shmdt(shared_data) == -1) {
        std::cerr << "Failed to detach shared memory segment." << std::endl;
        return 1;
    }
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        std::cerr << "Failed to delete shared memory segment." << std::endl;
        return 1;
    }
    return 0;
}

进程2 ( 读取数据 ):

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
    // 获取共享内存的key
    key_t key = ftok("shared_mem_example", 1234);
    // 获取共享内存区域
    int shmid = shmget(key, sizeof(int), 0666);
    if (shmid == -1) {
        std::cerr << "Failed to get shared memory segment." << std::endl;
        return 1;
    }
    // 将共享内存映射到进程的地址空间
    int* shared_data = (int*)shmat(shmid, NULL, 0);
    if (shared_data == (int*)-1) {
        std::cerr << "Failed to attach shared memory segment." << std::endl;
        return 1;
    }
    // 从共享内存读取数据
    int data = *shared_data;
    std::cout << "Data from shared memory: " << data << std::endl;
    // 断开与共享内存的连接
    if (shmdt(shared_data) == -1) {
        std::cerr << "Failed to detach shared memory segment." << std::endl;
        return 1;
    }
    return 0;
}

我们使用 shmget() 创建了一个共享内存区域,并获取了一个唯一的标识符。进程1将数据写入共享内存,并在最后删除了共享内存。进程2通过相同的共享内存标识符获取共享内存,并读取数据。


需要注意的是,这只是一个简单的示例,实际使用共享内存时需要合理地处理好同步和互斥的问题,以避免数据冲突和竞态条件。


三、进程的地址空间又是什么?

进程的地址空间是进程独立使用的虚拟地址空间。每个运行中的进程都有自己的地址空间,用于存储其代码、数据和栈等信息。


进程的地址空间是在每个进程的虚拟内存中分配的一块连续区域。虚拟内存是对物理内存的抽象,给每个进程提供了一个独立且私有的地址空间。这样,不同进程的地址空间之间是相互隔离的,每个进程都认为自己独占了整个地址空间。


在操作系统中,每个进程都有自己的页表,页表维护了虚拟地址到物理地址的映射关系。当进程中的指令引用虚拟地址时,操作系统通过页表将其转换为对应的物理地址,以便进行实际的读取或写入操作。

1.虚拟内存是如何转换到物理内存上的

当进程中的指令引用虚拟地址时,操作系统通过页表将其转换为对应的物理地址的过程如下:


1. 当进程执行一条指令,指令引用某个虚拟地址,例如加载数据或执行跳转等操作。


2. 在虚拟地址被访问之前,硬件将这个虚拟地址发送给操作系统中的内存管理单元(MMU)。


3. MMU检查指令引用的虚拟地址,并查找进程的页表。


4. 页表是进程独有的数据结构,包含虚拟地址与物理地址之间的映射关系。页表的具体实现方式可以是多级页表,例如二级或三级页表。


5. MMU使用页表中的映射信息,将虚拟地址转换为对应的物理地址。


6. 如果页表中存在有效的映射关系,MMU将转换后的物理地址返回给CPU。


7. CPU可以通过这个物理地址访问实际的物理内存,并执行读取或写入操作。


8. 如果页表中不存在有效的映射关系,或者指令引用的虚拟地址超出了进程的合法地址范围,会发生页错误(page fault),由操作系统处理。


9. 在页错误发生时,操作系统会根据异常处理机制介入,通常会触发页面置换或从硬盘中加载相应的数据。之后,页面将被映射到进程的虚拟地址空间,使进程可以继续执行。


总之,通过页表的映射,操作系统能够将进程的虚拟地址转换为对应的物理地址,从而让CPU能够正确访问实际的物理地址。

2.什么是页错误?

1. 当发生页错误时,CPU会暂停进程的执行,并触发一个异常(page fault exception)。


2. 操作系统的页错误处理程序介入执行,操作系统会接管处理这个异常。


3. 操作系统首先确定引发页错误的虚拟地址,以及哪个页表条目对应于该虚拟地址。


4. 操作系统检查页表条目中的状态位,如有效位、访问权限位等,来确定页错误的原因。可能的原因包括:无效的页表条目、访问权限不足、页表条目未在物理内存中等。


5. 根据页错误的原因,操作系统执行相应的处理操作:


  - 如果无效的页表条目是由于该虚拟页尚未分配而导致的,操作系统会为该虚拟页分配内存,并将相应的物理页映射到该虚拟地址。


  - 如果访问权限不足(例如,进程试图写入只读内存),操作系统会引发访问权限异常,并可能终止进程或进行其他相应操作。


  - 如果页表条目未在物理内存中(即,发生了缺页错误),操作系统会选择一个合适的物理页(可能从空闲页池中获取或通过页面置换算法选择待替换的页面),将数据从外部存储器(如硬盘)加载到该物理页中,并更新页表条目的映射。


6. 操作系统处理完页错误后,将控制返回给触发页错误的指令,并重新执行该指令。这时,指令引用的虚拟地址已经被正确映射到了物理地址,CPU可以继续执行读取或写入操作。


总的来说,当发生页错误时,操作系统会分析页错误的原因,并采取相应的行动来解决问题,例如分配新的物理页、调整页表映射或进行页面置换等。通过这些操作,操作系统确保进程可以继续执行,并能够正确地访问所需的数据。

四、生产者和消费者

1.生产者进程

#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
const int BUFFER_SIZE = 10;
const int KEY = 1234;
struct SharedMemory {
    int buffer[BUFFER_SIZE];
    int buffer_index;
    bool is_buffer_full;
};
void P(int semaphore_id, int index) {
    struct sembuf semaphore;
    semaphore.sem_num = index;
    semaphore.sem_op = -1;
    semaphore.sem_flg = 0;
    semop(semaphore_id, &semaphore, 1);
}
void V(int semaphore_id, int index) {
    struct sembuf semaphore;
    semaphore.sem_num = index;
    semaphore.sem_op = 1;
    semaphore.sem_flg = 0;
    semop(semaphore_id, &semaphore, 1);
}
int main() {
    // 创建共享内存
    int shmid = shmget(KEY, sizeof(SharedMemory), IPC_CREAT | 0666);
    if (shmid == -1) {
        std::cerr << "Shared memory creation error." << std::endl;
        return 1;
    }
    // 连接共享内存
    SharedMemory* shared_memory = (SharedMemory*)shmat(shmid, NULL, 0);
    if (shared_memory == (void*)-1) {
        std::cerr << "Shared memory attachment error." << std::endl;
        return 1;
    }
    // 创建互斥锁
    int mutex_id = semget(KEY, 1, IPC_CREAT | 0666);
    if (mutex_id == -1) {
        std::cerr << "Mutex creation error." << std::endl;
        return 1;
    }
    // 创建信号量
    int semaphore_id = semget(KEY + 1, 2, IPC_CREAT | 0666);
    if (semaphore_id == -1) {
        std::cerr << "Semaphore creation error." << std::endl;
        return 1;
    }
    // 初始化共享内存和信号量
    std::memset(shared_memory, 0, sizeof(SharedMemory));
    semctl(mutex_id, 0, SETVAL, 1);
    semctl(semaphore_id, 0, SETVAL, 0);
    semctl(semaphore_id, 1, SETVAL, BUFFER_SIZE);
    // 生产者进程
    for (int i = 1; i <= 20; ++i) {
        P(semaphore_id, 1);  // 等待缓冲区有空位可写
        P(mutex_id, 0);  // 加锁
        shared_memory->buffer[shared_memory->buffer_index] = i;
        ++shared_memory->buffer_index;
        std::cout << "Produced: " << i << std::endl;
        V(mutex_id, 0);  // 解锁
        V(semaphore_id, 0);  // 通知消费者可以消费
    }
    // 断开共享内存连接
    shmdt(shared_memory);
    return 0;
}

2.消费者进程

#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
const int BUFFER_SIZE = 10;
const int KEY = 1234;
struct SharedMemory {
    int buffer[BUFFER_SIZE];
    int buffer_index;
    bool is_buffer_full;
};
void P(int semaphore_id, int index) {
    struct sembuf semaphore;
    semaphore.sem_num = index;
    semaphore.sem_op = -1;
    semaphore.sem_flg = 0;
    semop(semaphore_id, &semaphore, 1);
}
void V(int semaphore_id, int index) {
    struct sembuf semaphore;
    semaphore.sem_num = index;
    semaphore.sem_op = 1;
    semaphore.sem_flg = 0;
    semop(semaphore_id, &semaphore, 1);
}
int main() {
    // 连接共享内存
    int shmid = shmget(KEY, sizeof(SharedMemory), 0666);
    if (shmid == -1) {
        std::cerr << "Shared memory attachment error." << std::endl;
        return 1;
    }
    // 连接互斥锁
    int mutex_id = semget(KEY, 1, 0666);
    if (mutex_id == -1) {
        std::cerr << "Mutex attachment error." << std::endl;
        return 1;
    }
    // 连接信号量
    int semaphore_id = semget(KEY + 1, 2, 0666);
    if (semaphore_id == -1) {
        std::cerr << "Semaphore attachment error." << std::endl;
        return 1;
    }
    // 连接共享内存
    SharedMemory* shared_memory = (SharedMemory*)shmat(shmid, NULL, 0);
    if (shared_memory == (void*)-1) {
        std::cerr << "Shared memory attachment error." << std::endl;
        return 1;
    }
    // 消费者进程
    for (int i = 1; i <= 20; ++i) {
        P(semaphore_id, 0);  // 等待缓冲区有数据可读
        P(mutex_id, 0);  // 加锁
        int data = shared_memory->buffer[shared_memory->buffer_index - 1];
        --shared_memory->buffer_index;
        std::cout << "Consumed: " << data << std::endl;
        V(mutex_id, 0);  // 解锁
        V(semaphore_id, 1);  // 通知生产者可以生产
    }
    // 断开共享内存连接
    shmdt(shared_memory);
    return 0;
}

总结

共享内存这种方式可以提高并发性和效率,特别适用于需要高效地在多个进程之间传递大量数据的情况,如图像处理、音视频处理等。也可以提高程序的效率和响应性,特别适用于需要并发访问共享数据的情况,如并发数据结构、线程池等。特别是在高性能计算领域,共享内存可以用于实现多个计算节点之间的数据共享和协同计算。这种方式可以提高计算效率和吞吐量,特别适用于需要并行计算和数据共享的科学计算、仿真模拟等应用。

目录
相关文章
|
19天前
|
存储 Java C++
C++ 引用和指针:内存地址、创建方法及应用解析
C++中的引用是现有变量的别名,创建时需用`&`运算符,如`string &meal = food;`。指针存储变量的内存地址,使用`*`创建,如`string* ptr = &food;`。引用必须初始化且不可为空,而指针可初始化为空。引用在函数参数传递和提高效率时有用,指针适用于动态内存分配和复杂数据结构操作。选择使用取决于具体需求。
38 9
|
24天前
|
存储 Linux C语言
【C++初阶】6. C&C++内存管理
【C++初阶】6. C&C++内存管理
34 2
|
7天前
|
存储 缓存 算法
C++从入门到精通:4.6性能优化——深入理解算法与内存优化
C++从入门到精通:4.6性能优化——深入理解算法与内存优化
|
7天前
|
存储 程序员 编译器
C++从入门到精通:3.4深入理解内存管理机制
C++从入门到精通:3.4深入理解内存管理机制
|
7天前
|
消息中间件 Linux
【linux进程间通信(二)】共享内存详解以及进程互斥概念
【linux进程间通信(二)】共享内存详解以及进程互斥概念
|
8天前
|
存储 人工智能 程序员
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
37 1
|
8天前
|
C语言 C++
【C++基础(九)】C++内存管理--new一个对象出来
【C++基础(九)】C++内存管理--new一个对象出来
|
9天前
|
存储 编译器 Linux
c++的学习之路:8、内存管理与模板
c++的学习之路:8、内存管理与模板
9 0
|
14天前
|
存储 Linux C语言
C/C++之内存旋律:星辰大海的指挥家
C/C++之内存旋律:星辰大海的指挥家
23 0
|
18天前
|
编译器 C++
C++ 解引用与函数基础:内存地址、调用方法及声明
C++ 中的解引用允许通过指针访问变量值。使用 `*` 运算符可解引用指针并修改原始变量。注意确保指针有效且不为空,以防止程序崩溃。函数是封装代码的单元,用于执行特定任务。理解函数的声明、定义、参数和返回值是关键。函数重载允许同一名称但不同参数列表的函数存在。关注公众号 `Let us Coding` 获取更多内容。
136 1