Qt多进程(七)内存映射

前言

本节我想介绍一种基于文件的内存映射(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)都能互通。

无内核资源泄漏风险:文件由文件系统管理,重启自动清理。

相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00614 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术14 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript