371 lines
13 KiB
C++
371 lines
13 KiB
C++
#ifndef CMem_H
|
||
#define CMem_H
|
||
|
||
#include "GdCPP_Exports.h"
|
||
#include <stdint.h>
|
||
#include <mutex>
|
||
#include <map>
|
||
|
||
/**
|
||
* @addtogroup Memory
|
||
** @brief
|
||
** @details
|
||
* @{
|
||
*
|
||
*/
|
||
///
|
||
/// \brief 内存管理的基类,实现:<br>
|
||
///
|
||
/// 需要了解的基本概念:
|
||
/// - 物理内存:内存条 + 内存交换文件
|
||
/// - 虚拟内存:应用程序认为它拥有连续的可用的内存,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)<br>
|
||
/// - [windows memory api](https://docs.microsoft.com/en-us/windows/win32/api/memoryapi)<br>
|
||
/// </pre>
|
||
|
||
|
||
/// \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<uint8_t*>(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:
|
||
/// 有些虚拟内存空间需要临时释放,再分拆、合并。
|
||
/// 为避时释放时被别的线程占了,采用以下两个措施:<br>
|
||
/// - 需要分拆的内存规定地址空间从后向前分配。<br>
|
||
/// - 临时释放前加锁,重新占用后解锁
|
||
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<CMemUsage*> 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;
|
||
};
|
||
|
||
/// 一段内存映射到一个内存映射文件,当访问超出文件顶部时,自动翻转到文件底部;超出底部时自动翻转到顶部。
|
||
///
|
||
/// <pre>
|
||
/// 如下图所示:
|
||
/// 内存 映像文件
|
||
/// -------------
|
||
/// | 2 |
|
||
/// ============== --- ==============
|
||
/// | 1 | | 1 |
|
||
/// -------------- --------------
|
||
/// | 2 | | 2 |
|
||
/// ============== --- ==============
|
||
/// | 1 |
|
||
/// --------------
|
||
/// </pre>
|
||
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
|