游戏服务器引擎的设计(五)内存管理

C++的程序都会对内存管理比较敏感,经常会发生内存泄漏的bug,同时为了内存的快速申请和释放,以及减少小块内存碎片,故大多都会有内存管理模块。

我在自己的服务器框架内加了内存管理,基本上是STL库的那一套,对小于128个字节的内存在内存池中管理申请和释放,而大于128的调用系统的内存申请及释放, 只不过在内部添加内存记录模块,当开启DEBUG标记的时候,会记录当前内存池总大小,已使用大小,和剩余大小。 需要检测或者查找申请记录的时候可以把当前内存情况输出到文件中。

// 这里简单讲下原理实现:

内存池中管理着128个字节以下的内存申请,如果大于128个字节的内存申请直接调用系统的malloc,如果小于128个字节就在内存池中申请。而内存池中维护着16个链表,每个链表里是已经申请分配好的内存块,他们的颗粒度是8字节,比如第一个链表里全部是8个字节大小的内存块,而第二个链表里都是16个字节的内存块,第三个链表里都是24个字节的内存块,以此类推直到128个字节就是第十六个链表。当业务层面要申请字节大小为1到8个的内存时,都会在第一个链表中把已经预分配好的内存节点取出来返回给业务层。 要申请大小为9到16的内存时就把第二个链表中取出一个节点分配出去。 而如果业务层释放内存的时候,就按照对应的大小插入的相应的链表中。如果链表空了,就从预先申请的一块大的内存中,再次分配N个相应大小的内存块插入到链表中。如果预先申请的大内存也分配完了,就再次调用系统的malloc 申请一块大内存。 所有的内存节点都是在这个大的内存块上分配的。这样避免了小块的内存碎片。

而DEBUG 模式开启的话,会在申请内存的时候记录申请的所在的文件,以及行号,用于排查问题,以及统计每个模块内存使用情况。

// 贴一下 :内存池头文件代码

// 8字节对齐(颗粒度)
	#define ALIGN 8

	// 在池中分配的最大大小
	#define MAX_SIZE 128

	// 内存池中类别个数 (MAX_SIZE / ALIGN)
	#define TYPE_COUNT 16

	// 8字节辅助偏移量
	#define ALIGN_OFFSET 3

	// 一次申请的内存大小
	#define ALLOC_MAX_SIZE 65536

	// 一次预分配的内存个数
	#define ONE_ALLOC_COUNT 32

	// 内存池
	class CMemoryPool
	{
	private:
		// 内存结点
		struct mem_node_t
		{
			union
			{
				mem_node_t* next;
				void* data;
			};
		};

		// 内存桶节点
		struct mem_chunk_t
		{
			mem_chunk_t* next;
			size_t size;
		};

		// 内存监控节点 监控内存使用情况
		struct mem_check_t
		{
		public:
			mem_check_t(void* p, size_t n, const char* name, int line):
				pMemory(p),nSize(n),nLine(line),fileName(name) {}
			mem_check_t(const mem_check_t& cpy) :
				pMemory(cpy.pMemory), nSize(cpy.nSize), nLine(cpy.nLine),
				fileName(cpy.fileName) {}
			void* pMemory;
			size_t nSize;
			int nLine;
			std::string fileName;
		};

		struct mem_check_t1
		{
			int useSize = 0;
			int allocCount = 0;
			int freeCount = 0;
		};

	public:
		CMemoryPool();
		virtual ~CMemoryPool();

	public:
		// 申请内存
		void* Alloc(size_t size);
		
		// 释放内存
		void Free(void* pData, size_t size);

		// 申请内存
		void* AllocDebug(size_t size, const char* fileName, int nLine);

		// 释放内存
		void FreeDebug(void* pData, size_t size);
	public:
		// 设置DEBUG 模式(开启内存监控)
		void SetDebug(bool isDebug);

		// 获取内存池总大小
		int GetMemoryPoolSize();

		// 获取当前剩余大小
		int GetMemoryRemianSize();

		// DEBUG模式下输出内存情况, 输出内存情况到日志文件
		void SaveDebugDump();

	private:
		// 预分配内存
		void refill(size_t size);

		// 从未分配内存中申请内存
		char* chunkalloc(int size, int& count);

		// itoa 实现
		char* itoa_(int val, char* buf, int size);
	private:

		// 预分配内存池队列
		mem_node_t* m_pPool[TYPE_COUNT];

		// 未分配内存块
		mem_chunk_t* m_pChunk;
		char* m_pBegin;
		char* m_pEnd;

		// 是否是debug 模式
		bool m_bDebug;

		// 内存监控列表
		std::map m_mapCheck;
		std::map  m_mapCountCheck;
		size_t check_size;
		size_t max_use_size;
		size_t max_use_count;
	};

// 当业务逻辑申请内存时调用:

// 申请内存
void* CMemoryPool::Alloc(size_t size)
{
	if (size == 0)
	{
		return (void*)"";
	}

    // 如果大于内存池管理的最大内存大小,直接调用系统内存分配
	if (size > MAX_SIZE)
	{
		void* p = malloc(size);
		if (!p)
		{
			// 申请内存失败
			printf("(CMemoryPool::Alloc)not enough memory, size:%lld", size);
			abort();
			return NULL;
		}

		return p;
	}

    // 根据申请的内存大小定位到内存池相应的内存列表
	mem_node_t** freeList = m_pPool + FREE_INDEX(size);
	if (!(*freeList))
	{
        // 当前链表中没有内存了,则向内部池中申请,扩充该大小的内存数量
		refill(size);
	}

    // 再次检查是否扩充的内存
	if (!(*freeList))
	{
		return NULL;
	}

    // 把链表第一个该大小的内存块取出来,
    // 返回给业务调用者
	mem_node_t* pNew = (*freeList);
	(*freeList) = (*freeList)->next;
	return (void*)pNew;
}

// 先对要申请的内存判定大小大于128的直接调用系统接口:

if (size > MAX_SIZE)
	{
		void* p = malloc(size);
		if (!p)
		{
			// 申请内存失败
			printf("(CMemoryPool::Alloc)no enough memory, size:%lld", size);
			abort();
			return NULL;
		}

		return p;
	}

// 然后对要申请的内存大小计算出所在链表的索引:

// 这里是宏定义
#define FREE_INDEX(bytes) ((bytes - size_t(1)) >> (int)ALIGN_OFFSET)
mem_node_t** freeList = m_pPool + FREE_INDEX(size);
	if (!(*freeList))
	{
		refill(size);
	}

// 如果当前链表空了,需要在内存池中未分配的大块内存中,分配N个对应大小的内存块加入到链表中

// 分配内存
void CMemoryPool::refill(size_t size)
{
	// 字节对齐
	size = (size + ((size_t)ALIGN) - 1) & ~(((size_t)ALIGN) - 1);
	
	// 预分配个数
	int nodeCount = ONE_ALLOC_COUNT;

	// 申请内存
	char* chunk = chunkalloc((int)size, nodeCount);
	if (!chunk)
	{
		return;
	}
	
	mem_node_t** pNodeList = m_pPool + FREE_INDEX(size);
	mem_node_t* curNode;
	for (int i = 0; i < nodeCount; ++i)
	{
		mem_node_t* pNewNodeList = (mem_node_t*)(chunk + (size * i));
		curNode = pNewNodeList;
		curNode->next = *pNodeList;
		*pNodeList = curNode;
	}

	return;
}

// chunkalloc 的实现:

// 从未分配内存中申请内存
char* CMemoryPool::chunkalloc(int size, int& count)
{
	// 申请总大小
	int maxSize = size * count;
	
	// 剩余大小
	int remainSize =(int)(m_pEnd - m_pBegin);
	
	// 判断内存是否充足
	if (remainSize >= maxSize)
	{
		char* p = m_pBegin;
		m_pBegin += maxSize;
		return p;
	}

	// 内存不足,但是检测是否够至少一个的空间的
	else if (remainSize >= size)
	{
		count = remainSize / size;
		int allocCount = count * size;
		char* p = m_pBegin;
		m_pBegin += allocCount;
		return p;
	}
	else
	{
		// 一个都没有,把剩余的空间,添加的空闲链表中,重新申请新内存分配
		if (remainSize > 0)
		{
			mem_node_t** pNodeList = m_pPool + FREE_INDEX(remainSize);
			mem_node_t* pNewFree = (mem_node_t*)m_pBegin;
			pNewFree->next = *pNodeList;
			*pNodeList = pNewFree;
			m_pBegin = m_pEnd = NULL;
		}

		// 重新申请新内存 ,每次申请固定大小64kb, 这个适用于总内存池
		//int newSize = maxSize * 2;
		int newSize = ALLOC_MAX_SIZE;
		char* pBuff = (char*)malloc(newSize);
		if (!pBuff)
		{
			printf("CMemoryPool::chunkalloc faild : no enough memory.\n");
			abort();
			return NULL;
		}
		mem_chunk_t* pChunk = (mem_chunk_t*)pBuff;
		m_pBegin = pBuff + sizeof(mem_chunk_t);
		m_pEnd = m_pBegin + ALLOC_MAX_SIZE - sizeof(mem_chunk_t);
		pChunk->size = ALLOC_MAX_SIZE - sizeof(mem_chunk_t);
		pChunk->next = m_pChunk;
		m_pChunk = pChunk;

		return chunkalloc(size, count);
	}
}

// 上面就是申请内存代码讲解, 下面贴一下DEBUG模式的申请函数实现,主要是添加了记录内存使用的代码段:

// 申请内存
void* CMemoryPool::AllocDebug(size_t size, const char* fileName, int nLine)
{
    // 走正常申请流程
	void* pData = Alloc(size);
	if (!pData)
	{
		return NULL;
	}

    // DEBUG模式记录了每一次申请内存的文件名及行号
	if (m_bDebug && fileName && fileName[0] != '\0')
	{
		char* file1 = strrchr(const_cast(fileName), FILE_MARK);
		char buf[32] = { '\0' };
		std::string name(file1 ? (file1 + 1) : fileName);
		name += ":";
		name += itoa_(nLine, buf, 10);
		max_use_size += (int)size;
		m_mapCheck.insert(std::make_pair((long long)pData, mem_check_t(pData, size, name.c_str(), nLine)));
		auto& p = m_mapCountCheck[name];
		p.allocCount += 1;
		p.useSize += (int)size;
	}

	return pData;
}

// 释放内存:

// 释放内存
void CMemoryPool::Free(void* pData, size_t size)
{
	if (size == 0 || !pData)
	{
		printf("CMemoryPool::Free faild : size = %lld pData = %lld\n", size, (long long)pData);
		return ;
	}

	if (size > MAX_SIZE)
	{
		free(pData);
		return;
	}

	mem_node_t** freeList = m_pPool + FREE_INDEX(size);
	mem_node_t* pData1 = (mem_node_t*)pData;
	pData1->next = *freeList;
	*freeList = pData1;
	return;
}

// debug 模式释放内存: 添加了删除申请记录的代码段

// 释放内存
void CMemoryPool::FreeDebug(void* pData, size_t size)
{
	Free(pData, size);

	if (!m_bDebug)
	{
		return;
	}
    
    // 这里删除申请记录
	max_use_size -= size;
	std::string fileName = "";
	auto iter = m_mapCheck.find((long long)pData);
	if (iter != m_mapCheck.end())
	{
		fileName = iter->second.fileName;
		m_mapCheck.erase(iter);
		m_mapCountCheck[fileName].freeCount += 1;
	}
}

// debug模式下打印内存使用情况:

// DEBUG模式下输出内存情况, 输出内存情况到日志文件
void CMemoryPool::SaveDebugDump()
{
	std::string fileName = "memory_pool_monitor.log";
	FILE* fp = fopen(fileName.c_str(), "wb");

	if (NULL == fp)
	{
		return;
	}

	fprintf(fp, "memory pool max size : %d, remain size  : %d use size : %lld\r\n\n",
		GetMemoryPoolSize(), GetMemoryRemianSize(), max_use_size);

	fprintf(fp, "===== memory pool use info: ========\r\n\n");
	for (auto iter = m_mapCountCheck.begin(); iter != m_mapCountCheck.end(); ++iter)
	{
		fprintf(fp, "%s -> alloc count : %d, free count : %d, now use size : %d \r\n",
			iter->first.c_str(), iter->second.allocCount, iter->second.freeCount, iter->second.useSize);
	}

	fprintf(fp, "\n======block point count : %lld =========\r\n\n", m_mapCheck.size());
	for (auto iter = m_mapCheck.begin(); iter != m_mapCheck.end(); ++iter)
	{

		fprintf(fp, "block pointer:%p size:%lld file: %s\r\n",
			iter->second.pMemory, iter->second.nSize, iter->second.fileName.c_str());
	}

	fclose(fp);

	return;
}

内存池的实现就基本到这里了,下面贴下对内存池封装的代码:

// 申请内存
	extern void* CoreDataAlloc(int size);
	// 释放内存
	extern void CoreDataFree(void* ptr, int size);
	// 申请内存
	extern void* CoreDataAllocDebug(int size, const char* fileName, int nLine);
	// 释放内存
	extern void CoreDataFreeDebug(void* ptr, int size);

	template 
	T* CoreDataNew(T* p)
	{
		T* ptr = (T*)CoreDataAlloc(sizeof(T));
		if (!ptr)
		{
			return ptr;
		}

		new (ptr) T();

		return ptr;
	}

	template 
	void CoreDataDelete(T* ptr)
	{
		if (!ptr)
		{
			return;
		}
		ptr->~T();

		CoreDataFree(ptr, sizeof(T));
	}

	template 
	T* CoreDataNewDebug(T* p, const char* fileName, int nLine)
	{
		T* ptr = (T*)CoreDataAllocDebug(sizeof(T), fileName, nLine);
		if (!ptr)
		{
			return ptr;
		}

		new (ptr) T();

		return ptr;
	}

	template 
	void CoreDataDeleteDebug(T* ptr)
	{
		if (!ptr)
		{
			return;
		}
		ptr->~T();

		CoreDataFreeDebug(ptr, sizeof(T));
	}

#ifdef _DEBUG
#define CORE_ALLOC(size) CoreData::CoreDataAllocDebug(size, __FILE__, __LINE__)
#define CORE_FREE(ptr, size) CoreData::CoreDataFreeDebug(ptr, size)
#define CORE_NEW(type) CoreData::CoreDataNewDebug((type*)0, __FILE__, __LINE__)
#define CORE_DELETE(ptr) CoreData::CoreDataDeleteDebug(ptr)
#else
#define CORE_ALLOC(size) CoreData::CoreDataAlloc(size)
#define CORE_FREE(ptr, size) CoreData::CoreDataFree(ptr, size)
#define CORE_NEW(type) CoreData::CoreDataNew((type*)0, __FILE__, __LINE__)
#define CORE_DELETE(ptr) CoreData::CoreDataDelete(ptr)
#endif

现在有很多的开源的内存管理的第三方库 比如jemalloc 等,我也有在用,大家没事都可以研究研究

你可能感兴趣的:(游戏开发,c++,游戏程序,服务器,架构,后端)