Select网络通信引擎:内存池

前面实现的服务器模型通过多个数据接收线程和多个数据发送线程能够实现同时与多个客户端进信息交互的目的。但是这个服务器还不能称为一个稳定的服务器,因为服务器再运行的过程中,涉及到大量的new和delete操作,比如产生一个回应报文,新增一个客户端。这些内存的申请和释放操作需要与操作系统的进行交互,而操作系统提供的这些服务稳定性并不是很好。因此考虑构建一个内存池,通过重载new和delete操作符,自己实现内存管理。

首先需要了解内存池的结构和运行机制。内存池实际上就是将多次申请小块的内存转变为一次申请大块内存,减少了申请内存的次数,从而提高了稳定性。本文实现内存池的主要思路就是将申请的到的一大块连续内存,分为等大小的小内存块,每次将这些小内存块分发出去满足用户的需求。
每个小的内存块又分为两个部分:控制头部和数据本体。控制头部包含的信息用于内存池的组织。为控制头部设计类MemoryBloack如下:

//内存池中的一个个内存块的控制头
class MemoryBloack//32字节的大小
{
public:
	//内存块编号
	int nID;
	//引用次数
	int nRef;
	//所属大内存块
	MemoryAlloc* pAlloc;
	//下一块位置
	MemoryBloack* pNext;
	//是否在内存池中
	bool bPool;

public:
	//预留
	char cNULL1;
	char cNULL2;
	char cNULL3;
};

nID代表该小内存块再整个内存池中的序号,该信息主要用于调试过程,实际并不需要。
nRef代表该内存块被分发出去了几次。若不存在内存共享,则该值为0或1。
pNext指向下一个空闲内存块的地址(注意是空闲内存块)
bPool代表该小内存块是否在最初分配的大内存块中(存在几种情况会在内存池之外进行内存的分配:1、内存池已满,不得不在内存池外申请地址。 2、申请的内存大小超出限制。因为内存池时被分割为一块块大小相同的小内存块,若某一次申请的内存大小超出该尺度,即一个小内存块放不下,就得去内存池外申请)

内存池MemoryAlloc就是将一个个小的内存块组织起来,为内存池设计类MemoryAlloc如下:

//内存池
class MemoryAlloc 
{
public:
	MemoryAlloc()
	{
		_pBuf = nullptr;
		_pHeader = nullptr;
		_BlockNum = 0;
		_BlockSize = 0;
	};
	~MemoryAlloc()
	{
		if (_pBuf) free(_pBuf); 
	};

	//申请内存
	void* allocMem(size_t nsize);
	//释放内存
	void freeMem(void* p);
	//初始化
	void InitMemory();
protected:
	//内存池的首地址:内存池申请的一大块内存所在位置
	char* _pBuf;
	//头部内存单元:指向第一个可用内存单元
	MemoryBloack* _pHeader;
	//内存单元的数量
	size_t  _BlockNum;
	//内存单元的大小
	size_t _BlockSize;
	//内存池中每一个块的真正大小
	size_t _realsize;
	//锁,防止在多线程情况下发生错误
	mutex _mutex;
};

首先分析MemoryAlloc中的变量:
_pBuf用来保存整个内存池(非常大的一块内存)的首地址。
_pHeader指向第一个空闲内存单元(小内存块)
_BlockNum表示内存池中小内存块的数量
_BlockSize表示内存池中每个小内存块的大小
_realsize表示小内存块的大小与控制头部大小的和(这才是内存单元的实际大小)
_mutex用于保护多线程情况下的安全

函数InitMemory用于初始化内存池,主要工作包括整个内存池这个一大块内存的申请和内存池中每个控制头部的初始化

void MemoryAlloc::InitMemory()
{
	_realsize = _BlockSize + sizeof(MemoryBloack);
	if (_pBuf)//已经被初始化
	{
		cout << "多次初始化内存块" << endl;
		return;
	}
	//为内存池申请内存
	_pBuf = (char*)malloc(_BlockNum * _realsize);

	//初始化内存池
	_pHeader = (MemoryBloack*)_pBuf;
	_pHeader->bPool = true;
	_pHeader->nID = 0;
	_pHeader->nRef = 0;
	_pHeader->pAlloc = this;
	_pHeader->pNext = nullptr;
	auto temp = _pHeader;
	for (size_t n = 1; n < _BlockNum; n++)
	{
		MemoryBloack* next = (MemoryBloack*)(_pBuf + n * _realsize);
		next->bPool = true;
		next->nID = n;
		next->nRef = 0;
		next->pAlloc = this;
		next->pNext = nullptr;
		temp->pNext = next;
		temp = next;
	}
}

函数allocMem给出一块内存,该块内存可能位于内存池中也可能位于内存池外。这里需要注意。返回给用户的地址不是控制头部的地址,而是数据本体的地址,因此再返回时需要偏移一个控制头部的长度。

void* MemoryAlloc::allocMem(size_t nsize)
{
	lock_guard<mutex> lock(_mutex);
	if (!_pBuf)
	{
		InitMemory();
	}
	MemoryBloack* pReturn = nullptr;
	//判断还有没有块可以用
	if (nullptr == _pHeader)//没有块可以用了
	{
		pReturn = (MemoryBloack*)malloc(nsize + sizeof(MemoryBloack));//用户需要的数据大小加上控制块的大小
		pReturn->bPool = false;
		pReturn->nID = -1;
		pReturn->nRef = 1;
		pReturn->pAlloc = this;
		pReturn->pNext = nullptr;
	}
	else
	{
		pReturn = _pHeader;
		_pHeader = _pHeader->pNext;
		if (pReturn->nRef == 1)
		{
			cout << "内存申请出错,该内存块已被占用" << endl;
			return pReturn;
		}
		pReturn->nRef = 1;//表示已经被分配
	}
	//cout << "location = " << pReturn << "  nID =  " << pReturn->nID << "   isPool: " << pReturn->bPool << "  nsize = " << nsize << endl;
	return ((char*)pReturn + sizeof(MemoryBloack));//返回的指针是偏移的
}

函数freeMem用于释放allocMem给出的内存。

void MemoryAlloc::freeMem(void* p)
{
	MemoryBloack* pBlock = (MemoryBloack*)((char*)p - sizeof(MemoryBloack));
	if (pBlock->bPool)//在内存池中
	{
		lock_guard<mutex> lock(_mutex);
		if (--pBlock->nRef != 0)
		{
			cout << "内存块多次引用" << endl;
			return;
		}
		pBlock->pNext = _pHeader;
		_pHeader = pBlock;
	}
	else//在内存池外
	{
		if (--pBlock->nRef != 0)
		{
			cout << "内存块多次引用" << endl;
			return;
		}
		free(pBlock);
		return;
	}
}

根据上面对于内存池的设计可以看出,每个内存池中分配出来的内存大小是固定的。如果内存单元的大小是256字节,即使用户只想要申请12字节的内存,也必须将整个256字节的内存单元分配给用户。故单一大小的内存池不够灵活,可能会造成内存的浪费。因此设计类MemoryMgr来控制多个内存池。为了便于不同尺寸的内存池的初始化,设计模板类MemoryAlloc_sized如下:

template<size_t nsize, size_t nNum>//便于类作为成员的初始化
class MemoryAlloc_sized :public MemoryAlloc
{
public:
	MemoryAlloc_sized()
	{
		const size_t n = sizeof(void*);

		_BlockSize = (nsize/n)*n + (nsize%n ? n: 0);//size对齐
		_BlockNum = nNum;
	}
};

设计多个内存池的控制类MemoryMgr如下:

//内存管理工具,内含多个大小不同的内存池
class MemoryMgr
{
private:
	//内存池
	MemoryAlloc_sized<64, 1000000> _mem_64;
	MemoryAlloc_sized<128, 100000> _mem_128;
	MemoryAlloc_sized<256, 1000> _mem_256;
	MemoryAlloc_sized<512, 100> _mem_512;
	MemoryAlloc_sized<1024, 100> _mem_1024;
	//内存池的映射数组,用于将不同大小的内存请求映射到不同的内存池中
	MemoryAlloc* _szAlloc[MAX_MEMEORY_SIZE + 1];

	MemoryMgr()
	{
		init(0, 64, &_mem_64);
		init(65, 128, &_mem_128);
		init(129, 256, &_mem_256);
		init(257, 512, &_mem_512);
		init(513, 1024, &_mem_1024);
	};

	~MemoryMgr()
	{

	}
	//初始化内存映射数组
	void init(int nBegin, int nEnd, MemoryAlloc* pMemA)
	{
		for (int n = nBegin; n <= nEnd;n++)
		{
			_szAlloc[n] = pMemA;
		}
	}
public:
	static MemoryMgr& Instance()//单例设计模式
	{
		static MemoryMgr mgr;
		return mgr;
	};

	//申请内存
	void* allocMem(size_t nsize);
	//释放内存
	void freeMem(void* p);

	//增加内存块的引用计数,比如共享内存
	void addref(void* pmem);
};

这是一个单例类,可从外部调用Instance函数来获取对象。该类中包含了五个内存单元大小不同的内存池。映射数组_szAlloc用于将用户申请的字节数映射到对应的内存池。例如用户想要申请一个312字节的内存块,通过_szAlloc[312]就可以获取到需要被放入的内存_mem_512

申请内存的函数和释放内存的函数实现如下:

void* MemoryMgr::allocMem(size_t nsize)
{
	if (nsize <= MAX_MEMEORY_SIZE)
	{
		return _szAlloc[nsize]->allocMem(nsize);
	}
	else
	{
		MemoryBloack* ret = (MemoryBloack*)malloc(nsize + sizeof(MemoryBloack));//用户需要的数据大小加上控制块的大小
		ret->bPool = false;
		ret->nID = -1;
		ret->nRef = 1;
		ret->pAlloc = nullptr;
		ret->pNext = nullptr;
		cout << "申请的空间超过MAX_MEMEORY_SIZE,在堆内存申请,大小为: " << nsize << endl;
		return ((char*)ret + sizeof(MemoryBloack));
	}
}


void MemoryMgr::freeMem(void* p)
{
	MemoryBloack* pBlock = (MemoryBloack*)((char*)p - sizeof(MemoryBloack));
	pBlock->pAlloc->freeMem(p);
}

//增加内存块的引用计数,比如共享内存
void MemoryMgr::addref(void* pmem)
{
	MemoryBloack* pBlock = (MemoryBloack*)((char*)pmem - sizeof(MemoryBloack));
	++pBlock->nRef;
}

申请内存时需要注意的就是当用户需要的内存大小已经超出了最大内存池的内存单元尺寸,即不可能满足用户要求时u,就要调用malloc函数在内存池外申请一块区域交给用户。(这里MAX_MEMEORY_SIZE = 1024)

以上就已经较为完整的实现了内存池,下面重载new运算符:

void* operator new(size_t size)
{
	return MemoryMgr::Instance().allocMem(size);
}

void* operator new[](size_t size)
{
	return MemoryMgr::Instance().allocMem(size);
}

void operator delete(void* p)
{
	MemoryMgr::Instance().freeMem(p);
}

void operator delete[](void* p)
{
	MemoryMgr::Instance().freeMem(p);
}


void* mem_alloc(size_t size)
{
	return malloc(size);
}


void mem_free(void* p)
{
	free(p);
}

当用户第一次调用new操作符的时候,通过static函数Instance会建立一个内存池的管理类MemoryMgr。此后再调用new运算符不再重新建立MemoryMgr对象。

将以上代码与服务器的代码整合。实现了服务器整体建立再内存池上的目的。

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页