内存池架构设计问题
通常来讲,当程序需要大量的申请与释放堆内存操作时,都需要自定义一个内存池,否则有可能会产生野指针,产生内存碎片,申请与释放速度较慢,程序不能长时间运行。
例如在Windows 的服务器编程中,就需要内存池的操作。
通过C++技术网 的帮助,我学会了IO完成端口的编写。虽然是效率很高,但是到了最后发现,IO完成端口的错误处理也是最多的。例如 GetQueuedCompletionStatus、 PostQueuedCompletionStatus、GetOverlappedResult、AcceptEx、WSASend、WSARecv、DisconnectEx等函数中返回FALSE之后 根据GetLastError的返回值处理。但是错误的返回值有很多种,比如本地的网络连接被关闭、内存不足、队列已满等。感觉这个错误处理需要时间的累积,怪不得有些公司需要人员长期维护,而不是一次性的编写好。让我不得不感叹,好处越多,坏处也跟着一起多了起来。
回归正题,这次我所说的是内存池架构设计的问题。
首先我先把我的一个简单设计的内存池代码写出来:
1.定义枚举常量:
enum
NodeType
{
NODE_RETREAT, // 未被占用内存
NODE_OCCUPY // 已被占用内存
};
这个枚举常量我是在后来加上去的,为了避免释放指针的时候重复释放而设置的。
2.定义内存池链表节点:
template <class T>
struct
MemNode : public
T
{
MemNode* m_pPrev; //
上一个内存
MemNode* m_pNext; //
下一个内存
NodeType m_Type; //
节点类型
};
这里可能要说我为什么选择派生自定义类型,而不是选择如下定义的结构体呢?
template <class T>
struct
MemNode
{
T m_Data; //
自定义类型
MemNode* m_pPrev; //
上一个内存
MemNode* m_pNext; //
下一个内存
NodeType m_Type; //
节点类型
};
如果自定义类型中也有个成员变量叫 m_pPrev 的话就会出现同名错误现象,为了解决这个问题,所以选择了派生自定义类型,成员变量名称不会造成冲突,但是我这个派生的话有个缺点,就是没有办法派生基础类型,比如char、short、int、long等基础类型。
如果非要用基础类型的话,只好定义一个自定义的结构体,例子如下:
struct
MyStruct
{
int m_nInt;
};
这样访问速度应该和普通访问int类型差不多。
3.定义内存池链表类:
template<class
T>
class CMemList
{
public:
CMemList();
~CMemList();
public:
// 把申请的新节点插入到链表末尾处,本函数不负责申请内存,请外部申请
VOID PushBackNode(MemNode<T>* pNode);
// 把某节点移除出链表,本函数不负责释放内存,请外部释放
VOID EraseNode(MemNode<T>* pNode);
// 把末尾处的节点移除掉,并赋值给pNode,如果链表为空,不会进行赋值操作,本函数不负责释放内存,请外部释放
VOID PopBackNode(MemNode<T>*& pNode);
// 返回当前链表的数量
SIZE_T GetListCount();
private:
MemNode<T>* m_pHead; //
链表头指针
MemNode<T>* m_pTail; //
链表尾指针
SIZE_T m_stCount; // 记录链表数量
};
template<class T>
inline CMemList<T>::CMemList()
{
m_pHead
= m_pTail = NULL;
m_stCount
= 0;
}
template<class T>
inline CMemList<T>::~CMemList()
{
m_pHead
= m_pTail = NULL;
m_stCount
= 0;
}
template<class T>
inline VOID CMemList<T>::PushBackNode(MemNode<T>* pNode)
{
if
(NULL == m_pHead && NULL == m_pTail)
{
pNode->m_pPrev = NULL;
pNode->m_pNext = NULL;
m_pHead = m_pTail = pNode;
}
else
{
m_pTail->m_pNext = pNode;
pNode->m_pPrev = m_pTail;
pNode->m_pNext = NULL;
m_pTail = pNode;
}
++m_stCount;
}
template<class T>
inline VOID CMemList<T>::EraseNode(MemNode<T>* pNode)
{
if
(m_pHead == pNode && m_pTail == pNode)
{
m_pHead = m_pTail = NULL;
}
else
if (m_pHead == pNode)
{
m_pHead = m_pHead->m_pNext;
m_pHead->m_pPrev = NULL;
}
else
if (m_pTail == pNode)
{
m_pTail = m_pTail->m_pPrev;
m_pTail->m_pNext = NULL;
}
else
{
MemNode<T>* pPrev = pNode->m_pPrev;
MemNode<T>* pNext = pNode->m_pNext;
pPrev->m_pNext = pNode->m_pNext;
pNext->m_pPrev = pPrev;
}
--m_stCount;
}
template<class T>
inline VOID CMemList<T>::PopBackNode(MemNode<T>*& pNode)
{
if
(NULL != m_pHead && NULL != m_pTail)
{
pNode = m_pTail;
if (NULL == m_pTail->m_pPrev
&& NULL == m_pTail->m_pNext) {
m_pHead = m_pTail
= NULL;
}
else
{
MemNode<T>* pPrev = m_pTail->m_pPrev;
pPrev->m_pNext
= NULL;
m_pTail = pPrev;
}
--m_stCount;
}
}
template<class T>
inline SIZE_T CMemList<T>::GetListCount()
{
return
m_stCount;
}
代码都是经过测试的,没什么异常的地方,硬要说异常的地方的话如下:
PushBackNode 参数给个假内存地址
EraseNode参数给个假内存地址 或 内存根本不在链表中
这些错的话可能来源于自己的粗心大意,写模板类时要非常注意,模板类出错有时候是看不出来的,所以需要先自己设一个假的模板类,比如把T想象成int,实现一次,没什么错,再弄成模板类。
做到这里基本完成大半了,还有最后一步。
4.定义内存池模板类:
template<class T>
class CMemPool
{
public:
CMemPool();
~CMemPool();
public:
// 申请内存
T* Alloc();
// 释放内存
VOID Free(T* p);
private:
CMemList<T> m_Retreat; //
未被占用内存链表
CMemList<T> m_Occupy; //
已被占用内存链表
};
template<class T>
inline CMemPool<T>::CMemPool()
{
}
template<class T>
inline CMemPool<T>::~CMemPool()
{
SIZE_T stIndex;
MemNode<T>* pNode;
for
(stIndex = m_Occupy.GetListCount(); stIndex > 0; --stIndex)
{
m_Occupy.PopBackNode(pNode);
::HeapFree(::GetProcessHeap(), 0, pNode);
}
for
(stIndex = m_Retreat.GetListCount(); stIndex > 0; --stIndex)
{
m_Retreat.PopBackNode(pNode);
::HeapFree(::GetProcessHeap(),
0, pNode);
}
}
template<class T>
inline T* CMemPool<T>::Alloc()
{
if
(0 == m_Retreat.GetListCount())
{
MemNode<T>* pNew = (MemNode<T>*)::HeapAlloc(::GetProcessHeap(),
HEAP_ZERO_MEMORY, sizeof(MemNode<T>));
if (NULL == pNew) {
return NULL;
}
m_Retreat.PushBackNode(pNew);
pNew->m_Type = NODE_RETREAT;
}
MemNode<T>* pAlloc(NULL);
m_Retreat.PopBackNode(pAlloc);
m_Occupy.PushBackNode(pAlloc);
pAlloc->m_Type
= NODE_OCCUPY;
return pAlloc;
}
template<class T>
inline VOID CMemPool<T>::Free(T* p)
{
if
(NODE_OCCUPY != ((MemNode<T>*)p)->m_Type) {
return;
}
m_Occupy.EraseNode((MemNode<T>*)p);
m_Retreat.PushBackNode((MemNode<T>*)p);
((MemNode<T>*)p)->m_Type
= NODE_RETREAT;
}
构造函数什么都不用做。
析构函数负责把已占用和未被占用的内存全部释放掉,应该不会产生什么野指针。
Alloc 负责申请内存,如果未被占用的内存链表中没有可重复使用的内存的话,就申请一个新的内存压进链表中,并把指针类型设为未被占用。然后从未被占用的内存链表末尾处移除出来,放入已被占用的内存链表中,并把指针的类型设为被占用,然后返回申请的内存指针。
Free 负责释放内存,先判断申请的内存是不是已被占用的类型,如果不是,直接返回,避免重复压进链表。然后把申请内存从已被占用链表移除出来,放进未被占用的内存链表中,并设置内存类型为未被占用。这样就只是逻辑上的删除,真正内存还没释放,这样就可以重复利用了。
就这样一个简单的内存池设计就弄好了。
接下来就是总结问题了:
1. 如果其他程序员粗心大意把内存越界操作了,就导致整个链表与内存池毁灭,最后就是程序奔溃。
2. Free函数操作的参数 如果把不是 Alloc 申请的内存的话,可能造成越界,最后程序崩溃。
3. 内存池是否需要设计一个峰值,如果申请的内存到了峰值,释放时不是压进未被占用的内存链表中,而是真正的释放内存?
4. 我的这个设计是基于Windows 平台的,如果是Linux 的话可能需要改成 malloc 函数,不推荐使用new。
5. 是否需要改成像STL 库那样的内存分配器呢?
6. 是否需要再做些什么改进?
我一个人智商有限,能想到问题也就这么多了,只好借助 C++技术网 的力量了。
最后感谢C++技术网的回答,也同时祝贺C++技术网及大家新年快乐!
C++技术网会员解答:
此问题整理一下,是自己实现内存池的相关问题。前面描述的实现过程都没什么问题,所以在此做一个整体性的解答和建议。
内存池是内存操作相对复杂的一个应用,如果操作不慎,就会引起严重问题。对于使用内存池时,建议要对内存池操作进行封装,隐藏内部操作细节,仅向上提供操作的上层API函数。这样可以规避使用内存池过程中带来的问题。内存池内部实现的机制要确保内存的正确使用。所以前两个问题就不再是问题。
既然是内存池,就是要充分利用内存,避免频繁的内存申请和释放。内存池的大小是程序指定的,只是一块比较大的内存而已。并不是说,有了内存池就让系统内存可用量急剧下降。内存池只是来提高内存使用效率的,和内存池总大小没有直接关系。所以,申请到的内存池使用达到全部大小时,说明开始申请的内存大小已经不够用了。此时的不够用只是自己申请的内存池内存容量用完了,如果系统内存可用容量充足,那一般要继续申请新的内存加入到内存池中,确保内存池正常运转。一般情况系统内存都是充足的。如果系统内存不够用,也就是分配内存失败时,那么可以采取真正释放内存的策略。这就是内存释放的策略问题。或者说,内存池的大小的使用,关联到系统内存可用容量。这仅仅是一个策略问题。
不过,内存池必须要有一个峰值。这个峰值就可以来决定是否要申请更多内存。这个峰值和内存释放没有关系。内存池总容量一定要高于峰值,峰值是执行内存管理策略的一个阈值。
至于说分配和释放内存使用哪种方式,看设计的内存池应用的范围即可。
内存池管理必须形成一个整体,只通过API对外开放。可以借鉴stl的内存管理。
基本上是这样了,如果还有问题,请在文后留言。