在Linux的运作环境当中,进程之间的通信是一件极为重要的事情。共享内存是一种较为高效的IPC(进程间通信)方式。它可以使得不同的进程直接去对同一块物理内存区域进行访问,如此便极大地提高了数据传输的效率。举个例子来讲,共享内存就好似一块大家都能够使用的公共黑板,进程A在上面写上了内容,进程B立刻就能够看见,中间不需要很多繁琐的的数据拷贝的步骤。
在实际的项目运作过程之中,共享内存经常被应用于那种需要频繁进行数据交换的情形当中,比如多媒体处理、数据库缓存这一类的情况。当遭遇到复杂的数据结构以及算法的时候,我们常常会用到STL容器。STL容器具备着丰富的数据结构,比如说vector、map、list等等,使用起来是比较方便的,能够极大地提高开发的效率。那么将共享内存和STL容器结合起来加以运用,似乎是一个很不错的选择,既可以利用共享内存的高效性,又能够借助STL容器的便利性 。
共享内存 STL 容器崩溃场景
面试题写作模版我在实际项目中就遇到了这样的问题。当时,我们的项目是一个多媒体处理系统,需要在多个进程之间共享一些复杂的数据结构,这些数据结构用 STL 中的 map 来存储再合适不过。于是,我信心满满地将 map 放到了共享内存中,代码大概长这样:
#include <iostream>#include <map>#include <sys/shm.h>#include <sys/stat.h>#include <fcntl.h>#include <cstring>// 定义共享内存结构struct SharedData { std::map<int, int> dataMap;};int main() { // 创建共享内存对象 int shm_fd = shm_open("/shared_memory", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); if (shm_fd == -1) { perror("shm_open"); return 1; } // 配置共享内存大小 if (ftruncate(shm_fd, sizeof(SharedData)) == -1) { perror("ftruncate"); return 1; } // 映射共享内存到进程地址空间 SharedData* shared_data = (SharedData*)mmap(0, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (shared_data == MAP_FAILED) { perror("mmap"); return 1; } // 向共享内存中的map插入数据 for (int i = 0; i < 10; ++i) { shared_data->dataMap[i] = i * 2; } // 打印共享内存中的map数据 for (const auto& pair : shared_data->dataMap) { std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl; } // 取消映射并关闭共享内存 if (munmap(shared_data, sizeof(SharedData)) == -1) { perror("munmap"); return 1; } if (close(shm_fd) == -1) { perror("close"); return 1; } return 0;}然而,运行这段代码时,程序直接崩溃了,报错信息是 “Segmentation fault (core dumped)”。这就很奇怪了,在普通的内存空间中,这样使用 map 一点问题都没有,为什么放到共享内存里就不行了呢?我又尝试在不同的进程中访问这个共享内存中的 map,结果还是崩溃,有时候还会出现一些奇怪的内存错误,比如 “double free or corruption”。这就像走进了一个迷宫,完全摸不着头脑,到底是哪里出了问题呢?
共享内存崩溃的根源
面试题写作模版std::string和std::vector由于其动态内存分配的特性,无法保证内存空间的连续性。std::vector为例,当我们向std::vector中添加元素时,如果当前分配的内存空间已满,它会重新分配一块更大的内存,然后将原内存中的元素复制或移动到新内存中 。这就导致std::vector内部的元素在内存中的存储位置可能会发生变化,无法始终保持在连续的物理内存上。假设我们创建了一个std::vector<int>,并向其中添加了一些元素:#include <vector>int main() { std::vector<int> vec; for (int i = 0; i < 10; ++i) { vec.push_back(i); } return 0;}要是vec的初始容量比较小的话,那么伴随元素不断地进行添加,它就需要多次重新去进行分配内存。每一次所分配的内存地址有可能是不连续的。这和共享内存所要求的连续物理内存存储是不一样的情况。要是直接把这样的std::vector放置到共享内存里的话,不同的进程在进行访问的时候,就极有可能因为内存不连续而没有办法正确地去读取数据 。
(2)跨进程访问问题——在操作系统的范畴之中,每一个进程都具备其自身单独的虚拟地址空间。这是因为要去确保进程和进程之间能够相互进行隔离并且处于安全的状态。不同进程的虚拟地址到物理地址的映射是不相同的。也就是说同一个虚拟地址在不同进程当中有可能对应着不同的物理内存位置。
std::string 以及 std::vector 这里面都存在着用指针去指向动态分配的内存区域这种情况。举例来讲 std::string 里面有一个指针是指向用来存放字符串内容的字符数组的,std::vector 有一个指针是指向用来存放元素的内存块的。当这些对象被放置在共享内存里面的时候,不同的进程去进行访问的时候,由于进程虚拟地址空间是相互隔离的,所以这些指针在别的进程里面所呈现出来的值是没有用处的。
因为它们所指向的是原来进程内部的虚拟地址,在别的进程里面这个虚拟地址所对应的物理内存并不是所想要的数据。以下是一个简单的代码示例,展示了跨进程访问包含 std::string 的结构体时可能出现的问题:
#include <iostream>#include <string>#include <sys/shm.h>#include <sys/ipc.h>#include <unistd.h>struct SharedData { std::string str;};int main() { key_t key = ftok(".", 'a'); int shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget"); return 1; } SharedData *sharedData = (SharedData*)shmat(shmid, nullptr, 0); if (sharedData == (SharedData*)-1) { perror("shmat"); return 1; } if (fork() == 0) { // 子进程 sharedData->str = "Hello from child"; std::cout << "Child process set string: " << sharedData->str << std::endl; } else { // 父进程 sleep(1); // 等待子进程设置数据 std::cout << "Parent process read string: " << sharedData->str << std::endl; // 这里可能会出错 } if (shmdt(sharedData) == -1) { perror("shmdt"); return 1; } if (shmctl(shmid, IPC_RMID, nullptr) == -1) { perror("shmctl"); return 1; } return 0;}在该示例当中,父子进程运用共享内存来传递包含 std::string 的结构体。子进程对子进程中的 std::string设置了值。但是当父进程进行读取的时候,由于 std::string 内部的指针在不同的进程里不再起作用了,极有可能没办法正确地读取到子进程所设置的字符串内容,这样就会致使程序出现问题。
(3)生命周期管理问题——共享内存的生命历程是由操作系统来进行管理的。它和运用它的进程的生命历程是相互独立的。一旦共享内存被创建起来,只要没有被经过shmctl函数传入IPC_RMID命令去进行显式的删除操作,它就会始终在系统之中存在着。
std::string以及std::vector对象的生命周期和其所从属的进程是存在关联的。当一个进程宣告结束的时候,进程当中的std::string以及std::vector对象就会被予以销毁。在这个时候析构函数就会自动地进行执行,将它们内部动态申请的堆内存给释放掉。
共享内存所存储的乃是容器对象自身。它并不对容器内部动态分配的数据内存进行托管。倘若创建或者写入容器的进程出现退出的情况,那么内部的数据内存便会被系统予以回收。在这个时候共享内存里遗留下来的容器指针就变成了野指针。其他进程对它进行访问就会出现问题,例如会触发段错误、程序出现崩溃、数据出现错乱这类未定义的行为。下面提供可直接编译运行的代码示例,复现该生命周期异常问题:
#include <iostream>#include <vector>#include <sys/shm.h>#include <sys/ipc.h>#include <unistd.h>#include <cstdlib>// 错误用法:共享内存中直接存放std::vectorstruct SharedData { std::vector<int> data;};int main() { key_t key = ftok(".", 'b'); int shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget failed"); return 1; } SharedData* shm_data = (SharedData*)shmat(shmid, nullptr, 0); if (shm_data == (SharedData*)-1) { perror("shmat failed"); return 1; } pid_t pid = fork(); if (pid == 0) { // 子进程:向共享内存的vector写入数据 shm_data->data.push_back(100); shm_data->data.push_back(200); std::cout << "子进程写入数据完成" << std::endl; // 子进程直接退出,触发vector析构,释放内部数据内存 exit(0); } else { // 父进程等待子进程退出 sleep(1); // 子进程已退出,vector内部内存已被释放,此处访问野指针 std::cout << "父进程尝试读取数据" << std::endl; // 大概率触发段错误、程序崩溃 std::cout << "读取值:" << shm_data->data[0] << std::endl; } shmdt(shm_data); shmctl(shmid, IPC_RMID, nullptr); return 0;}子进程在成功向共享内存当中的vector里写入数据之后就直接退出掉了。当进程进行销毁的时候会执行vector的析构函数,去释放内部所存储元素的动态内存。父进程之后去访问共享内存里的vector,实际访问的是已经被回收掉的内存空间,必然会出现数据异常、段错误、程序崩溃这类的问题。这就是共享内存绝对不可以直接使用STL动态容器的核心缘由当中的一个。
共享内存合法数据结构
面试题写作模版(1)使用固定大小数组——由于std::string以及std::vector由于动态内存分配的缘故没有办法直接放置在共享内存当中,那我们便采用固定大小的数组来对它们进行替代。固定大小的数组在内存里占据着连续的空间,它的大小在编译的时候就已经被确定好了,不会发生动态的变化。如此一来便确保了内存的连续性,特别适宜运用在共享内存里面。
例如,当我们需要在共享内存中存储字符串时,可以使用固定大小的字符数组。假设我们要存储一个不超过 100 个字符的字符串,可以这样定义结构体:
struct SharedString { char str[100];};在使用时,我们就可以将 SharedString 结构体存放在共享内存中,不同进程通过共享内存访问这个结构体时,由于字符数组的内存是连续的,并且其地址在不同进程中映射到共享内存的同一物理位置,所以能够正确地读写字符串内容。比如,一个进程向str数组中写入字符串:
#include <sys/shm.h>#include <sys/ipc.h>#include <string.h>#include <unistd.h>#include <iostream>int main() { key_t key = ftok(".", 'a'); int shmid = shmget(key, sizeof(SharedString), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget"); return 1; } SharedString *sharedStr = (SharedString*)shmat(shmid, nullptr, 0); if (sharedStr == (SharedString*)-1) { perror("shmat"); return 1; } strcpy(sharedStr->str, "Hello from shared memory"); std::cout << "Process wrote: " << sharedStr->str << std::endl; if (shmdt(sharedStr) == -1) { perror("shmdt"); return 1; } return 0;}另一个进程读取这个字符串:
#include <sys/shm.h>#include <sys/ipc.h>#include <iostream>int main() { key_t key = ftok(".", 'a'); int shmid = shmget(key, sizeof(SharedString), 0); if (shmid == -1) { perror("shmget"); return 1; } SharedString *sharedStr = (SharedString*)shmat(shmid, nullptr, 0); if (sharedStr == (SharedString*)-1) { perror("shmat"); return 1; } std::cout << "Process read: " << sharedStr->str << std::endl; if (shmdt(sharedStr) == -1) { perror("shmdt"); return 1; } return 0;}同样,当需要存储一组数据时,也可以使用固定大小数组。比如存储 10 个整数:
struct SharedData { int numbers[10];};这样,不同进程对共享内存中 SharedData 结构体的访问就能保证数据的一致性和正确性 。
(2)手动内存管理——为了避免std::string以及std::vector出现动态内存分配这样的问题,我们可以采用手动内存管理的途径。在共享内存之中,手动地去进行分配以及管理数据的内存,确保内存的连续性以及跨进程访问的正确性。
具体实现时,我们可以在共享内存中预先分配一块足够大的内存空间,然后在这个空间内手动管理数据的存储和释放。例如,我们可以使用malloc和free函数来管理内存(在共享内存环境下,需要注意内存的释放时机和跨进程一致性)。假设我们要在共享内存中存储一些自定义的数据结构,首先在共享内存中分配内存:
#include <sys/shm.h>#include <sys/ipc.h>#include <cstdlib>#include <cstring>#include <unistd.h>#include <iostream>struct CustomData { int id; // 假设这里还有其他成员};int main() { key_t key = ftok(".", 'a'); int shmid = shmget(key, sizeof(CustomData) * 10 + sizeof(size_t), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget"); return 1; } char *sharedMem = (char*)shmat(shmid, nullptr, 0); if (sharedMem == (char*)-1) { perror("shmat"); return 1; } size_t *dataCountPtr = (size_t*)sharedMem; *dataCountPtr = 0; CustomData *dataStart = (CustomData*)(sharedMem + sizeof(size_t)); // 模拟添加数据 CustomData newData = {1}; size_t currentCount = *dataCountPtr; CustomData *targetData = dataStart + currentCount; memcpy(targetData, &newData, sizeof(CustomData)); (*dataCountPtr)++; std::cout << "Process added data with id: " << targetData->id << std::endl; if (shmdt(sharedMem) == -1) { perror("shmdt"); return 1; } return 0;}在读取数据的进程中:
#include <sys/shm.h>#include <sys/ipc.h>#include <iostream>int main() { key_t key = ftok(".", 'a'); int shmid = shmget(key, sizeof(CustomData) * 10 + sizeof(size_t), 0); if (shmid == -1) { perror("shmget"); return 1; } char *sharedMem = (char*)shmat(shmid, nullptr, 0); if (sharedMem == (char*)-1) { perror("shmat"); return 1; } size_t *dataCountPtr = (size_t*)sharedMem; size_t count = *dataCountPtr; CustomData *dataStart = (CustomData*)(sharedMem + sizeof(size_t)); for (size_t i = 0; i < count; ++i) { CustomData *data = dataStart + i; std::cout << "Process read data with id: " << data->id << std::endl; } if (shmdt(sharedMem) == -1) { perror("shmdt"); return 1; } return 0;}在这个过程中,我们需要自己维护数据的结构和内存的使用情况,确保不同进程之间的数据一致性 。同时,要注意内存的释放操作,避免内存泄漏。比如,当所有进程都不再使用共享内存中的数据时,需要正确地释放内存。
#include <boost/interprocess/shared_memory_object.hpp>#include <boost/interprocess/mapped_region.hpp>#include <boost/interprocess/containers/string.hpp>#include <boost/interprocess/allocators/allocator.hpp>#include <iostream>#include <cstring>namespace bip = boost::interprocess;typedef bip::allocator<char, bip::managed_shared_memory::segment_manager> CharAllocator;typedef bip::basic_string<char, std::char_traits<char>, CharAllocator> SharedString;int main() { try { // 创建共享内存对象 bip::shared_memory_object shm(bip::open_or_create, "MySharedMemory", bip::read_write); shm.truncate(1024); // 映射共享内存区域 bip::mapped_region region(shm, bip::read_write); void *addr = region.get_address(); // 创建共享内存管理对象 bip::managed_shared_memory segment(bip::attach_only, "MySharedMemory"); // 从共享内存中获取字符串对象 SharedString *sharedStr = segment.find<SharedString>("MyString").first; if (!sharedStr) { // 如果不存在,创建一个新的字符串对象 CharAllocator alloc_inst(segment.get_segment_manager()); sharedStr = segment.construct<SharedString>("MyString")("Initial value", alloc_inst); } // 修改字符串内容 sharedStr->assign("Hello from boost::interprocess", sharedStr->get_allocator()); std::cout << "Process wrote: " << *sharedStr << std::endl; } catch (const std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; } return 0;}在另一个进程中读取这个字符串:
#include <boost/interprocess/shared_memory_object.hpp>#include <boost/interprocess/mapped_region.hpp>#include <boost/interprocess/containers/string.hpp>#include <boost/interprocess/allocators/allocator.hpp>#include <iostream>namespace bip = boost::interprocess;typedef bip::allocator<char, bip::managed_shared_memory::segment_manager> CharAllocator;typedef bip::basic_string<char, std::char_traits<char>, CharAllocator> SharedString;int main() { try { // 打开共享内存对象 bip::shared_memory_object shm(bip::open_only, "MySharedMemory", bip::read_only); // 映射共享内存区域 bip::mapped_region region(shm, bip::read_only); void *addr = region.get_address(); // 创建共享内存管理对象 bip::managed_shared_memory segment(bip::attach_only, "MySharedMemory"); // 从共享内存中获取字符串对象 SharedString *sharedStr = segment.find<SharedString>("MyString").first; if (sharedStr) { std::cout << "Process read: " << *sharedStr << std::endl; } else { std::cerr << "String not found in shared memory" << std::endl; } } catch (const std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; } return 0;}通过使用 boost::interprocess 库,我们可以方便地在共享内存中存储和操作字符串、数组等数据结构,避免了直接使用 std::string 和 std::vector 带来的问题 。
end
如果这篇文章对你有所启发,欢迎点赞、在看,转发三连。星标⭐账号,还可以第一时间收到推送,感谢你的收看,我们下期再见~
往期干货推荐