完成端口通讯服务器设计 (IOCP Socket Server)
第一章:是谁神化了IOCP
Windows系统下的socket模型有多种,其中完成例程的效率也是相当高的,其它的也不差(相关模型知识这里不多做介绍,读者可以自己搜索或查阅有关资料)。但是不知道为什么,一提起IOCP就会有很多人质疑:IOCP真的有这么神话吗?
尽管质疑,依然有很多人还是在茫茫网络中苦苦寻一个完整的IOCP源码,希望能够对了解IOCP起到事半功倍的作用,不过得到的大多也只是残缺不全的。什么是IOCP?IOCP的机制是什么?IOCP有怎样的性能?当一个人深入了解IOCP以后,才解开了它神话之谜:其实它没有什么神话。很多人之所以质疑IOCP,说出上面那句话的时候,其实是他正在神化IOCP,主要是因为对IOCP不了解,甚至不知道。所以,是谁神化了IOCP呢?是那些不了解IOCP 但又想了解却没有进展的人。
IOCP主要针对数据吞吐量和连接并发量而设计。有些人使用IOCP,做的却是堵塞模式的事情:对每个连接自己建立一个发送队列,每次才投递一个发送请求给IOCP,等该请求已决后才又出列一个再投递给IOCP。任何一个服务器,能达到怎样的性能,对设计者的要求也是苛刻。根据服务器对性能要求,合理利用通讯模型,才是设计者的关键。如果在一个只有100个终端且每个终端每10秒才发送一个数据包的服务器系统里,用什么Socket模型都一样,甚至用Win98系统做都可以。
对于一个服务器而言,需要设计者对内存管理,对网络状况,对操作系统等等都要有深入了解,并具有深厚的技术功底。否则,还会产生更多神化IOCP的人。
服务器性能,系统支持是基础,设计者水平是关键。而这个水平条件,没有一个衡量的最终标准,它是永无止境的,会随着时间和经验的积累不断提高。
第二章:内存管理(AWE)
有牛人曾经说过,服务器玩的就是内存。仔细想想,确实是如此。服务器对内存的需求是巨大的,对内存的要求也是苛刻的。如何在内存管理上下功夫使服务器性能达到一个质的飞跃,是服务器设计中的首要解决的问题。
说到内存,我想刚开始设计服务器的人会说,不就申请释放吗,有什么难呢。从操作步骤来说,确实就这么两个,没有再多了的工作了。当我们采用虚拟内存分配或堆分配从操作系统获取内存的时候,总以为我们获得了足够的内存就可以让服务器安心工作了。但事情并未就这么简单,操作系统在一定条件下,还可以征用已经分配给你的物理内存,它会将你的物理内存数据复制到页交换文件中,然后把本来给你的物理内存再分配给别的进程,当你的进程访问你所获得的虚拟地址集的数据时,它会再个空(或许也是从别的进程征用)的物理内存,再从页交换文件里面调出你原来的数据放回到新的物理内存里面,并将这个物理内存映射到你申请的虚拟内存地址集内(有关这项内容请参考操作系统
的内存管理)。这个过程是相当耗费CPU资源且十分缓慢的,尤其是对硬盘虚拟内存文件的读写。其它大道理本文不多说,关于操作系统内存管理的原理可以从《Windows核心编程》、《Windows操作系统》、《操作系统》等书籍上了解。
我们可以使用lookaside lists技术来重新使用已经分配的内存的,或者使用SetWorkingSetSize来设置标志告知操作系统不要交换我的内存,但不外乎多一次操作而已。这个操作到底消耗多少的CPU资源,本人也没有考究过,但从性能要求的角度来说,多一事不如少一事。本文讨论的内存管理,将采用AWE(地址窗口化扩展)的技术,将申请到的物理内存保留为非分页内存,这部分的内存不会被页交换文件所交换,关于AWE请参阅以上提到的书籍。
(下面提到的“内存管理”,将仅针对应用程序自己的内存管理功能模块(下文称之为内存管理器)而言,已非上面提到的操作系统的内存管理。)
衡量内存管理器性能的有两个,一个是内存分配时的效率(分配效率),另一个内存交还时的效率(释放效率),亦即二者操作的时间性,这个时间越短那么可以认为它的效率越高。下面的讨论,假定内存管理器是以页为最小分配单位,至于页的大小是多少才合适,稍后再说。
先谈分配效率(下面提到方法仅为本人归纳后的方法,不是学术上的算法):
1、单链表型
也就是将所有空闲内存块(即空闲内存碎片,下称空闲碎片)组成一个空闲碎片链表。当提出内存分配申请的时候,从这个链表头遍历查合乎要求的内存或从大的碎片里面分割出来。这个方式简单,但如果空闲碎片多而且小于申请要求的时候,就需要做众多的循环操作。
单链表型排列方式可分为:
a按地址高低排列 n
b安碎片由小到大排列
c不排列(先进先出)
2、多链表型
事实上,多链表型就是将上述按b方式排列的单链表,根据一定的大小档次截断而组成的多个链表集(相关算法请参阅上文提到的书籍)。
多链表我将之分为a、b型。
a型如下表:
从上表我们看到,0~<4K的ListA有8个碎片节点;4K~<8K的ListB有4个碎片节点,分别是:4K、6K、7K、7K;8K~<16K的ListC为空;16K~<24K的ListD有4个碎片节点,分别是:16K、19K、22K、23K。
假如要分配一个5K的内存,就可以直接从ListB查(如果ListB为空,则继续向更大空间的链表查)直到底部的ListN。这样就可以避免遍历ListA,假如ListA碎片极多的情况下那么就可以节省更多的时间。
b型如下表
右边的情况不多说,基本和a型的一致。那么左边的映射表是怎么回事呢?映射表始终没有空的,如果ListA不为空,那么a就指向ListA,如果ListA为空,那么它再指向ListB,以此类推直到底部的ListN。如果要分配9K 内存,直接从c取链表头,实际上它指向的ListN,这样当N =1000+的时候,节省的时间就和a型相比就非常可观了。
再看看释放效率:
1针对内存分配方案的第1种类型
释放步骤为:
a到比释放内存块的地址低的并是相连接的空闲碎片然后与之合并再重新排列;
b不管a成立与否,再比释放内存块的地址高的并是相连接的空闲碎片然后与之合并再重新排列;
c在a和b不成立的情况,则按排列规则插入空闲碎片链表。
postthreadmessage上述步骤中,我们发现空闲碎片是一个巨量级的时候效率及其低下。
2针对内存分配方案第2种类型
a从ListA到ListN到比释放内存块的地址低的并是相连接的空闲碎片然后与之合并再重新排列;
b不管a成立与否,再从ListA到ListN比释放内存块的地址高的并是相连接的空闲碎片然后与之合并再重新排列;
c在a和b不成立的情况,根据释放内存的大小到归档链表,按排列规则插入该空闲碎片链表。
在这个情况中,工作量比起1更加大的多。
3内存块链表法
这个链表不是指上面所说的空闲碎片链表,而是所有的内存不管空闲的或是使用的,按地址由低到高排列的双向内存块链表。当然,我们不能在释放的时候再去排列所有的内存块,这样的话效率也是相当低的。
如何排列这个链表,可以从分配的时候下功夫:
pBlock为空闲的碎片块
……
if(pBlock->dwSize > dwSize)
{//如果空闲块的大小大于要分配的
//从大的里面切出一块来使用,该块容量减少
pBlock->dwSize -= dwSize;
//返回分配的地址
Result = (PGMEM_BLOCK)pBlock->pAddr;
//该空闲块向后指向新的空闲地址
pBlock->pAddr = (char*)Result + dwSize;
//获取一个新节点
PGMEM_BLOCK pTmp;
pTmp = pmbGMemNodePool;
pmbGMemNodePool = pmbGMemNodePool->pmbNext;
//将新分配出去的块插在该空闲块前面
pTmp->pAddr = Result; //分配出去的内存块地址
pTmp->dwSize = dwSize; //分配出去的内存块大小
pTmp->pmbNext = pBlock; //下个指针指向被分割的空闲块
pTmp->pmbPrior = pBlock->pmbPrior;
if(pTmp->pmbPrior)
pTmp->pmbPrior->pmbNext = pTmp;
pBlock->pmbPrior = pTmp;
……
}
根据上述代码,在分配的时候只需很少的代码量就可以完成了排列要求,这个小小的开支,就可以在释放的时候起到非常高的效率:
从上图看到,内存块链表顺序是:ABCDEFG,空闲块链表顺序是:EBG。通过要释放的块的地址经过计算(FreeAddr –AddrHead)/ PageSize得出该块在页标志的位置,就可以到要释放的内存块在内存块链表中的位置是F,通过上下指针就可以知道与F相邻的两个块是EG,根据标志判断是否为空闲块,然后通过双向链表操作来合并这个三个块,这样几乎不要任何遍历操作就可以轻松完成了释放合
并的操作。当然如果相邻两个块都没有空闲的,则按排列规则插入空闲块队列。估计没有比这个更好的释放合并内存的算法了。
经过了上面的方法介绍,这个时候我们应该清楚我们该做什么了吧。如果采用内存分配第2种“多链表b型”的方案和内存释放第3种“内存块链表法”的方案,那么这个内存管理器一定是优越的吧。于是开始噼里啪啦的编码……发现一个头痛的问题,即使把内存释放的工作让独立线程来处理,空闲内存块的排列依然消耗很多时间。这个时候不禁问:我们到底在干什么?用这么强大的内存管理器来做操作系统吗?不,我们不是做操作系统,那还有什么方法让内存管理更为简单呢?

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。