读书人

完成端口内存池设计时的想法请大家看

发布时间: 2012-03-06 20:47:55 作者: rapoo

完成端口内存池设计时的想法,请大家看看我想的对不对
最近在写高并发服务器用到了完成端口:

1、完成端口对输入输出的处理需要预先申请一个Overlapped缓存空间,并且要保持该缓存所在页面一直呆在物理内存里(一个页面大小根据情况有所不同,我的破电脑页大小是4K,也就是说如果你有一块Overlapped缓存虽然只有100个字节,但是整个4K的页都必须呆在物理内存里),而不是像一般空间一样可以被系统随意调度出物理内存(windows内存管理机制:长时间不用到的页会从物理内存中转出),但是这带来一个问题:如果输入输出请求量过于巨大,则导致被锁定的页太多,从而使的物理空间被占满影响到其他程序的运行,甚至完成端口本身也无法响应请求。(虽然windowsNT会对锁定页面做限制:即大概是不能超过物理内存空间的1/8)。

2、每次进行I/O操作时都要申请新的内存,用完后再释放,显然是非常影响运行效率的,而且也会产生大量的内存碎片。所以一般情况下高性能的服务器要求:在程序运行之初先把需要用到的内存全部申请好,每次需要内存的时候直接拿来用就可以了,等到程序退出的时候再把内存一次性释放掉。-----这个就是内存池技术。

我从网上看了很多人的内存池代码,发现他们有一个比较大的缺陷:他们的内存池不是一整块申请,而是一小块一小块申请,比如:

for i := 2 to IO_MEM_MAX_COUNT do
begin
PUNode(IOMemLast)^.Next := HeapAlloc(Heap, HEAP_ZERO_MEMORY, sizeof(TIOMem));
IOMemList[i] := PUNode(IOMemLast)^.Next;
IOMemLast := PUNode(IOMemLast)^.Next;
end;

我觉的这样申请的内存有一个很大的问题:由于这些一块一块的内存不是一起申请的,所以地址是不连续的,也就是这些内存会分散在不同的页中,这样就导致被锁定在物理内存中的页无形中多了很多,这应该很好理解的。

所以我认为应该一次性申请地址空间连续的内存,这样整齐的内存会减少很多被锁定的页面。

------------------------------------------------不一定成熟的想法。


[解决办法]
loki库里有一个smallobjectalloc,对于小型对象的分配管理的非常好,可以直接拿来用,也有很多借鉴的地方。
另外,既然程序使用IOCP,是服务,是长时间运行的程序,这一类程序还是一次从系统中分配足够的内存,然后自己管理比较好。系统默认的分配方式,能够适应各种情况,同时也意味着会有更多的薄记,更低的效率,这对于一个以效率为诉求的服务来说是一种伤害。
[解决办法]
就像gunsand所说的,完成端口和内存池其实没有什么联系。

如果考虑使用内存池的话,确实可以一些内存优化的动作。
比如说复用内存,避免频繁的分配和释放。

至于LZ所说的一下分配一块巨大的内存,然后在这个内存中进行处理;
以避免频繁的内存锁定问题。这个效果如何,其实应该测试一下。

不过对于计算机操作系统而言,内存的管理分配模块是一个
经过高度优化之后的产品(包含了非常多的内存分配算法),
以尽可能的避免内存碎片以及快速的分配。自己做的内存池的管理
模块并不一定会有着明显的优势(除非你经过测试证明了);
相反可能会对你的程序造成一些意外的影响。
而且,从整个程序的角度来看,无论你使用哪种内存分配管理
手段,都不可避免的会造成内存碎片。(详细的讨论可以参考
计算机程序设计艺术一书,上面有详细的介绍)。

IOCP的优势是I/O操作的多路复用,减少线程等待I/O操作的时间
,提高CPU的利用率。把它和Windows提供的线程池结合起来,
对于程序的效率有着显著的提高。
[解决办法]
还有一个疑问:完成端口如何管理那些超时的socket连接?
比较通常的思路是:建立一个链表来把所有连接保存起来,然后定时遍历连接,超时的断开,同时为每一个链表节点建立一个互斥量以同步线程:
PLink =^ TLink;
TLink = record
Socket: TSocket;
RemoteIP: string[30];
RemotePort: DWord;
//最后收到数据时的系统节拍
TickCountActive: DWord;
//处理该连接的当前线程的信息
Section: TRTLCriticalSection;
Next: PLink;
Prior: PLink;
end;
[解决办法]
我一般都是开辟一个空的内存池,随着使用内存块的增加逐步扩大内存池,因为是基于客户端或者小型服务器,大规模高并发的内存存取操作基本上没有,感觉这么做也足够了,比频繁的new、delete效率高。

但是如果做大型服务器的话,就不能用这种方法了,最好的办法是程序运行时就申请出“足够大”的内存区块,之后所有的数据操作都在这个“池”中进行。难的地方就是不知道这个“足够大”到底是多大,如果内存池的容量远远大于使用所需的容量,会造成浪费;如果内存池容量不够用,还需要进行扩张,又变成了不连续的内存区块。
[解决办法]
内存碎片产生的根源是程序无序的申请任意大小的内存,最后导致系统堆上的内存不贯。系统内存有字节对齐
问题。我们先申请一块大内存(当然要考虑申请失败的情况),然后规划我们需要每次申请的内存块,内存块
可以按照最小内存块倍数分配。程序运行期内只向大内存块申请但不释放。大内存块内部全部复用。提高运行
效率,同时打印内存分配日志用于debug。

读书人网 >.NET

热点推荐