Apache内存池解析

自己开发了一个股票智能分析软件,功能很强大,需要的点击下面的链接获取:

https://www.cnblogs.com/bclshuai/p/11380657.html

Apache内存池解析

1.前言

内存分配会占用时间,影响程序运行效率;特别是内存申请或销毁特别频繁的程序;内存分配会产生内存碎片,分为内部碎片和外部碎片;因为所有的内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,系统只能分配4的整数倍的内存,多余的内存空间称为内部碎片;外部碎片指的是还没有被分配出去(不属于任何进程),分配内存时,系统会搜索直到找到大小符合的连续内存块,因为内存分配是随机的,系统的内存被分割成无数的小片段,但有些内存因为太小了,无法分配给申请内存空间的新进程的内存空闲区域,则变成外部碎片。

内存池的设计理念是采用内存池来占用并管理一部分系统内存,实现内存的快速申请和归还;内存池可以减少系统内存碎片的产生、提高内存管理性能;在大型的服务器中内存池的作用尤其明显。内存池的内部结构主要由数组或链表组成;

2.Apache内存池

2.1 Apache内存池概述

       Apache内存池的设计理念是通过建立一个内存池来实现对程序的内存管理,程序需要内存时向内存池去申请,而不是像系统直接申请,程序释放内存,不是直接还给系统,而是还给内存池去管理,内存池可以自己管理一定限额的内存,不用马上还给系统,只有超过限额时才会还给系统,当然内存池管理的内存也是从系统申请过来的,一个进程的内存池中本来是没有管理一点内存的,程序第一次向内存池申请内存,内存池向系统申请内存,程序第一次将内存还给内存池,内存池并不将内存还给系统,而是自己管理起来,内存池这才开始管理起内存。程序第二次从内存池中申请内存时,内存池可以直接将第一次归还的内存直接给程序用,不够了再向系统去申请,就这样程序在一次次归还中建立起内存池中的内存库,供程序使用,内存中管理的内存大小应该是程序的内存使用最大峰值;通过这样一种机制减少程序直接向系统反复申请释放内存所消耗的时间,提高程序的运行效率,同时也是以程序一直占用最大峰值的内存为代价;只有当内存池销毁后,才会将内存池中的内存还给系统;

       内存池实际上是一个树状的结构,有多个内存池结点组成一个总的内存池,如图2-1所示,这只是一个示意图,当然可以往下继续延伸。根内存池结点global和分配子allocator是全局变量,生命周期与程序相同,程序启动时创建,程序关闭时销毁,而其他的内存池结点的生命周期与具体的应用有关,比如对于HTTP连接而言,要创建连接内存池结点和请求内存池结点,连接内存池结点挂在根内存池下面,生命周期与HTTP连接相同,连接创建时创建,连接断开时销毁;请求内存池结点是连接内存池结点的子结点,挂在连接内存池结点下面,在请求开始时创建,请求结束时销毁,生命周期比连接内存池结点短;分配子allocator才是整个内存池中的“池”的概念,内存池中的内存都是它向系统去申请的,它像一个内存工厂,从系统中申请一定大小的内存,打包成块;内存池结点就像是一个个批发零售商,从分配子那里申请一定大小的内存块,例如一下申请一个8kb的内存结点MemeNode,然后将8kb大小的内存结点挂在内存池结点的active指针下面,一个请求中需要内存时,可能是一次需要4字节、8字节、1024字节等,就直接从active指针下的结点里获取内存,等到8kb的内存用完或者不够后,再向allocator申请下一个内存结点MemeNode挂在active后面,active形成一个环形内存结点链表,表示内存池结点中正在使用的内存结点链表。当该内存池结点销毁后,挂在active下面的内存结点归还给allocator,由allocator继续分配给其他的内存池结点,从而实现循环利用。销毁内存池结点时,该内存池结点下所有的子内存池结点都会被销毁,子内存池结点的active下面挂的内存结点也会返还给allocator;以上就是对内存池整体的描述;先有个骨架,然后再将具体的血肉填充进去。

Apache内存池解析                       

图2-1内存池整体结构图

2.2 Apache内存分配子

       上文中讲到分配子allocator才代表了整个内存池中“池”的概论,首先讲pfree[MAX_INDEX],这是一个内存结点指针数组,先将内存结点理解为固定大小的内存块,allocator像是内存的批发商,内存结点都是较大的具有固定大小的内存块。pfree[MAX_INDEX]数组的每个元素都指向一个内存结点MemNode的链表,提供给内存池结点申请使用,除0索引下面挂的内存结点,其他元素下面挂的内存结点大小都相同,数组索引号与每个索引号对应内存结点大小的对应关系为 内存结点大小=4K*(index+1),例如索引号为1,结点大小则为8K,索引号为2结点大小为12K,依次类推,MAX_INDEX为宏定义,一般等于20,表示数组索引号的最大值,索引号1~MAX_INDEX下面挂的结点大小相同,称为规则结点;索引号0下挂的内存结点大小都大于4K*(MAX_INDEX+1),且内存结点大小不一定相同,称为不规则结点;采用数组索引的方式是为了方便存取,增加存取速度。当申请内存时,先将申请的大小转化为index值,然后直接根据index大小去在数组的索引号下查找是否有结点,先在索引1~MAX_INDEX下查找规则结点,如果没找到,则在0索引下查找,如果index超过MAX_INDEX,则直接在0索引下查找,如果都没找到则直接从系统申请。例如需要一个12k的内存结点,索引号为2,则在pfree[2]指针下查找是否有12K的结点,如果有则返回12K结点,如果没有则在pfree[3]下找16k的结点,一直到pfree[MAX_INDEX],以最合适的结点为原则,尽量使用allocator里已经挂载的内存结点,避免从系统去申请,16K的内存块给内存池结点去支配,也不会浪费。

分配子中CurrentMaxIndex就是用来表示pfree[MAX_INDEX]数组中最大有效索引号;MaxManageIndex表示分配子最大可以管理的内存大小,按照公式4K*(index +1)转化为index索引,以索引值表示大小,MaxManageIndex初始化为APR_ALLOCATOR_MAX_FREE_UNLIMITED,该值实际为0,表明分配子对于回收空闲结点的大小并不设门槛,意味着即使结点再大,系统也不会回收。CurrentFreeIndex表示分配子还可以继续管理的内存索引值大小;例如程序刚启动,分配子中没有管理任何内存结点,CurrentMaxIndex=0,表示pfree[MAX_INDEX]指针数组中指针都是空的,都是无效的;设置MaxManageIndex=400,即分配子最大可以管理内存4K*(400+1)=1604kb的内存。因为一开始分配子没有管理任何内存,所以CurrentFreeIndex=MaxManageIndex=400,表示可以再管理1604kb 的内存。当程序第一次申请内存时,因为分配子中全是空的,所以直接从系统去申请内存块。当程序第一次释放时,例如释放一个12kb的结点给allocator,12kb 对应的索引号为2,allocator将这个结点挂在pfree[2]指针下面,这时CurrentMaxIndex=2,表示最大有效结点在索引号2下面,指针数组中pfree[2]后面的指针都为空。CurrentFreeIndex=400-2=398,表示分配子只能管理1604kb-12kb=1592kb大小的内存,如果释放结点内存超过1592kb,则直接还给系统,不在给分配子管理;CurrentFreeIndex是程序将内存还给分配子还是给系统的一个判断标准,

MaxManageIndex设置好后不会变化。如果程序再释放一个20kb的内存结点给分配子,index大小为4,则挂载pfree[4]指针下面。CurrentMaxIndex大小由2变为4,表示最大的可用结点的索引值是4, pfree[4]后面的指针均为空。CurrentFreeIndex=398-4=394。如果程序需要申请一个16kb的内存结点,16kb的索引值为3,索引值3小于CurrentMaxIndex的值4,所以在规则结点中找,首先根据索引值3在pfree[3]下查找,因为pfree[3]下面没有挂载任何结点,所以继续在pfree[4]下面找,刚好有一个20kb的结点,将20kb的结点摘下,返回给程序使用,但是此时需要调整CurrentMaxIndex的值位2,因为pfree[2]指针以后的指针都是空指针,下面没有挂载任何的结点。CurrentFreeIndex的值为394+4=398。CurrentFreeIndex是随着结点挂载和摘取动态变化的,将内存结点挂在数组指针下时,CurrentFreeIndex值减小,但是不能小于0,摘取内存结点给程序使用时,CurrentFreeIndex值增大。如果还的结点超过MAX_INDEX索引值,但是不超过CurrentFreeIndex,则直接挂载在索引0指针下面,如果超过CurrentFreeIndex的值398,则直接归还给系统。挂载和摘取的操作都是在数组指针下的第一个内存结点。Pcs 临界区是多线程访问时候的互斥,而owner则记录了当前分配子所属于的内存池结点。

Apache内存池解析

图2-2内存分配子示意图

2.3 内存结点

内存结点的结构如下图所示,由结点头和剩余空间组成,结点头用来管理和记录该结点,剩余空间用于程序内存分配。例如需要申请一个内存结点占用的总空间最小为8kb,剩余空间大小=8kb-结点头大小;

Apache内存池解析

2-3内存结点示意图

struct MemNode

{

       struct MemNode                   *next;//指向下一个结点指针

       struct MemNode                  **ref;//指向自己的结点指针

       unsigned int                  index;//结点大小,

       unsigned int                  free_index;//空余容量的大小,索引大小

       char                             *first_avail;//空余内存块的起始地址

       char                             *endp;//空余内存块的末尾地址

};

内存结点的结构如图所示,next指向下一个内存结点,形成链表,ref为指向自身的指针的指针,指向上一个结点的next指针,上一个结点的next指针指向本节点;index大小固定,表示该结点总大小的索引值;free_index大小随着结点空间使用而减小,表示剩余空间索引值的大小,active链表中会根据这个值进行排序,大的在前,小的在后;first_avail指针指向可用空间的头部,endp指向可用空间的尾部;

2.4 active活动链表

Apache内存池解析

图2-4active中内存结点连接方式

       Active活动链表挂载着内存池结点使用的内存块,按照free_index大小逆序排列,申请内存先判断active第一个结点够不够用,不够再向内存池分配子申请大块内存;内存池结点所在的内存块也会参与内存分配,是active的第一个内存结点块;

2.5 Appach内存池的优缺点

优点:

(1)       内存池开始为空,在内存申请和归还的过程中建立起内存池管理的链表,按需分配,不是一开始就分配一堆;

(2)       内存池管理的结点可以循环利用,减少直接向系统申请和释放,节省了系统的开销,提高程序性能;

(3)       内存池可以设置上限,内存池不会占用超过限额的内存;内存池按照内存池结点来管理内存,每个内存池结点对应一个有固定生命周期的应用,应用结束,销毁内存池结点,应用中占用的内存都会归还给内存池,不会有内存泄露的问题,但前提是内存池结点要正确的创建和销毁;

缺点:

(1)       内存池管理的空闲内存和程序正在使用的内存之和为程序使用内存的最大峰值,而且程序会一直占用最大峰值内存,内存池从系统申请的内存会一直增大到上限,超过上限时,将超过的部分归还给系统。内存池中管理的空闲内存无法在长期闲置的情况下归还给系统。

(2)       Active指向的链表中,结点的排序是按照index来排序,排序的最小单位是4kb的大小粒度,粒度太大,如果第二个结点只剩3byte,后面的结点都是4095byte,他们的index都相同,但是因为第二个节点为3byte,其后的所有结点的内存都无法被分配,造成内存空间的浪费。

(3)       是按照程序中不同应用的生命周期来建立内存池结点,例如一个http连接,通过内存池结点来给http连接中用到的内存进行分配,则在该http连接的整个生命周期内,占用的内存都无法归还给内存池;内存池的创建和销毁也是非常麻烦,要考虑内存池结点的生命周期。如果内存池结点没有销毁,则该内存池结点下挂载的内存都会泄露。

(4)       内存池有多个内存池结点,内存池结点之间公用一个锁,在多线程的程序中,锁竞争也会影响内存申请释放的性能。

 

 

 

 

详细请参考大神的博客

http://blog.csdn.net/klinymao/article/details/54783409

 

来源:bclshuai

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年10月12日
下一篇 2020年10月12日

相关推荐