您好,欢迎访问三七文档
当前位置:首页 > 商业/管理/HR > 销售管理 > 完成端口通讯服务器设计_IOCP Socket Server
完成端口通讯服务器设计(IOCPSocketServer)第一章:是谁神化了IOCPWindows系统下的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操作系统》、《操作系统》等书籍上了解。我们可以使用lookasidelists技术来重新使用已经分配的内存的,或者使用SetWorkingSetSize来设置标志告知操作系统不要交换我的内存,但不外乎多一次操作而已。这个操作到底消耗多少的CPU资源,本人也没有考究过,但从性能要求的角度来说,多一事不如少一事。本文讨论的内存管理,将采用AWE(地址窗口化扩展)的技术,将申请到的物理内存保留为非分页内存,这部分的内存不会被页交换文件所交换,关于AWE请参阅以上提到的书籍。(下面提到的“内存管理”,将仅针对应用程序自己的内存管理功能模块(下文称之为内存管理器)而言,已非上面提到的操作系统的内存管理。)衡量内存管理器性能的有两个,一个是内存分配时的效率(分配效率),另一个内存交还时的效率(释放效率),亦即二者操作的时间性,这个时间越短那么可以认为它的效率越高。下面的讨论,假定内存管理器是以页为最小分配单位,至于页的大小是多少才合适,稍后再说。先谈分配效率(下面提到方法仅为本人归纳后的方法,不是学术上的算法):1、单链表型也就是将所有空闲内存块(即空闲内存碎片,下称空闲碎片)组成一个空闲碎片链表。当提出内存分配申请的时候,从这个链表头遍历查找合乎要求的内存或从大的碎片里面分割出来。这个方式简单,但如果空闲碎片多而且小于申请要求的时候,就需要做众多的循环操作。单链表型排列方式可分为:a按地址高低排列nb安碎片由小到大排列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不成立的情况,则按排列规则插入空闲碎片链表。上述步骤中,我们发现空闲碎片是一个巨量级的时候效率及其低下。2针对内存分配方案第2种类型a从ListA到ListN找到比释放内存块的地址低的并是相连接的空闲碎片然后与之合并再重新排列;b不管a成立与否,再从ListA到ListN找比释放内存块的地址高的并是相连接的空闲碎片然后与之合并再重新排列;c在a和b不成立的情况,根据释放内存的大小找到归档链表,按排列规则插入该空闲碎片链表。在这个情况中,工作量比起1更加大的多。3内存块链表法这个链表不是指上面所说的空闲碎片链表,而是所有的内存不管空闲的或是使用的,按地址由低到高排列的双向内存块链表。当然,我们不能在释放的时候再去排列所有的内存块,这样的话效率也是相当低的。如何排列这个链表,可以从分配的时候下功夫:pBlock为空闲的碎片块……if(pBlock-dwSizedwSize){//如果空闲块的大小大于要分配的//从大的里面切出一块来使用,该块容量减少pBlock-dwSize-=dwSize;//返回分配的地址Result=(PGMEM_BLOCK)pBlock-pAddr;//该空闲块向后指向新的空闲地址pBlock-pAddr=(char*)Result+dwSize;//获取一个新节点PGMEM_BLOCKpTmp;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种“内存块链表法”的方案,那么这个内存管理器一定是优越的吧。于是开始噼里啪啦的编码……发现一个头痛的问题,即使把内存释放的工作让独立线程来处理,空闲内存块的排列依然消耗很多时间。这个时候不禁问:我们到底在干什么?用这么强大的内存管理器来做操作系统吗?不,我们不是做操作系统,那还有什么方法让内存管理更为简单呢?来看下表,一个应用服务器对内存动态需求量的统计表:看了上面这个表终于明白,原来在内存需求中类型为F和G的最多,似乎豁然开朗,假定内存足够的情况下将内存管理器的页大小设置为512B,那么即使空闲内存块是不排列的,分配内存的效率也是极高的。但对类型D这个使用频率最高的内存需求怎么解决呢?任何只想使用一个内存管理器就能做好服务器的想法是错误的。针对很多种内存需求,必须加以分类。针对这个4K内存需求,可以使用一个固定大小的内存池来解决,只要是这个内存类型需求的都要在这个内存池操作,比如SocketI/O操作的缓冲区等(将在以后介绍)。通过以上讨论,采用内存分配方案的第1种类型c方式排列和内存释放方案第3种类型,来满足不固定大小的动态内存分配需求,即可一定程度上达到对性能要求的目的。当然,使用内存分配方案的第2种b类型的,也未必不可,多消耗些CPU资源而已。内存管理没有绝对的方法,所以上述内容仅仅是讨论如何根据内存需求设计内存管理器。例程截图:源码说明:例程源码下载地址:和GMem.h是内存管理单元的源码文件,欢迎指正。历程源码没有优化,能达到测试目的即可,提供的测试数据仅供参考,并无实质意义。第三章:不要迷信API(单链表的另一种算法)用这个标题可能会牵强了点。只是因为在性能优化中遇到这样的事情,因此用来做标题而已,由此通过一个小事抛出本文介绍的内容:单链表的另一种算法。我本人也做了个ARM的OS内核,虽然及其简单,但是OS的机理大相径庭。操作系统要做到线程同步,需要进入中断(应用程序同步一般是软件中断)才能做到,在进入和退出这两个环节付出的代价相当昂贵的(事实上操作系统的分时处理机制(时钟中断)也在付出昂贵的代价,但那是不可避免的)。有时候了解的越多,考虑的因素越复杂,做出的结果会越错误。正如上面所说的,线程同步需要使用临界会消耗巨大时间(比起非使用临界而言)。在做内存管理的释放函数的时候,有牛人告诉我,可以使用独立线程来处理释放工作,在释放函数里面给它发个消息就得了,比如使用PostThreadMessage一个函数就搞定了,又避免了使用临界。刚开始我一听,也感觉真的是很简单,避免了释放的时候释放函数做太多工作而造成堵塞。但随后一想,我向他提出了几个问题:“线程什么时候读取消息?隔1毫秒?隔10毫秒?会不会因为延时造成内存耗尽的错象?”他又给我推荐了SetEvent,用它及时告知Thread去处理释放工作。感觉好像“非常有道理”。不过我没有立即去这么做,而是做了几个代码段去测试(实际对于一个资深的程序员来说根本不需要测试他们是否使用同步机制,本人只想:
本文标题:完成端口通讯服务器设计_IOCP Socket Server
链接地址:https://www.777doc.com/doc-6108841 .html