相信不少小伙伴在面试过程中都被问到过共享内存。通过阅读本文您可以学习到一套比较完整的共享内存使用流程,以及共享内存在使用过程中常见问题的处理方法。
本文介绍一个基于共享内存SHM实现的消息队列,通过该队列能够实现多进程数据通信。在工程项目中,也具有一定的参考价值。
具体内容如下:
- SHM循环队列需求说明
- SHM循环队列整体设计
- SHM Manager设计
- SHM Mutex设计
- Circular Queue结构设计
- Circular Queue入队设计
- Circular Queue出队设计
- SHM Queue设计
- 总结
00 SHM循环队列需求说明
00.1 功能性需求
- 需要基于共享内存,实现多个进程(非父子关系)之间相互通信的消息队列。
- 每个进程都可以写入消息和读出消息。
- 共享内存的消息队列使用循环队列。
00.2 稳定性需求
其中某个进程崩溃时,要尽可能不影响其他进程访问共享内存。
01 SHM循环队列整体设计
为了实现上述的SHM消息队列,需要完成如下一些模块的设计:
- 一个管理SHM对象的模块,SHM Manager,用于创建/销毁或打开/关闭SHM对象。
- 一个维护循环队列的模块,Circular Queue。
- 一个保证多个进程安全访问共享内存的同步模块,SHM Mutex。
- 一个用户调用接口模块,SHM Queue。
为了简化设计,我们将进程分为两类:
- 一个manager进程,用于创建和销毁SHM资源,同时也能读写消息。
- 多个Normal进程,不需要创建和销毁SHM资源,只需要打开和关闭由manager进程创建的SHM资源,并通过循环队列读写消息。
程序运行流程如下:
在Manager进程启动,并创建SHM资源后,SHM进入可用状态。在此状态中,Normal进程才能启动并读写SHM循环消息队列。
02 SHM Manager设计
这里我们使用POSIX SHM接口(使用其他SHM方式也可以)。
02.1 ShmManager接口定义
ShmManager接口定义如下:
class ShmManager {
public:
ShmManager();
~ShmManager();
// 初始化SHM资源对象,即创建/打开
bool Init(uint64_t shm_size, bool is_manager);
// 释放SHM资源,即销毁/关闭
void Release();
void* GetMemory(uint64_t& mem_size);
private:
bool is_manager_{
false};
int shm_fd_{
-1};
void* start_ptr_{
nullptr};
uint64_t shm_size_{
0};
static const std::string shm_name_;
};
02.2 创建/打开SHM
创建/打开SHM资源的代码如下:
const std::string ShmManager::shm_name_("/shm_queue");
bool ShmManager::Init(uint64_t shm_size, bool is_manager) {
shm_size_ = shm_size;
is_manager_ = is_manager;
int oflag = O_RDWR;
if (is_manager_) {
// manager进程需要O_CREAT标记创建SHM资源
oflag |= O_CREAT;
// manager进程需要删除历史残留文件,normal进程不需要
shm_unlink(shm_name_.c_str());
}
// 1. 创建共享内存对象
shm_fd_ = shm_open(shm_name_.c_str(), oflag, 0666);
if (shm_fd_ == -1) {
std::cerr << "shm_open failed:" << strerror(errno)
<< std::endl;
return false;
}
if (is_manager_) {
// 2. 设置共享内存大小,normal进程不需要
if (ftruncate(shm_fd_, shm_size_) == -1) {
std::cerr << "ftruncate failed:" << strerror(errno)
<< std::endl;
return false;
}
}
// 3. 将共享内存映射到当前进程地址空间
start_ptr_ = mmap(0, shm_size_, PROT_WRITE|PROT_READ,
MAP_SHARED, shm_fd_, 0);
if (start_ptr_ == MAP_FAILED) {
std::cerr << "mmap failed:" << strerror(errno)
<< std::endl;
start_ptr_ = nullptr;
return false;
}
return true;
}
上面的代码产生了一个共享内存的区域,共享内存的首地址为start_ptr_,大小为shm_size_。
对于manager进程来说,调用shm_open时,需要使用O_RDWR | O_CREAT参数,以确保能新建SHM对象。对于normal进程,需要使用O_RDWR参数,以确保打开已有的SHM对象。
normal进程不需要调用ftruncate函数,来修改共享内存大小。
另外,manager进程需要在调用shm_open前,调用shm_unlink函数,以防止之前由于进程崩溃等原因,残留的shm文件能够被清理掉。以增加程序稳定性。
02.3 销毁/关闭SHM
销毁/关闭SHM资源的代码如下:
void ShmManager::Release() {
// 1. 解除映射
if (start_ptr_ != nullptr) {
munmap(start_ptr_, shm_size_);
start_ptr_ = nullptr;
}
// 2. 关闭共享内存对象
if (shm_fd_ != -1) {
close(shm_fd_);
shm_fd_ = -1;
}
// 3. 删除位于/dev/shm下的shm文件,normal进程不需要
if (is_manager_) {
shm_unlink(shm_name_.c_str());
}
shm_size_ = 0;
}
对于normal进程,在退出过程中不需要删除shm文件。
对于SHM文件,如果在调用shm_unlink函数之前,其它进程已经打开了该文件的文件描述,那么此文件描述一直有效,即使某些进程通过shm_unlink函数删除了该文件。
02.4 其它接口
GetMemroy接口实现如下:
void* ShmManager