完成端口通讯服务器(IOCP Socket Server)设计(二)内存管理(AWE)

完成端口通讯服务器(IOCP Socket Server)设计

(二)内存管理(AWE)

 

Copyright © 2009 代码客(卢益贵)版权所有

QQ:48092788   源码博客:http://blog.csdn.net/guestcode

 

有牛人曾经说过,服务器玩的就是内存。仔细想想,确实是如此。服务器对内存的需求是巨大的,对内存的要求也是苛刻的。如何在内存管理上下功夫使服务器性能达到一个质的飞跃,是服务器设计中的首要解决的问题。

说到内存,我想刚开始设计服务器的人会说,不就申请释放吗,有什么难呢。从操作步骤来说,确实就这么两个,没有再多了的工作了。当我们采用虚拟内存分配或堆分配从操作系统获取内存的时候,总以为我们获得了足够的内存就可以让服务器安心工作了。但事情并未就这么简单,操作系统在一定条件下,还可以征用已经分配给你的物理内存,它会将你的物理内存数据复制到页交换文件中,然后把本来给你的物理内存再分配给别的进程,当你的进程访问你所获得的虚拟地址集的数据时,它会再找个空(或许也是从别的进程征用)的物理内存,再从页交换文件里面调出你原来的数据放回到新的物理内存里面,并将这个物理内存映射到你申请的虚拟内存地址集内(有关这项内容请参考操作系统的内存管理)。这个过程是相当耗费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不成立的情况,则按排列规则插入空闲碎片链表。

上述步骤中,我们发现空闲碎片是一个巨量级的时候效率及其低下。

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种“内存块链表法”的方案,那么这个内存管理器一定是优越的吧。于是开始噼里啪啦的编码……发现一个头痛的问题,即使把内存释放的工作让独立线程来处理,空闲内存块的排列依然消耗很多时间。这个时候不禁问:我们到底在干什么?用这么强大的内存管理器来做操作系统吗?不,我们不是做操作系统,那还有什么方法让内存管理更为简单呢?

来看下表,一个应用服务器对内存动态需求量的统计表:

 

看了上面这个表终于明白,原来在内存需求中类型为F和G的最多,似乎豁然开朗,假定内存足够的情况下将内存管理器的页大小设置为512B,那么即使空闲内存块是不排列的,分配内存的效率也是极高的。但对类型D这个使用频率最高的内存需求怎么解决呢?任何只想使用一个内存管理器就能做好服务器的想法是错误的。针对很多种内存需求,必须加以分类。针对这个4K内存需求,可以使用一个固定大小的内存池来解决,只要是这个内存类型需求的都要在这个内存池操作,比如Socket I/O操作的缓冲区等(将在以后介绍)。

通过以上讨论,采用内存分配方案的第1种类型c方式排列和内存释放方案第3种类型,来满足不固定大小的动态内存分配需求,即可一定程度上达到对性能要求的目的。当然,使用内存分配方案的第2种b类型的,也未必不可,多消耗些CPU资源而已。

内存管理没有绝对的方法,所以上述内容仅仅是讨论如何根据内存需求设计内存管理器。

例程截图: 

 

 

 


源码说明:

GMem.cpp和GMem.h是内存管理单元的源码文件,欢迎指正。历程源码没有优化,能达到测试目的即可,提供的测试数据仅供参考,并无实质意义。

 

例程源码下载:

http://download.csdn.net/source/1607811

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/GuestCode/archive/2009/08/27/4488402.aspx

posted @ 2009-09-30 14:38  独奏者  阅读(1137)  评论(0编辑  收藏  举报