QT基于mmap文件映射机制实现的内存池方法总结

发布于:2025-02-16 ⋅ 阅读:(29) ⋅ 点赞:(0)

在现代计算机系统中,高效的内存管理对于程序性能有着至关重要的影响。尤其是在处理大量数据或频繁分配和释放小块内存的应用场景下,传统的内存分配方式(如malloc和free)可能会导致显著的性能开销和内存碎片化问题。为了克服这些问题,开发者们常常会设计并实现自定义的内存池(Memory Pool),以优化内存分配策略,提升应用程序的响应速度和稳定性。

本篇文章将聚焦于一种特殊的内存池实现——基于mmap文件映射机制的内存池。通过使用操作系统提供的mmap接口,我们可以将文件或者匿名段映射到进程的地址空间,从而获得一块连续的虚拟内存区域。这种方法不仅能够避免传统堆分配带来的碎片化问题,而且还可以利用文件系统的缓存机制来提高I/O效率,特别适合需要高效管理和访问大块内存的应用。

mmap文件映射机制介绍

mmap(Memory-Mapped File I/O)是一种在Unix-like操作系统(包括Linux和macOS)以及Windows中用于将文件或设备映射到进程的地址空间的技术。它允许程序直接对内存进行读写操作,而不是通过传统的I/O系统调用如read和write来访问文件内容。这种方式可以简化编程模型,同时提供更高效的I/O性能。
mmap文件映射机制

工作原理

当使用mmap时,操作系统会为应用程序分配一块虚拟内存区域,并将其与磁盘上的文件关联起来。这块内存看起来就像是一个普通的内存数组,但实际上它的内容是直接从文件加载进来的。任何对这块内存的修改都会反映到对应的文件上,或者根据具体的映射选项,在适当的时候同步回文件。

匿名映射:不与任何文件关联,通常用于创建共享内存段。
文件映射:将文件的内容映射到内存,使得文件的一部分或全部可以像内存一样被访问。

主要优点

提高I/O效率:减少了用户态和内核态之间的数据拷贝次数,因为数据可以直接在页缓存和应用之间传递。
简化编程接口:程序员可以通过指针直接访问文件内容,无需显式的读写操作。
支持大文件处理:对于非常大的文件,不需要一次性将整个文件载入内存,而是按需加载页面。
并发性:多个进程可以通过映射相同的文件来实现内存共享,从而方便地进行进程间通信。

代码实现

请注意,为了保持文章的简洁性和重点突出,文中仅粘贴了部分核心代码,完整实现请参阅项目的源代码库。这些代码片段旨在帮助理解内存池的工作原理和技术要点,而完整的功能和错误处理逻辑则包含在未展示的代码中。

初始化

在实现基于mmap的文件映射技术时,初始化阶段是至关重要的。它为后续的内存访问操作奠定了基础,并确保了系统资源能够被安全有效地使用。首先,需要选择一个合适的文件作为映射的目标,这个文件既可以是现有的,也可以是为了映射而特别创建的新文件。对于现有文件,程序必须以适当的权限打开该文件,以便执行读取、写入或同时进行这两种操作。接下来,调用操作系统提供的mmap函数(或其等价物),通过提供必要的参数如映射大小、保护标志、映射类型以及文件描述符来建立文件与进程地址空间之间的映射关系。成功的初始化不仅使得文件内容如同常规内存一样可直接访问,而且优化了I/O性能,减少了数据复制次数。

void MemoryPool::initializePool(size_t memSize_mb)
{
    //内存映射
    size_t memSize_bit = memSize_mb * 1024 * 1024;
    assert(std::numeric_limits<decltype(MemBlock::blockIndex)>::max() * 4 > memSize_bit && "over MaxMemSize!");

    QString cachePath = QStandardPaths::standardLocations(QStandardPaths::TempLocation).first();
    QDir dir(cachePath);
    if(!dir.exists())
        dir.mkpath(cachePath);

    QString fileUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
    m_virtualFile.reset(new QFile(QString("%1/%2.tmp").arg(cachePath, fileUuid)));
    if(m_virtualFile->open(QIODevice::ReadWrite | QIODevice::Truncate)){
        m_virtualFile->resize(memSize_bit);
        m_memoryPtr = m_virtualFile->map(0, memSize_bit, QFile::NoOptions);
        if(m_memoryPtr == nullptr)
        {
            qDebug() << "fileMap failed:" << m_virtualFile->error() << "reason:" << m_virtualFile->errorString();
            assert(false);
        }

        m_blockCount = static_cast<size_t>(std::floor((double)memSize_mb * 1024.0 * 1024.0 / double(BLOCK_SIZE)));
    } else {
        qDebug() << "file open failed" << m_virtualFile->error() << "reason:" << m_virtualFile->errorString();
        assert(false);
    }
}

申请内存

内存分配是通过allocate成员函数实现的,它遵循一种紧凑的内存布局策略,旨在最小化碎片并最大化连续可用内存。当请求一定数量的字节时,allocate会计算所需的块数目,并尝试找到一段足够大的连续空闲区域来满足请求。算法首先检查是否可以在当前已使用的最高地址之后添加新的分配,如果不行,则遍历已使用的块列表,寻找可以插入新分配的间隙。一旦确定了合适的起始位置,就更新内部数据结构(如m_useBlocks),记录下新分配的块信息,并返回指向这块内存的指针。这种分配机制不仅保证了高效利用内存资源,而且还支持灵活的内存管理需求,例如动态增长的应用场景。

void* MemoryPool::allocate(size_t memSize_bit) 
{
    size_t takeBlockSize = static_cast<size_t>(std::ceil(memSize_bit / double(BLOCK_SIZE)));
    //判断是否还有适用的空闲内存
    size_t allocIndex = -1;
    if(!m_useBlocks.empty()) {
        const MemBlock &useBlock = *m_useBlocks.rbegin();
        if(m_blockCount - useBlock.blockIndex + useBlock.blockSize > takeBlockSize) { //TODO >=
            allocIndex = useBlock.blockIndex + useBlock.blockSize;
        } else {
            //从碎片中找
            size_t startIndex = 0;
            for(auto iter = m_useBlocks.begin(); iter != m_useBlocks.end(); ++iter) {
                size_t freeBlockSize = iter->blockIndex - startIndex;
                if(freeBlockSize >= takeBlockSize) {
                    allocIndex = startIndex;
                    break;
                }
                startIndex += iter->blockIndex + iter->blockSize;
            }
        }
    } else {
        if(m_blockCount >= takeBlockSize)
            allocIndex = 0;
    }

    assert(allocIndex >= 0);
    assert(allocIndex < m_blockCount);

    //记录
    m_useBlocks.emplace(MemBlock(allocIndex, takeBlockSize));

    return getMemPtr(allocIndex);
}

回收申请的内存

内存的释放由deallocate成员函数处理,它是内存池管理中的重要组成部分,负责回收不再需要的内存块,以便它们可以被重新分配给未来的请求。当接收到一个指向先前分配内存的指针时,deallocate会根据该指针计算出对应的块索引,并从记录已使用块的数据结构中移除相应的条目。这样做既恢复了内存池的状态,也使得这些内存块再次变为可分配状态。此外,cllocate成员函数提供了一种更激进的方式清空整个内存池,即清除所有当前使用的块,使内存池回到刚初始化后的状态。正确的内存释放逻辑是防止内存泄漏的关键,同时也有助于维持应用程序的稳定性和响应速度。

void MemoryPool::deallocate(void *ptr) 
{
    int index = getSizeIndex(ptr);
    if(index < 0)
        return;

    MemBlock block(index, 0);
    auto iter = m_useBlocks.find(block);
    assert(iter != m_useBlocks.end());
    m_useBlocks.erase(iter);
}

后续扩展

1.利用自由链表和位图等数据结构来追踪空闲内存块的状态。这将有助于减少内存碎片并提高分配和释放操作的效率。
2.通过锁优化、无锁数据结构或分段锁等技术,减少多线程环境下的竞争开销,从而提升并发性能。
3.利用动态页扩展算法、预分配与懒加载、内存回收与重用机制等,升级为动态扩展页的方式,以无限扩大可用内存
4.基于此内存池、模板、对象池设计原则等技术,开发对象池,解决某些高频发构造的对象导致性能降低的问题场景

总结

通过基于mmap文件映射机制的内存池实现,我们显著优化了内存分配效率并减少了碎片化问题,提升了应用程序的性能和稳定性,解决了大数据量占用内存量高的问题。


网站公告

今日签到

点亮在社区的每一天
去签到