前言
本节我想介绍一下基于共享内存(Shared Memory) + 信号量(Semaphore)实现的进程间通信(IPC)方式。在Qt中,已经实现了相应的封装类,因此也有了良好的跨平台特性。
一、共享内存(Shared Memory)基本原理
1.什么是共享内存?
共享内存是操作系统提供的一种 高效 IPC 机制,允许多个进程映射同一块物理内存区域,从而直接读写数据。
数据无需复制(不像 socket 或管道),延迟极低、吞吐极高,适合大数据块(如图像、音频、缓存)传输。
2. 为什么需要同步机制?
共享内存本身不提供同步!多个进程同时读写会导致竞态条件(Race Condition)。
必须配合 信号量(Semaphore)、互斥锁(Mutex)、事件(Event) 等同步原语。
在之后的示例代码中,我使用了两个 QSystemSemaphore 实现经典的 "生产者-消费者"模型:
m_freeSemaphore:表示"缓冲区是否空闲"(初始值 = 1)
m_usedSemaphore:表示"是否有新数据"(初始值 = 0)
3.Qt 对共享内存的封装:QSharedMemory + QSystemSemaphore
QSharedMemory,创建/附加/访问共享内存块
QSystemSemaphore,跨进程信号量(用于同步)
注意:QSharedMemory 不是线程安全的,即使在同一进程内多线程访问,也需加锁(如 lock() / unlock())。
4.共享内存作为 IPC 的优缺点
优点:
极致性能,数据零拷贝,微秒级延迟
适合大数据,图像、视频帧、科学计算结果等
跨语言兼容,C/C++/Rust/C# 都可访问同名共享内存
Qt 封装简洁,QSharedMemory API 易用
缺点:
无内置同步,必须手动管理信号量/互斥体
生命周期管理复杂,进程崩溃后可能残留(需清理逻辑)
平台差异,Windows/Linux 权限、命名规则不同
调试困难,无法像 socket 那样抓包,需专用工具(如 RAMMap)
仅限本机,不能跨机器
二、代码示例
和之前一样,单独创建一个窗口类来测试共享内存:
(这个类的具体实现上存在一些问题,当个参考就行)
cpp
#ifndef SHAREDMEMORYWINDOW_H
#define SHAREDMEMORYWINDOW_H
#include <QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSharedMemory>
#include <QSystemSemaphore>
#include <QTimer>
#include <QDateTime>
class SharedMemoryWindow : public QWidget
{
Q_OBJECT
public:
explicit SharedMemoryWindow(const QString &role, QWidget *parent = nullptr);
private slots:
void onSendMessage();
void onReadMessage();
void onTimerTimeout();
private:
void appendLog(const QString &msg);
void setupSharedMemory();
QString m_role;
QTextEdit *m_logView;
QLineEdit *m_inputEdit;
QPushButton *m_sendButton;
QPushButton *m_readButton;
QSharedMemory *m_sharedMemory;
QSystemSemaphore *m_freeSemaphore;
QSystemSemaphore *m_usedSemaphore;
QTimer *m_timer;
QString m_shmKey;
QString m_freeSemKey;
QString m_usedSemKey;
};
#endif // SHAREDMEMORYWINDOW_H
cpp
#include "sharedmemorywindow.h"
#include <QLabel>
SharedMemoryWindow::SharedMemoryWindow(const QString &role, QWidget *parent)
: QWidget(parent)
, m_role(role)
, m_sharedMemory(nullptr)
, m_freeSemaphore(nullptr)
, m_usedSemaphore(nullptr)
, m_timer(nullptr)
, m_shmKey("ipc_test_shm")
, m_freeSemKey("ipc_test_free")
, m_usedSemKey("ipc_test_used")
{
setWindowTitle("Shared Memory - " + role);
resize(600, 500);
m_logView = new QTextEdit();
m_logView->setReadOnly(true);
m_inputEdit = new QLineEdit();
m_inputEdit->setPlaceholderText("Enter message to write...");
m_sendButton = new QPushButton("Write to Shared Memory");
m_readButton = new QPushButton("Read from Shared Memory");
QVBoxLayout *mainLayout = new QVBoxLayout();
mainLayout->addWidget(m_logView);
mainLayout->addWidget(m_inputEdit);
mainLayout->addWidget(m_sendButton);
mainLayout->addWidget(m_readButton);
setLayout(mainLayout);
connect(m_sendButton, &QPushButton::clicked, this, &SharedMemoryWindow::onSendMessage);
connect(m_readButton, &QPushButton::clicked, this, &SharedMemoryWindow::onReadMessage);
setupSharedMemory();
if (m_role == "Server") {
// Server periodically checks for messages
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &SharedMemoryWindow::onTimerTimeout);
m_timer->start(1000); // Check every second
}
appendLog("Shared Memory " + m_role + " initialized");
}
void SharedMemoryWindow::setupSharedMemory()
{
m_sharedMemory = new QSharedMemory(m_shmKey, this);
m_freeSemaphore = new QSystemSemaphore(m_freeSemKey, 1, QSystemSemaphore::Create);
m_usedSemaphore = new QSystemSemaphore(m_usedSemKey, 0, QSystemSemaphore::Create);
}
void SharedMemoryWindow::onSendMessage()
{
QString msg = m_inputEdit->text();
if (msg.isEmpty()) return;
if (!m_freeSemaphore->acquire()) {
appendLog("Failed to acquire free semaphore: " + m_freeSemaphore->errorString());
return;
}
if (m_sharedMemory->attach()) {
m_sharedMemory->detach(); // Detach first if already attached
}
QByteArray data = msg.toUtf8();
if (!m_sharedMemory->create(data.size())) {
appendLog("Failed to create shared memory: " + m_sharedMemory->errorString());
m_freeSemaphore->release();
return;
}
m_sharedMemory->lock();
memcpy(m_sharedMemory->data(), data.constData(), data.size());
m_sharedMemory->unlock();
m_usedSemaphore->release(); // Signal that data is available
appendLog("Wrote to shared memory: " + msg);
m_inputEdit->clear();
}
void SharedMemoryWindow::onReadMessage()
{
if (!m_usedSemaphore->acquire()) {
appendLog("Failed to acquire used semaphore: " + m_usedSemaphore->errorString());
return;
}
if (!m_sharedMemory->attach()) {
appendLog("Failed to attach to shared memory: " + m_sharedMemory->errorString());
m_freeSemaphore->release();
return;
}
m_sharedMemory->lock();
QByteArray data(static_cast<const char*>(m_sharedMemory->data()), m_sharedMemory->size());
m_sharedMemory->unlock();
m_sharedMemory->detach();
m_freeSemaphore->release();
appendLog("Read from shared memory: " + QString::fromUtf8(data));
}
void SharedMemoryWindow::onTimerTimeout()
{
// Try to read without blocking
if (m_usedSemaphore->acquire(/*0*/)) { // Non-blocking acquire
if (!m_sharedMemory->attach()) {
m_freeSemaphore->release();
return;
}
m_sharedMemory->lock();
QByteArray data(static_cast<const char*>(m_sharedMemory->data()), m_sharedMemory->size());
m_sharedMemory->unlock();
m_sharedMemory->detach();
m_freeSemaphore->release();
appendLog("Server received: " + QString::fromUtf8(data));
}
}
void SharedMemoryWindow::appendLog(const QString &msg)
{
m_logView->append(QDateTime::currentDateTime().toString("hh:mm:ss") + " | " + msg);
}
稍微解释一下共享内存和信号量的类使用:
cpp
QSharedMemory *m_sharedMemory;
QSystemSemaphore *m_freeSemaphore;
QSystemSemaphore *m_usedSemaphore;
m_shmKey("ipc_test_shm")
m_freeSemKey("ipc_test_free")
m_usedSemKey("ipc_test_used")
m_sharedMemory = new QSharedMemory(m_shmKey, this);
m_freeSemaphore = new QSystemSemaphore(m_freeSemKey, 1, QSystemSemaphore::Create);
m_usedSemaphore = new QSystemSemaphore(m_usedSemKey, 0, QSystemSemaphore::Create);
这三个玩意儿创建的时候,都需要一个key值,一般来说就是两个进程约定好的字符串。只有字符串相同,像系统申请到的共享内存才是同一块。
虽然sharedMemory有lock和unlock,但对于跨进程来说,必须要搭配信号量来解决并发。
信号量是一个整型计数器,用于控制对有限数量资源的并发访问。
它支持两个原子操作:
acquire()(P 操作):申请资源,计数器减 1;若值 ≤ 0,则阻塞等待。
release()(V 操作):释放资源,计数器加 1;若有等待者,唤醒一个。
可以把信号量想象成"停车场空位计数器":
acquire() = 开车进入(空位 -1)
release() = 开车离开(空位 +1)
若空位为 0,新来的车必须排队等待
代码中使用了两个信号量,分别是释放信号量freeSem、使用信号量usedSem。
我写一下我的理解思路。
1.进程a想要写入数据,需要申请资源。freeSem初始值是1,此时调用了acquire接口,可正常通过。计数器-1后归零。之后它可以正常写入数据,但期间如果有其他进程想要写数据,计数器已经是0。调用acquire时-1会阻塞等待。
2.进程b想要读取数据,它首先想要调用usedSem的acquire,但它的初始值是0,于是会阻塞等待。
3.进程a写入数据后,调用了usedSem的release接口,计数器+1。紧接着,进程b原本阻塞的usedSem被放开,成功进入读取步骤。
4.此时如果进程a想要写入数据,因为freeSem=0,此时调用acquire会阻塞。
5.进程b读取完数据后,调用freeSem的release,其计数器+1。紧接着,进程a原本阻塞的freeSem被放开,进入写入数据环节。
以上,是单向的写入和读取,通过两个信号量来管理写入和读取的时机。
三、解决程序重复打开的问题
在实际工作中,我曾经遇到过一个需求,客户希望我们的exe无法重复打开,也就是说一个exe不要对应多个进程。
当时我就是使用共享内存来实现的。思路很简单,第一个进程在main中申请一块内存,第二个进程在申请同名内存的时候如果失败,就证明第一个进程已经打开,之后它自己return 0退出程序即可。
代码也非常简单:
cpp
QSystemSemaphore sema("xxxxx",1,QSystemSemaphore::Open);
sema.acquire();// 在临界区操作共享内存 SharedMemory
QSharedMemory mem("xxxxxxx");// 全局对象名
if (!mem.create(1))// 如果全局对象以存在则退出
{
//QMessageBox::information(0, QObject::tr("提示"),QObject::tr("程序运行中!"));
sema.release();
return 0;
}
sema.release();
可以看到,条件判断就是以mem.create(1)来判定的(申请内存1字节,完全不影响程序本身的运行)。
这里虽然用到了信号量,但本质上是多余的,因为也不涉及内存本身的写入和读取。
四、总结
共享内存说白了,就是两个进程共享同一块内存,并搭配信号量的使用,解决写入读取的竟态问题。我们对于进程的理解中,经常是这样描述的:
**进程是系统进行资源分配的基本单位。**它拥有独立的地址空间、文件描述符、环境变量、用户 ID 等系统资源。理论上,进程是资源相互隔离,拥有独立性和安全性的。但在共享内存这种实现之下,资源也能实现共享,甚至效率还很高。
另外提一嘴,**线程是 CPU 调度和执行的基本单位。**一个进程可以包含多个线程,这些线程共享进程的资源(如内存、文件句柄),但每个线程有自己的栈、寄存器状态和程序计数器。