前言
本节我想介绍一种基于文件的内存映射(Memory-Mapped File),它是一种非常实用且跨平台友好的进程间通信(IPC)方式。虽然它在性能上略逊于原生共享内存(QSharedMemory),但在健壮性、可调试性和兼容性方面有显著优势。
一、内存映射
核心思想:
1.将一个磁盘文件的内容映射到进程的虚拟地址空间。
2.对映射区域的读写操作,直接反映到文件中(由操作系统负责同步)。
3.多个进程映射同一个文件,即可实现共享数据------这就是一种 IPC 机制。
你可以把它理解为:
"用文件作为中介的共享内存" ------ 数据存在文件里,但访问像内存一样快。
二、Qt中的实现:QFile::map()
Qt 通过 QFile 类提供了跨平台的内存映射支持:
1.file.open(QFile::ReadWrite),打开文件(必须先打开)
2.file.map(offset, size),返回 uchar* 指针,指向映射区域
3.file.unmap(ptr),解除映射
4.file.resize(size),确保文件足够大(映射大小不能超过文件实际大小)
file类调用的接口就是这些,得到的uchar*指针指向的就是文件的虚拟内存区域,直接修改可以复写在文件中。打开的时候需要文件载体,这是一个实际的文件,可以命名后缀为dat文件。针对进程通信,我们也可以有一些数据格式的设计,比方说协议头等。
另外,读取完数据之后,可以按需进行清除,比方说主动清零整个映射区。这个在后续代码中有体现。
三、代码示例
新增界面测试类:
cpp
#ifndef MEMORYMAPWINDOW_H
#define MEMORYMAPWINDOW_H
#include <QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTimer>
#include <QDateTime>
#include <QFile>
#include <QDir>
class MemoryMapWindow : public QWidget
{
Q_OBJECT
public:
explicit MemoryMapWindow(const QString &role, QWidget *parent = nullptr);
private slots:
void onSendMessage();
void onReadMessage();
void onTimerTimeout();
private:
void appendLog(const QString &msg);
void setupMemoryMap();
QString m_role;
QTextEdit *m_logView;
QLineEdit *m_inputEdit;
QPushButton *m_sendButton;
QPushButton *m_readButton;
QTimer *m_timer;
QString m_mapFilePath;
QFile *m_mapFile;
};
#endif // MEMORYMAPWINDOW_H
cpp
#include "memorymapwindow.h"
#include <QLabel>
MemoryMapWindow::MemoryMapWindow(const QString &role, QWidget *parent)
: QWidget(parent)
, m_role(role)
, m_timer(nullptr)
, m_mapFile(nullptr)
, m_mapFilePath(QDir::tempPath() + "/ipc_test_mmap.dat")
{
setWindowTitle("Memory Map - " + 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 Memory Map");
m_readButton = new QPushButton("Read from Memory Map");
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, &MemoryMapWindow::onSendMessage);
connect(m_readButton, &QPushButton::clicked, this, &MemoryMapWindow::onReadMessage);
setupMemoryMap();
if (m_role == "Server") {
// Server periodically checks for messages
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &MemoryMapWindow::onTimerTimeout);
m_timer->start(1000); // Check every second
}
appendLog("Memory Map " + m_role + " initialized at: " + m_mapFilePath);
}
void MemoryMapWindow::setupMemoryMap()
{
// Ensure file exists with sufficient size
QFile file(m_mapFilePath);
if (!file.exists()) {
if (file.open(QFile::WriteOnly)) {
file.resize(1024); // Create with 1KB size
file.close();
}
}
}
void MemoryMapWindow::onSendMessage()
{
QString msg = m_inputEdit->text();
if (msg.isEmpty()) return;
QFile file(m_mapFilePath);
if (!file.open(QFile::ReadWrite)) {
appendLog("Failed to open memory map file: " + file.errorString());
return;
}
if (file.size() < 1024) {
file.resize(1024);
}
uchar *mapped = file.map(0, 1024);
if (!mapped) {
appendLog("Failed to map memory");
file.close();
return;
}
// Clear the area first
memset(mapped, 0, 1024);
// Write magic header and message
QByteArray data = msg.toUtf8();
if (data.size() > 1020) data.truncate(1020);
memcpy(mapped, "IPC!", 4); // Magic header
memcpy(mapped + 4, data.constData(), data.size());
file.unmap(mapped);
file.close();
appendLog("Wrote to memory map: " + msg);
m_inputEdit->clear();
}
void MemoryMapWindow::onReadMessage()
{
QFile file(m_mapFilePath);
if (!file.exists()) {
appendLog("Memory map file does not exist");
return;
}
if (!file.open(QFile::ReadOnly) || file.size() < 1024) {
appendLog("Memory map file is too small or cannot be opened");
return;
}
uchar *mapped = file.map(0, 1024);
if (!mapped) {
appendLog("Failed to map memory for reading");
file.close();
return;
}
// Check magic header
if (memcmp(mapped, "IPC!", 4) != 0) {
appendLog("No valid data in memory map (magic mismatch)");
file.unmap(mapped);
file.close();
return;
}
// Read message starting from offset 4
QByteArray data((char*)(mapped + 4), 1020);
int nullPos = data.indexOf('\0');
if (nullPos != -1) {
data.truncate(nullPos);
}
file.unmap(mapped);
file.close();
appendLog("Read from memory map: " + QString::fromUtf8(data));
}
void MemoryMapWindow::onTimerTimeout()
{
QFile file(m_mapFilePath);
if (!file.exists() || file.size() < 1024) return;
uchar *mapped = file.map(0, 1024);
if (!mapped) return;
// Check magic header
if (memcmp(mapped, "IPC!", 4) == 0) {
// Read message starting from offset 4
QByteArray data((char*)(mapped + 4), 1020);
int nullPos = data.indexOf('\0');
if (nullPos != -1) {
data.truncate(nullPos);
}
// Only report if there's actual content
if (!data.isEmpty()) {
appendLog("Server received: " + QString::fromUtf8(data));
// Clear the message after reading
memset(mapped, 0, 1024);
}
}
file.unmap(mapped);
file.close();
}
void MemoryMapWindow::appendLog(const QString &msg)
{
m_logView->append(QDateTime::currentDateTime().toString("hh:mm:ss") + " | " + msg);
}
运行效果:

四、总结
可以看到,本质上这种方式就是通过一个文件作为媒介,得到一个多进程都可以直接操作的内存块指针。使用上其实需要注意,比方说文件是否存在,通信过程中文件是否会被删除。而且涉及到多端使用会产生竟态的问题,可以像上一节一样使用信号量来管理写入读取的时机。
内存映射这种方法,和共享内存还是有点像的,以下进行一些对比总结:

现代操作系统会将频繁访问的映射文件缓存在内存(Page Cache)中,所以实际 I/O 很少,性能接近共享内存,尤其在本地 SSD 上几乎无感。
优点:
简单可靠:无需处理共享内存的"创建/附加"复杂逻辑。
天然持久化:进程崩溃后,数据仍在文件中(可用于恢复)。
易于调试:直接 cat /tmp/ipc_test_mmap.dat 查看内容。
跨语言兼容:任何能读写文件+内存映射的语言(Python/C#/Rust)都能互通。
无内核资源泄漏风险:文件由文件系统管理,重启自动清理。