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)都能互通。

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

相关推荐
七夜zippoe41 分钟前
异步编程实战:构建高性能Python网络应用
开发语言·python·websocket·asyncio·aiohttp
tianyuanwo42 分钟前
Python虚拟环境深度解析:从virtualenv到virtualenvwrapper
开发语言·python·virtualenv
看见繁华1 小时前
GO 教程
开发语言·后端·golang
Yy_Yyyyy_zz1 小时前
深入理解 Go 的多返回值:语法、编译原理与工程实践
开发语言·后端·golang
AAA.建材批发刘哥1 小时前
02--C++ 类和对象上篇
开发语言·c++
廋到被风吹走1 小时前
【Java】【JVM】垃圾回收深度解析:G1/ZGC/Shenandoah原理、日志分析与STW优化
java·开发语言·jvm
xrkhy1 小时前
Java全栈面试题及答案汇总(3)
java·开发语言·面试
菩提祖师_1 小时前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
刘97531 小时前
【第22天】22c#今日小结
开发语言·c#
赵民勇1 小时前
Qt项目缺少Quick模块错误解决方案
linux·qt