#ifndef CMem_H #define CMem_H #include "GdCPP_Exports.h" #include #include #include /** * @addtogroup Memory ** @brief ** @details * @{ * */ /// /// \brief 内存管理的基类,实现:
/// /// 需要了解的基本概念: /// - 物理内存:内存条 + 内存交换文件 /// - 虚拟内存:应用程序认为它拥有连续的可用的内存,win10 64位有128T /// - 内存交换文件paging file: 内存不够用了,系统将最近没使用的内存保存到文件中,用到再交换回来。 /// - page size:系统管理内存的单元,x86、x64系统使用4-KB, IA-64用8-KB. /// - 内存分配粒度:windows从虚拟内存分配一个range时的最小单位,win10是64k /// - large page: 大内存的应用为优化内存访问以比page大的多的单位使用物理内存(好像是2M). /// 刚开机的时候分配没问题,电脑运行一段时间就没有足够连续的物理内存来分配了。 /// /// 需要了解的windows内存操作:reserve/commit/decommit/release/lock/unlock /// /// 参考资料: /// - 《windows核心编程》第五版 /// - [windows memory management](https://docs.microsoft.com/en-us/windows/win32/memory/memory-management)
/// - [windows memory api](https://docs.microsoft.com/en-us/windows/win32/api/memoryapi)
/// /// \brief CMem 定义了一块内存,包括逻辑、物理两个方面。 /// - 内存的逻辑属性:包括内存地址、可用内存大小、外部容器大小。 /// - 内存的物理方法:包括内存如何分配、释放、调整大小的函数接口。 /// CMem作为所有内存的基类,物理层定义了一个在现有内存中取一部分使用。 class GDCPP_API CMem { public: virtual ~CMem() { //dealloc(); // 基类析构没必要清空属性变量了 }; // ----------- 逻辑内存 ----------- /// \brief 内存起始地址。可以引用,但不要直接更改,通过alloc()、dealloc()来修改 uint8_t* Addr = 0; /// \brief 可用内存大小 size_t memSize = 0; /// 容器大小,containerSize>=memSize /// 不同子类有不同的容器。 /// - 基类容器就是已经分配好的一块内存 /// - CWinMem的容器是AllocVirtual函数分配的虚拟内存空间 size_t containerSize = 0; bool selfAlloc = false; // 是否自己分配的内存,如果是,析构时需要释放 // ----------- 物理内存:分配、释放、调整大小 ----------- /// @brief 基类在指定容器中分配内存。子类需要重载。 /// @param addr 容器所在地址 /// @param container 容器大小 /// @param size 实际使用大小。 /// @return 是否分配成功 virtual bool alloc(void* addr, size_t container, size_t size) { // 注意Addr当前不知道时可以赋0,container不能为0 ASSERT(container != 0); ASSERT(size <= container); Addr = static_cast(addr); containerSize = container; memSize = size; selfAlloc = false; return true; } /// @brief 如果子类没有重载,默认调用alloc(nullptr, container, size) virtual bool alloc(size_t container, size_t size) { return alloc(nullptr, container, size); } /// @brief 如果子类没有重载,默认调用alloc(addr, size, size),而不是alloc(addr, size, 0) virtual bool alloc(void* addr, size_t size) { return alloc(addr, size, size); }; /// @brief 如果子类没有重载,默认调用alloc(nullptr, size, size),而不是alloc(nullptr, size, 0) virtual bool alloc(size_t size) { return alloc(nullptr, size, size); } /// \brief 基类不自动释放内存。子类需要重载实现释放内存。 virtual void dealloc() { Addr = nullptr; memSize = 0; containerSize = 0; //如果不想containerSize清0,使用resize()函数 selfAlloc = false; } // 下面两个函数调整内存大小,区别在于如果原先的内存有多的,一个释放掉,一个不释放只减小memSize。 /// 在容器范围内调整可用内存的大小,多余的释放。 /// 如果containerSize不够时,基类只返回false /// 子类需要重载,决定是重新申请,还是返回false。 virtual bool resize(size_t size) { if (containerSize == 0) //不知道容器大小 return false; if (size <= containerSize) { memSize = size; return true; } else { return false; } }; /// 在容器范围内调整可用内存的大小,多余的不释放只减小memSize,不释放。 /// 如果containerSize不够时,基类只返回false /// 子类需要重载,决定是重新申请,还是返回false。 virtual bool reserve(size_t size) { if (containerSize == 0) //不知道容器大小 return false; if (size <= containerSize) { memSize = size; return true; } else { return false; } } /// \brief 从操作系统的的虚拟空间分配内存,不分配物理内存 /// \param addr 分配到指定地址。如果不指定地址,请用后面的同名函数更方便,并增加确认内存之前是空的。 /// \param size 空间大小,会保存到PhysicSize,并设置freeFunc指针为freeInVirtual /// \param commit 是否立即分配物理内存 virtual bool _allocVirtual(void* addr, size_t size, bool commit); static uint8_t* allocVirtual(size_t size); static void freeVirtual(uint8_t* addr); protected: /// 有些虚拟内存空间需要临时释放,再分拆、合并。 /// 为避时释放时被别的线程占了,采用以下两个措施:
/// - 需要分拆的内存规定地址空间从后向前分配。
/// - 临时释放前加锁,重新占用后解锁 static std::mutex uplocker; /// \brief 从操作系统的的虚拟空间分配内存,不分配物理内存 /// \param addr 分配到指定地址。如果不指定地址,请用后面的同名函数更方便,并增加确认内存之前是空的。 /// \param size 空间大小,会保存到PhysicSize,并设置freeFunc指针为freeInVirtual /// \param commit 是否立即分配物理内存 virtual bool _allocUpperVirtual(size_t size, bool commit); }; /// 统计内存使用的接口类。需要统计内存使用情况的子类继承此类。 /// 内存统计功能没有合并到CMem的原因: /// 内存的分配、释放时必须功能,而内存统计是个辅助功能。 /// 基类是在现有内存中划一块使用,是不需要统计内存使用量,使其更轻量一点。 class GDCPP_API CMemUsage : virtual public CMem // 虚继承CMem,这样在统计表中访问CMemUsage指针,就可以获取其Addr、containerSize和memSize { public: CMemUsage() { // 构造时就自动添加到统计表,但未命名 std::lock_guard guard(MemListLock); // 索引表加锁 MemList.push_back(this); } virtual ~CMemUsage() { // 析构时从统计表中删除 std::lock_guard guard(MemListLock); // 加锁 for (auto it = MemList.begin(); it != MemList.end(); it++) { if (*it == this) { MemList.erase(it); return ; } } } // 不允许复制构造 CMemUsage(const CMemUsage&) = delete; /// @brief 被统计的内存块需要起一个名称。构造时为空,需要用户层指定名称,最好不要重名,可以是中文。 std::wstring memName; /// @brief 内存类型名称,通常在子类构造时设定,方便分类统计。 std::wstring memType; /// @brief 统计已经分配的内存ContainerSize总和,调用updateUsage()、report()时更新最新值 static size_t totalContainerSize; /// @brief 统计已经分配的内存memSize总和,调用updateUsage()、report()时更新最新值 static size_t totalMemSize; /// @brief 分配的同时命名 // virtual bool allocNamed(const wchar_t* name, size_t container, size_t size) = 0; virtual bool allocNamed(const std::wstring& name, size_t container, size_t size) = 0; protected: /// @brief 所有已经内配内存索引表,用于统计所有分配的内存 static std::list MemList; /// @brief 索引表添加、删除时加锁 static std::mutex MemListLock; /// @brief 更新统计值。内部调用,未加锁 static void _updateUsage() { totalContainerSize = 0; totalMemSize = 0; for (auto mem : MemList) { totalContainerSize += mem->containerSize; totalMemSize += mem->memSize; } } public: /// @brief 更新统计值。外部调用,加锁 static void updateUsage() { std::lock_guard guard(MemListLock); // 加锁 _updateUsage(); } /// @brief 输出报表到str static void report(std::wstring& str); virtual std::wstring details() { return std::wstring(); } }; class GDCPP_API CWinMem : virtual public CMemUsage // 继承内存统计接口 { public: CWinMem() { memType = L"Win"; // 标记内存内存类型 } ~CWinMem() override { dealloc(); } using CMem::alloc; // 防止基类的同名函数被子类掩盖 /// @brief 分配虚拟内存 /// \param addr 可以为0,由win自动分配一个对齐64k的地址;可以为指定地址,但必须是未分配的。 /// \param container 虚拟内存大小,必须大于0。内部会对齐到64k /// \param size 提交的物理内存大小。size <= container /// 可以等于0,之后再用reserve()、resize()提交物理内存。 /// \return 是否分配成功。 virtual bool alloc(void* addr, size_t container, size_t size) override; /// @brief 分配虚拟内存成功后给它命名为name virtual bool allocNamed(const std::wstring& name, size_t container, size_t size) override { const bool ret = alloc(nullptr, container, size); if (ret) memName = name; return ret; } /// @brief 特殊应用场景从虚拟内存的高端地址往下分配 bool allocUpper(size_t container, size_t size); virtual bool resize(size_t size) override; virtual bool reserve(size_t size) override; virtual void dealloc() override; std::wstring details() override; }; /// 一段内存映射到一个内存映射文件,当访问超出文件顶部时,自动翻转到文件底部;超出底部时自动翻转到顶部。 /// ///
/// 如下图所示:
///      内存               映像文件
/// -------------
/// |      2     |
/// ==============  ---  ==============
/// |      1     |       |      1     |
/// --------------       --------------
/// |      2     |       |      2     |
/// ==============  ---  ==============
/// |      1     |
/// --------------
/// 
class GDCPP_API CFileMapMem2 : virtual public CMemUsage { public: CFileMapMem2() { memType = L"FileMap"; // 标记内存内存类型 } ~CFileMapMem2() { dealloc(); } using CMem::alloc; // 防止基类的同名函数被子类掩盖 /// @brief 执行了以下操作: /// - 分配2*container大小的虚拟空间,未分配物理内存。 /// - 创建了内存映射文件,但未映射到内存 /// @param name 名字保存到memName,同时加了一个前缀用于创建内存映射文件 /// @param container 内存映射文件的大小。 /// @param size 本类此参数为0 /// @return virtual bool allocNamed(const std::wstring& name, size_t container, size_t size) override final; virtual void dealloc() override final; /// @brief 调整大小,必须在0和containerSize两个之间调整,不支持其它数值。 virtual bool resize(size_t size) override final; /// @brief 调整大小,必须在0和containerSize两个之间调整,不支持其它数值。 virtual bool reserve(size_t size) override final { return resize(size); }; /// @brief 本类必须使用带名称的分配函数,不带名字的直接返回false bool alloc(void* addr, size_t container, size_t size) override final { return false; } protected: // 本类自己的Addr、containerSize、memSize保存供外部访问的地址空间 // 以下3个成员保存整个内存空间、顶部、底部只读访问的映射空间 CMem allMem; // 保存整个虚拟内存空间 CMem TopMem; // 地址空间在顶部,映射到文件的底部 CMem BottomMem; // 地址空间在底部,映射到文件的顶部 /// 内存镜像文件的句柄 HANDLE hand=nullptr; /// 共享文件的名称 std::wstring _nativeKey; /// 出错码 int _error; /// 出错信息 std::wstring _errorString; bool mapFile(); void unmapFile(bool realloc); public: /// @brief 验证内存映射是否正确 /// @return bool verifyMap(bool compare_content=false); std::wstring details() override; }; /** * @} //group */ #endif // CMem_H