下面给你一篇可直接整理成博客的版本。重点补上了 QSharedMemory + QSystemSemaphore、多读单写跨进程读写锁、单例共享内存管理类,以及工业使用时需要注意的"零拷贝边界"。
Qt 进程间通信:从 QSharedMemory、QSystemSemaphore 到工业可用的零拷贝共享内存封装
在 Qt 项目中,如果两个程序运行在同一台电脑上,并且需要高效交换数据,就会涉及 IPC,也就是进程间通信。
前面我们已经介绍过 QLocalSocketQLocalSocket文章。它适合传递 JSON、命令、状态、小数据包。但是如果两个进程之间需要高频传递大量数据,例如:
text
雷达点迹
目标列表
图像帧
实时状态数组
高频采样数据
如果全部通过 QLocalSocket 发送,就会产生较多的数据复制和事件分发开销。这时更适合使用:
text
QSharedMemory + QSystemSemaphore
其中:
text
QSharedMemory 负责共享大块内存
QSystemSemaphore 负责跨进程同步
简单理解:
text
QLocalSocket 像本机版 TCP,适合传消息
QSharedMemory 多个进程映射同一块内存,适合传大数据
QSystemSemaphore 解决多个进程同时读写时的同步问题
一、为什么需要 QSharedMemory
正常情况下,每个进程都有独立的虚拟地址空间。
text
进程 A 内存空间
├── 变量
├── 对象
└── 缓冲区
进程 B 内存空间
├── 变量
├── 对象
└── 缓冲区
进程 A 不能直接访问进程 B 的普通变量。
例如进程 B 中有:
cpp
int targetCount = 10;
进程 A 不能直接访问这个变量。因为两个进程的内存空间是隔离的。
QSharedMemory 解决的问题是:让两个或多个进程映射同一块共享内存。
text
进程 A 虚拟地址 ─┐
├── 同一块物理内存
进程 B 虚拟地址 ─┘
这样进程 A 写入共享内存后,进程 B 可以从同一块共享内存读取数据。
二、操作系统底层干了什么
使用共享内存时,操作系统大致做了几件事:
text
1. 创建一块由内核管理的共享内存对象
2. 进程 A attach 这块共享内存
3. 操作系统把这块共享内存映射到进程 A 的虚拟地址空间
4. 进程 B attach 同一块共享内存
5. 操作系统把同一块共享内存映射到进程 B 的虚拟地址空间
6. 两个进程看到的是不同的虚拟地址
7. 但底层对应的是同一批物理内存页
所以:
text
进程 A 写入共享内存
进程 B 读取共享内存
本质上是访问同一块物理内存
这就是共享内存性能高的原因。
不过需要注意:共享内存只解决"数据共享"问题,不解决"谁先读、谁先写、能不能同时读写"的问题。
所以必须配合同步机制。
Qt 中常用:
text
QSystemSemaphore
三、QSystemSemaphore 是什么
QSystemSemaphore 是 Qt 提供的系统级信号量封装。
它可以用于:
text
跨线程同步
跨进程同步
控制多个进程对共享资源的访问
例如共享内存中有一块目标数据区:
text
共享内存
├── Header
└── TargetRecord[1000]
如果一个进程正在写入目标数组,另一个进程同时读取,就可能读到一半新数据、一半旧数据。
所以需要加锁。
最简单的是互斥锁:
text
一次只允许一个进程访问共享内存
但是这种方式效率不高。因为多个读取进程本来可以同时读,只要没有写入者就不会互相影响。
因此更好的方式是:
text
多读单写锁
也就是:
text
多个读者可以同时读取
写者写入时,禁止其他读写
读者读取时,写者等待
四、为什么要封装跨进程读写锁
Qt 有 QReadWriteLock,但是它主要用于同一进程内的多线程同步,不适合直接跨进程使用。
跨进程场景需要使用系统级同步对象,例如:
text
QSystemSemaphore
我们可以基于 QSystemSemaphore 实现一个跨进程读写锁:
text
CrossProcessRwLock
目标:
text
1. 支持多读单写
2. 支持跨进程
3. 支持同一进程内多线程
4. 使用 RAII,避免忘记解锁
5. 可以保护 QSharedMemory
6. 读写方可以直接访问共享内存指针
五、零拷贝需要先说明清楚
使用共享内存可以做到 IPC 层面的零拷贝。
也就是说:
text
数据不需要从进程 A 通过 socket 复制到内核缓冲区,
再从内核缓冲区复制到进程 B。
两个进程直接映射同一块共享内存。
但是要注意:
text
如果你把 JSON 序列化成 QByteArray,再 memcpy 到共享内存,
这个过程仍然存在一次业务层拷贝。
真正更接近零拷贝的用法是:
text
直接在共享内存中的结构体数组或环形缓冲区上写字段
读取方在读锁保护下直接读取共享内存指针
例如:
cpp
record->id = 1;
record->tvRecRange = 5000.0;
record->result = 1;
而不是:
cpp
QByteArray json = doc.toJson();
memcpy(sharedMemory.data(), json.constData(), json.size());
所以本文中的"零拷贝"指的是:
text
进程间数据交换不再通过 socket 复制数据;
读写方直接访问共享内存中的数据结构。
六、跨进程多读单写锁封装类
下面是一个工业项目可用的基础版跨进程读写锁封装。
特点:
text
1. 基于 QSharedMemory 保存 readerCount
2. 基于 QSystemSemaphore 实现跨进程同步
3. 支持多个读者同时读取
4. 写者独占
5. 使用 serviceQueue 减少写者饥饿
6. 提供 RAII ReadLocker / WriteLocker
文件名建议:
text
CrossProcessRwLock.h
代码如下。
cpp
#ifndef CROSSPROCESSRWLOCK_H
#define CROSSPROCESSRWLOCK_H
#include <QSharedMemory>
#include <QSystemSemaphore>
#include <QScopedPointer>
#include <QMutex>
#include <QMutexLocker>
#include <QString>
#include <cstring>
class CrossProcessRwLock
{
public:
CrossProcessRwLock() = default;
~CrossProcessRwLock() = default;
CrossProcessRwLock(const CrossProcessRwLock &) = delete;
CrossProcessRwLock &operator=(const CrossProcessRwLock &) = delete;
bool open(const QString &name, QString *errorMessage = nullptr)
{
QMutexLocker locker(&m_openMutex);
if (m_opened) {
return true;
}
if (name.trimmed().isEmpty()) {
setError(errorMessage, QStringLiteral("CrossProcessRwLock name is empty"));
return false;
}
m_name = name;
m_initSem.reset(new QSystemSemaphore(m_name + "_init_sem",
1,
QSystemSemaphore::Open));
if (!acquireSem(m_initSem.data(), QStringLiteral("init_sem"), errorMessage)) {
return false;
}
bool initSemHeld = true;
m_stateMemory.setKey(m_name + "_state_memory");
bool created = m_stateMemory.create(static_cast<int>(sizeof(State)));
if (!created) {
if (m_stateMemory.error() == QSharedMemory::AlreadyExists) {
if (!m_stateMemory.attach()) {
releaseSem(m_initSem.data(), QStringLiteral("init_sem"), nullptr);
setError(errorMessage,
QStringLiteral("attach lock state memory failed: %1")
.arg(m_stateMemory.errorString()));
return false;
}
} else {
releaseSem(m_initSem.data(), QStringLiteral("init_sem"), nullptr);
setError(errorMessage,
QStringLiteral("create lock state memory failed: %1")
.arg(m_stateMemory.errorString()));
return false;
}
} else {
State *s = state();
std::memset(s, 0, sizeof(State));
s->magic = magicValue();
s->version = versionValue();
s->readerCount = 0;
}
State *s = state();
if (!s || s->magic != magicValue()) {
releaseSem(m_initSem.data(), QStringLiteral("init_sem"), nullptr);
setError(errorMessage, QStringLiteral("invalid rwlock state memory"));
return false;
}
if (!releaseSem(m_initSem.data(), QStringLiteral("init_sem"), errorMessage)) {
return false;
}
initSemHeld = false;
m_serviceQueueSem.reset(new QSystemSemaphore(m_name + "_service_queue_sem",
1,
QSystemSemaphore::Open));
m_readerMutexSem.reset(new QSystemSemaphore(m_name + "_reader_mutex_sem",
1,
QSystemSemaphore::Open));
m_resourceSem.reset(new QSystemSemaphore(m_name + "_resource_sem",
1,
QSystemSemaphore::Open));
if (!m_serviceQueueSem || !m_readerMutexSem || !m_resourceSem) {
if (initSemHeld) {
releaseSem(m_initSem.data(), QStringLiteral("init_sem"), nullptr);
}
setError(errorMessage, QStringLiteral("create semaphore object failed"));
return false;
}
m_opened = true;
return true;
}
bool isOpen() const
{
return m_opened;
}
bool lockForRead(QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("rwlock is not open"));
return false;
}
/*
* serviceQueueSem 用于排队,避免大量读者源源不断进入导致写者饥饿。
* readerMutexSem 用于保护 readerCount。
* resourceSem 表示真正的共享资源。
*/
if (!acquireSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), errorMessage)) {
return false;
}
if (!acquireSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), errorMessage)) {
releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), nullptr);
return false;
}
State *s = state();
if (!s || s->magic != magicValue()) {
releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), nullptr);
releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), nullptr);
setError(errorMessage, QStringLiteral("invalid rwlock state"));
return false;
}
++s->readerCount;
if (s->readerCount == 1) {
if (!acquireSem(m_resourceSem.data(), QStringLiteral("resource_sem"), errorMessage)) {
--s->readerCount;
releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), nullptr);
releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), nullptr);
return false;
}
}
bool ok = true;
ok = releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), errorMessage) && ok;
ok = releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), errorMessage) && ok;
return ok;
}
bool unlockRead(QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("rwlock is not open"));
return false;
}
if (!acquireSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), errorMessage)) {
return false;
}
State *s = state();
if (!s || s->magic != magicValue()) {
releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), nullptr);
setError(errorMessage, QStringLiteral("invalid rwlock state"));
return false;
}
if (s->readerCount <= 0) {
releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), nullptr);
setError(errorMessage, QStringLiteral("unlockRead called without read lock"));
return false;
}
--s->readerCount;
bool ok = true;
if (s->readerCount == 0) {
ok = releaseSem(m_resourceSem.data(), QStringLiteral("resource_sem"), errorMessage) && ok;
}
ok = releaseSem(m_readerMutexSem.data(), QStringLiteral("reader_mutex_sem"), errorMessage) && ok;
return ok;
}
bool lockForWrite(QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("rwlock is not open"));
return false;
}
if (!acquireSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), errorMessage)) {
return false;
}
if (!acquireSem(m_resourceSem.data(), QStringLiteral("resource_sem"), errorMessage)) {
releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), nullptr);
return false;
}
if (!releaseSem(m_serviceQueueSem.data(), QStringLiteral("service_queue_sem"), errorMessage)) {
releaseSem(m_resourceSem.data(), QStringLiteral("resource_sem"), nullptr);
return false;
}
return true;
}
bool unlockWrite(QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("rwlock is not open"));
return false;
}
return releaseSem(m_resourceSem.data(), QStringLiteral("resource_sem"), errorMessage);
}
class ReadLocker
{
public:
explicit ReadLocker(CrossProcessRwLock *lock, QString *errorMessage = nullptr)
: m_lock(lock)
{
if (m_lock) {
m_locked = m_lock->lockForRead(errorMessage);
}
}
~ReadLocker()
{
unlock();
}
ReadLocker(const ReadLocker &) = delete;
ReadLocker &operator=(const ReadLocker &) = delete;
bool isLocked() const
{
return m_locked;
}
void unlock()
{
if (m_lock && m_locked) {
m_lock->unlockRead(nullptr);
m_locked = false;
}
}
private:
CrossProcessRwLock *m_lock = nullptr;
bool m_locked = false;
};
class WriteLocker
{
public:
explicit WriteLocker(CrossProcessRwLock *lock, QString *errorMessage = nullptr)
: m_lock(lock)
{
if (m_lock) {
m_locked = m_lock->lockForWrite(errorMessage);
}
}
~WriteLocker()
{
unlock();
}
WriteLocker(const WriteLocker &) = delete;
WriteLocker &operator=(const WriteLocker &) = delete;
bool isLocked() const
{
return m_locked;
}
void unlock()
{
if (m_lock && m_locked) {
m_lock->unlockWrite(nullptr);
m_locked = false;
}
}
private:
CrossProcessRwLock *m_lock = nullptr;
bool m_locked = false;
};
private:
struct State
{
quint32 magic;
quint32 version;
qint32 readerCount;
qint32 reserved;
};
private:
static quint32 magicValue()
{
return 0x52574C4Bu; // RWLK
}
static quint32 versionValue()
{
return 1u;
}
State *state()
{
return static_cast<State *>(m_stateMemory.data());
}
const State *state() const
{
return static_cast<const State *>(m_stateMemory.constData());
}
static void setError(QString *errorMessage, const QString &message)
{
if (errorMessage) {
*errorMessage = message;
}
}
static bool acquireSem(QSystemSemaphore *sem,
const QString &name,
QString *errorMessage)
{
if (!sem) {
setError(errorMessage, QStringLiteral("semaphore is null: %1").arg(name));
return false;
}
if (!sem->acquire()) {
setError(errorMessage,
QStringLiteral("acquire semaphore failed [%1]: %2")
.arg(name, sem->errorString()));
return false;
}
return true;
}
static bool releaseSem(QSystemSemaphore *sem,
const QString &name,
QString *errorMessage)
{
if (!sem) {
setError(errorMessage, QStringLiteral("semaphore is null: %1").arg(name));
return false;
}
if (!sem->release()) {
setError(errorMessage,
QStringLiteral("release semaphore failed [%1]: %2")
.arg(name, sem->errorString()));
return false;
}
return true;
}
private:
QString m_name;
QSharedMemory m_stateMemory;
QScopedPointer<QSystemSemaphore> m_initSem;
QScopedPointer<QSystemSemaphore> m_serviceQueueSem;
QScopedPointer<QSystemSemaphore> m_readerMutexSem;
QScopedPointer<QSystemSemaphore> m_resourceSem;
mutable QMutex m_openMutex;
bool m_opened = false;
};
#endif // CROSSPROCESSRWLOCK_H
七、读写锁的核心原理
这个读写锁内部使用三个系统信号量:
text
serviceQueueSem 排队信号量,避免写者饥饿
readerMutexSem 保护 readerCount
resourceSem 真正保护共享资源
共享状态保存在一块小的 QSharedMemory 中:
cpp
struct State
{
quint32 magic;
quint32 version;
qint32 readerCount;
qint32 reserved;
};
读锁流程
text
1. 进入 serviceQueueSem 排队
2. 获取 readerMutexSem
3. readerCount++
4. 如果是第一个读者,则获取 resourceSem
5. 释放 readerMutexSem
6. 释放 serviceQueueSem
7. 开始读取共享内存
多个读者可以同时进入。
写锁流程
text
1. 进入 serviceQueueSem 排队
2. 获取 resourceSem
3. 释放 serviceQueueSem
4. 开始写共享内存
写者持有 resourceSem 时,其他读者和写者都不能访问共享内存。
八、工业可用的共享内存单例封装
读写锁有了之后,还需要一个统一的共享内存管理类。
目标:
text
1. 进程内单例
2. 自动 create 或 attach 共享内存
3. 使用跨进程读写锁保护共享内存
4. 提供 withRead / withWrite 接口
5. 使用 lambda 直接访问共享内存指针
6. 避免把裸指针泄漏到锁外
文件名建议:
text
SharedMemoryBlock.h
代码如下。
cpp
#ifndef SHAREDMEMORYBLOCK_H
#define SHAREDMEMORYBLOCK_H
#include "CrossProcessRwLock.h"
#include <QSharedMemory>
#include <QSystemSemaphore>
#include <QScopedPointer>
#include <QMutex>
#include <QMutexLocker>
#include <QString>
#include <cstring>
class SharedMemoryBlock
{
public:
static SharedMemoryBlock &instance()
{
static SharedMemoryBlock inst;
return inst;
}
SharedMemoryBlock(const SharedMemoryBlock &) = delete;
SharedMemoryBlock &operator=(const SharedMemoryBlock &) = delete;
bool openOrCreate(const QString &key,
int bytes,
QString *errorMessage = nullptr)
{
QMutexLocker locker(&m_openMutex);
if (m_opened) {
return true;
}
if (key.trimmed().isEmpty()) {
setError(errorMessage, QStringLiteral("shared memory key is empty"));
return false;
}
if (bytes <= 0) {
setError(errorMessage, QStringLiteral("shared memory size is invalid"));
return false;
}
m_key = key;
m_size = bytes;
/*
* 初始化信号量用于保护 create / attach / memset 初始化阶段。
* 防止多个进程同时初始化共享内存。
*/
m_initSem.reset(new QSystemSemaphore(m_key + "_mem_init_sem",
1,
QSystemSemaphore::Open));
if (!m_initSem->acquire()) {
setError(errorMessage,
QStringLiteral("acquire memory init semaphore failed: %1")
.arg(m_initSem->errorString()));
return false;
}
m_memory.setKey(m_key);
bool created = m_memory.create(bytes);
if (!created) {
if (m_memory.error() == QSharedMemory::AlreadyExists) {
if (!m_memory.attach()) {
m_initSem->release();
setError(errorMessage,
QStringLiteral("attach shared memory failed: %1")
.arg(m_memory.errorString()));
return false;
}
} else {
m_initSem->release();
setError(errorMessage,
QStringLiteral("create shared memory failed: %1")
.arg(m_memory.errorString()));
return false;
}
} else {
std::memset(m_memory.data(), 0, static_cast<size_t>(bytes));
}
if (m_memory.size() < bytes) {
m_initSem->release();
setError(errorMessage,
QStringLiteral("shared memory size mismatch, actual=%1, expect=%2")
.arg(m_memory.size())
.arg(bytes));
return false;
}
if (!m_initSem->release()) {
setError(errorMessage,
QStringLiteral("release memory init semaphore failed: %1")
.arg(m_initSem->errorString()));
return false;
}
if (!m_rwLock.open(m_key + "_rwlock", errorMessage)) {
return false;
}
m_opened = true;
return true;
}
bool isOpen() const
{
return m_opened;
}
int size() const
{
return m_size;
}
template <typename Func>
bool withRead(Func func, QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("shared memory is not open"));
return false;
}
CrossProcessRwLock::ReadLocker locker(&m_rwLock, errorMessage);
if (!locker.isLocked()) {
return false;
}
const char *base = static_cast<const char *>(m_memory.constData());
func(base, m_memory.size());
return true;
}
template <typename Func>
bool withWrite(Func func, QString *errorMessage = nullptr)
{
if (!m_opened) {
setError(errorMessage, QStringLiteral("shared memory is not open"));
return false;
}
CrossProcessRwLock::WriteLocker locker(&m_rwLock, errorMessage);
if (!locker.isLocked()) {
return false;
}
char *base = static_cast<char *>(m_memory.data());
func(base, m_memory.size());
return true;
}
private:
SharedMemoryBlock() = default;
~SharedMemoryBlock() = default;
static void setError(QString *errorMessage, const QString &message)
{
if (errorMessage) {
*errorMessage = message;
}
}
private:
QString m_key;
int m_size = 0;
bool m_opened = false;
QSharedMemory m_memory;
CrossProcessRwLock m_rwLock;
QScopedPointer<QSystemSemaphore> m_initSem;
mutable QMutex m_openMutex;
};
#endif // SHAREDMEMORYBLOCK_H
九、为什么这个单例类可以实现零拷贝访问
它的核心接口是:
cpp
withRead()
withWrite()
例如写入方:
cpp
SharedMemoryBlock::instance().withWrite([](char *base, int bytes) {
// 直接写共享内存
});
读取方:
cpp
SharedMemoryBlock::instance().withRead([](const char *base, int bytes) {
// 直接读共享内存
});
这样读写方都直接访问共享内存地址。
关键点是:
text
不要把共享内存指针保存到 lambda 外面
不要在锁释放后继续使用 base 指针
也就是说,下面这种写法是错误的:
cpp
const char *g_ptr = nullptr;
SharedMemoryBlock::instance().withRead([&](const char *base, int) {
g_ptr = base;
});
// 错误:锁已经释放,不能继续使用 g_ptr
正确做法是:
cpp
SharedMemoryBlock::instance().withRead([](const char *base, int) {
// 在这个作用域内直接读取
});
十、工业场景示例:共享打击结果环形缓冲区
下面设计一个适合工业场景的共享内存结构。
需求:
text
1. 一个进程持续写入打击结果
2. 一个或多个进程读取打击结果
3. 支持高频写入
4. 固定容量
5. 写满后覆盖旧数据
6. 读取方可以按顺序读取最新数据
7. 尽量零拷贝
1. 共享内存数据结构
注意:共享内存中不要直接放 QString、QVector、QMap 这类复杂对象。
应该放:
text
int
double
char 数组
POD 结构体
固定长度数组
定义如下。
cpp
#ifndef STRIKESHAREDTYPES_H
#define STRIKESHAREDTYPES_H
#include <QtGlobal>
#pragma pack(push, 1)
struct StrikeSharedHeader
{
quint32 magic;
quint32 version;
quint32 capacity;
quint32 count;
quint32 writeIndex;
quint32 reserved;
quint64 nextSeq;
};
struct StrikeRecord
{
quint64 seq;
qint32 id;
double tvRecRange;
double irRecRange;
double radarDetRange;
double firingRange;
qint32 result;
};
#pragma pack(pop)
static inline quint32 strikeMagic()
{
return 0x5354524Bu; // STRK
}
static inline quint32 strikeVersion()
{
return 1u;
}
static inline int strikeMemorySize(int capacity)
{
return static_cast<int>(sizeof(StrikeSharedHeader) +
sizeof(StrikeRecord) * capacity);
}
static inline StrikeSharedHeader *strikeHeader(char *base)
{
return reinterpret_cast<StrikeSharedHeader *>(base);
}
static inline const StrikeSharedHeader *strikeHeader(const char *base)
{
return reinterpret_cast<const StrikeSharedHeader *>(base);
}
static inline StrikeRecord *strikeRecords(char *base)
{
return reinterpret_cast<StrikeRecord *>(base + sizeof(StrikeSharedHeader));
}
static inline const StrikeRecord *strikeRecords(const char *base)
{
return reinterpret_cast<const StrikeRecord *>(base + sizeof(StrikeSharedHeader));
}
#endif // STRIKESHAREDTYPES_H
2. 初始化共享内存
cpp
#include "SharedMemoryBlock.h"
#include "StrikeSharedTypes.h"
#include <QDebug>
#include <cstring>
bool initStrikeSharedMemory(int capacity)
{
QString error;
const QString key = QStringLiteral("Strike_Result_Shared_Memory");
const int bytes = strikeMemorySize(capacity);
if (!SharedMemoryBlock::instance().openOrCreate(key, bytes, &error)) {
qDebug() << "open shared memory failed:" << error;
return false;
}
bool ok = SharedMemoryBlock::instance().withWrite([capacity](char *base, int bytes) {
Q_UNUSED(bytes)
StrikeSharedHeader *h = strikeHeader(base);
if (h->magic != strikeMagic() || h->version != strikeVersion()) {
std::memset(base, 0, static_cast<size_t>(strikeMemorySize(capacity)));
h->magic = strikeMagic();
h->version = strikeVersion();
h->capacity = static_cast<quint32>(capacity);
h->count = 0;
h->writeIndex = 0;
h->nextSeq = 1;
}
}, &error);
if (!ok) {
qDebug() << "init shared memory content failed:" << error;
return false;
}
return true;
}
3. 写入打击结果
这里写入的是固定结构体,直接写共享内存槽位。
cpp
bool appendStrikeRecord(qint32 id,
double tvRecRange,
double irRecRange,
double radarDetRange,
double firingRange,
qint32 result)
{
QString error;
bool ok = SharedMemoryBlock::instance().withWrite(
[&](char *base, int bytes) {
Q_UNUSED(bytes)
StrikeSharedHeader *h = strikeHeader(base);
if (h->magic != strikeMagic()) {
return;
}
StrikeRecord *records = strikeRecords(base);
quint32 index = h->writeIndex % h->capacity;
StrikeRecord *r = &records[index];
/*
* 直接写共享内存槽位。
* 这里没有通过 QLocalSocket 复制整包数据到另一个进程。
*/
r->seq = h->nextSeq++;
r->id = id;
r->tvRecRange = tvRecRange;
r->irRecRange = irRecRange;
r->radarDetRange = radarDetRange;
r->firingRange = firingRange;
r->result = result;
h->writeIndex = (h->writeIndex + 1) % h->capacity;
if (h->count < h->capacity) {
++h->count;
}
},
&error);
if (!ok) {
qDebug() << "append strike record failed:" << error;
return false;
}
return true;
}
4. 读取全部有效记录
读取方在读锁内直接访问共享内存,不需要把共享内存整体复制出来。
cpp
void readStrikeRecords()
{
QString error;
bool ok = SharedMemoryBlock::instance().withRead(
[](const char *base, int bytes) {
Q_UNUSED(bytes)
const StrikeSharedHeader *h = strikeHeader(base);
if (h->magic != strikeMagic()) {
qDebug() << "invalid strike shared memory";
return;
}
const StrikeRecord *records = strikeRecords(base);
quint32 count = h->count;
quint32 capacity = h->capacity;
/*
* writeIndex 指向下一次写入位置。
* 如果已经写满,最旧数据就在 writeIndex。
* 如果没写满,最旧数据在 0。
*/
quint32 oldestIndex = (count == capacity) ? h->writeIndex : 0;
for (quint32 i = 0; i < count; ++i) {
quint32 index = (oldestIndex + i) % capacity;
const StrikeRecord &r = records[index];
qDebug() << "seq =" << r.seq
<< "id =" << r.id
<< "tv =" << r.tvRecRange
<< "ir =" << r.irRecRange
<< "radar =" << r.radarDetRange
<< "firing =" << r.firingRange
<< "result =" << r.result;
}
},
&error);
if (!ok) {
qDebug() << "read strike records failed:" << error;
}
}
5. 读取指定 seq 的记录
如果读取方希望获取指定数据,建议每条记录带一个 seq。
cpp
bool findStrikeRecordBySeq(quint64 targetSeq)
{
QString error;
bool found = false;
bool ok = SharedMemoryBlock::instance().withRead(
[&](const char *base, int bytes) {
Q_UNUSED(bytes)
const StrikeSharedHeader *h = strikeHeader(base);
if (h->magic != strikeMagic()) {
return;
}
const StrikeRecord *records = strikeRecords(base);
for (quint32 i = 0; i < h->count; ++i) {
const StrikeRecord &r = records[i];
if (r.seq == targetSeq) {
found = true;
qDebug() << "found seq =" << r.seq
<< "id =" << r.id
<< "result =" << r.result;
return;
}
}
},
&error);
if (!ok) {
qDebug() << "find record failed:" << error;
return false;
}
return found;
}
如果需要保持环形缓冲区的真实顺序,也可以按 oldestIndex 的方式遍历。
十一、三个典型使用场景
场景一:高频目标数据共享
例如:
text
DataReceiver.exe 负责 UDP 接收、协议解析
MainUI.exe 负责界面显示
设计:
text
DataReceiver.exe
写入 QSharedMemory 中的目标数组
MainUI.exe
定时读取 QSharedMemory 中的目标数组
适合:
text
目标位置
目标速度
识别结果
打击结果
设备实时状态
优势:
text
不用每帧通过 socket 发送大块数据
界面进程可以按自己的刷新频率读取最新数据
读写互不直接阻塞网络接收逻辑
场景二:图像帧共享
例如一个进程负责采集图像,另一个进程负责显示图像。
设计:
text
共享内存布局:
Header
width
height
format
frameSeq
dataSize
ImageData
采集进程:
text
写入图像帧数据
更新 frameSeq
显示进程:
text
读取 frameSeq
如果 frameSeq 变化,则刷新显示
这种方式比通过 socket 传整张图像更高效。
场景三:共享实时状态表
例如一个后台服务维护系统状态:
text
设备在线状态
雷达状态
光电状态
转台状态
干扰设备状态
当前任务状态
界面进程直接读取共享内存中的状态表。
适合:
text
多界面进程共用一份状态
多个监控程序读取同一份状态
后台服务独立运行
界面程序崩溃后重启仍能读取最新状态
十二、QSharedMemory + QSystemSemaphore 和 QLocalSocket 如何配合
在工程中,不一定二选一。
最推荐的组合是:
text
QSharedMemory 放大数据
QSystemSemaphore 保护共享内存
QLocalSocket 发送通知和命令
例如:
text
数据进程写入共享内存
↓
通过 QLocalSocket 发送 JSON:
{
"cmd": "newStrikeRecord",
"seq": 1024
}
↓
界面进程收到通知
↓
从共享内存读取 seq=1024 的记录
这样做的好处:
text
大数据不走 socket
socket 只传小通知
共享内存负责高性能数据交换
十三、工业使用注意事项
1. 不要在共享内存中放复杂 Qt 对象
不要这样做:
cpp
struct BadRecord
{
QString name;
QVector<int> values;
};
因为 QString、QVector 内部有指针、引用计数、堆内存。
另一个进程无法直接使用这些指针。
应该使用:
cpp
struct GoodRecord
{
char name[64];
int values[16];
};
或者使用固定二进制结构。
2. 指针不能跨进程保存
共享内存里的地址在不同进程中可能不一样。
进程 A 中:
text
base = 0x10000000
进程 B 中:
text
base = 0x30000000
它们映射的是同一块物理内存,但虚拟地址可能不同。
所以共享内存结构中不要保存裸指针。
错误:
cpp
struct BadHeader
{
char *dataPtr;
};
正确:
cpp
struct GoodHeader
{
quint32 dataOffset;
quint32 dataLength;
};
跨进程共享数据时,应该保存:
text
offset
length
index
seq
不要保存指针。
3. QSystemSemaphore 不是崩溃恢复锁
如果一个进程在持有信号量时崩溃,其他进程可能一直等待。
例如:
text
进程 A lockForWrite
进程 A 崩溃
进程 B lockForRead 一直等待
这是系统信号量方案的常见问题。
工业项目中可以增加:
text
心跳字段
ownerPid
lastUpdateTime
超时检测
异常恢复机制
看门狗进程
更高级的方案可以考虑使用操作系统原生 robust mutex,但 Qt 的 QSystemSemaphore 本身不直接提供完整崩溃恢复能力。
4. 不要把共享内存当数据库
共享内存适合:
text
实时数据
最新状态
固定容量缓存
高频读写
不适合:
text
长期保存
复杂查询
无限追加
历史归档
如果要保存历史记录,应该使用:
text
文件
CSV
SQLite
MySQL
二进制日志
共享内存更适合作为实时数据通道,而不是持久化存储。
5. 读写锁不建议递归使用
本文的 CrossProcessRwLock 是非递归锁。
不要这样:
cpp
withWrite([] {
withRead([] {
// 错误:可能死锁
});
});
也不要在同一线程中重复获取写锁。
推荐规则:
text
一个作用域内只持有一种锁
锁内逻辑尽量短
不要在锁内做耗时操作
不要在锁内调用可能阻塞的函数
十四、整体推荐架构
对于高频网络数据接收,可以采用如下架构:
text
UDP 接收线程
↓
解析数据
↓
写入 QSharedMemory 环形缓冲区
↓
通过 QLocalSocket 通知界面有新数据
↓
界面进程读取共享内存
示意:
text
┌──────────────────────┐
│ DataReceiver.exe │
│ │
│ UDP ReadyRead │
│ 协议解析 │
│ 写共享内存 │
└──────────┬───────────┘
│ QLocalSocket 小通知
▼
┌──────────────────────┐
│ MainUI.exe │
│ │
│ 收到 newData 通知 │
│ 读共享内存 │
│ 刷新界面 │
└──────────────────────┘
共享内存:
┌─────────────────────────────────┐
│ Header │
│ RingBuffer<TargetRecord> │
└─────────────────────────────────┘
这种设计的优点:
text
高频数据不压垮 socket
界面进程只读需要的数据
支持多个读取进程
写入进程和读取进程解耦
性能比纯 socket 方案更好
十五、总结
QSharedMemory + QSystemSemaphore 是 Qt 中实现高性能本机进程间通信的重要方案。
核心结论:
text
1. QSharedMemory 负责共享数据
2. QSystemSemaphore 负责跨进程同步
3. QReadWriteLock 不能直接跨进程用
4. 多读单写需要自己基于 QSystemSemaphore 封装
5. 固定结构体 + 环形缓冲区适合工业实时数据
6. QLocalSocket 适合配合共享内存发送通知
7. 共享内存不是数据库,不适合长期存储
8. 真正零拷贝要求直接在共享内存结构上读写
对于工业项目,推荐组合是:
text
QSharedMemory:
存放实时大数据
CrossProcessRwLock:
提供跨进程多读单写保护
SharedMemoryBlock:
提供单例式共享内存访问
QLocalSocket:
发送小型 JSON 通知和控制命令
最终架构可以概括为:
text
大数据走共享内存
同步靠系统信号量
通知走 QLocalSocket
历史归档走文件或数据库
这样既能保证性能,又能保持工程结构清晰。