对于APR中的所有的对象中,内存池对象应该是其余对象内存分配的基础,不仅是APR中的对象,而且对于整个Apache中的大部分对象的内存都是从内存池中进行分配的,因此我们将把内存池作为整个APR的基础。
2.1 内存池概述
在C语言中,内存管理的问题臭名昭著,一直是开发人员最头疼的问题。对于小型程序而言,少许的内存问题,比如内存泄露可能还能忍受,但是对于Apache这种大负载量的服务器而言,内存的问题变得尤其重要,因为丝毫的内存泄露以及频繁的内存分配都可能导致服务器的效率下降甚至崩溃。
通常情况下,内存的分配和释放通常都是mallloc和free显式进行的。这样做显得单调无味,同时也可能充满各种令人厌恶的问题。对同一块内存的多次释放通常会导致页面错误,而一直不释放又导致内存泄露,并且使得服务器性能大大下降。
为了在大而且复杂的Apache中避免内在的内存管理问题,Apache的开发者创建了一套基于池概念的内存管理方案,最后这套方法移到APR中成为通用的内存管理方案。
在这套方案中,核心概念是池的概念。Apache中的内存分配的基本结构都是资源池,包括线程池,套接字池等等。内存池通常是一块很大的内存空间,一次性被分配成功,然后需要的时候直接去池中取,而不需要重新分配,这样避免的频繁的malloc操作,而且另一方面,即时内存的使用者忘记释放内存或者根本就不想分配,那么这些内存也不会丢失,它们仍然保存在内存池中,当内存池被销毁的时候这些内存将自动的被销毁。
由于Apache中的大部分资源的分配都是从内存池中分配的,因此对于大部分的Apache函数,如果其内部需要进行资源分配,那么它的函数参数中总是会带有一个内存池参数,该内存池参数指明分配内存来自的内存池,比如下面的两个函数:
APR_DECLARE(apr_array_header_t *) apr_array_copy(apr_pool_t *p,const apr_array_header_t *arr);
APU_DECLARE_NONSTD(apr_status_t) apr_bucket_setaside_noop(apr_bucket *data,apr_pool_t *pool);
由于在函数的内部需要进行内存分配,因此这两个函数的参数中都指定了一个apr_pool_t的结构,用以指名函数内存分配来自的内存池。在后面的大部分过程中我们对于该参数将不再做多余的解释。
Apache中的内存池并不是仅仅一个内存池,相反而是存在多个内存池,这些内存池之间形成层次结构。如果Apache中仅仅存在一个内存池的话,潜在的问题是所有的内存分配都来自这个池,而且最要命的这些内存必须在整个Apache关闭时候才被释放,这一点显然不是那么合情合理,为此Apache中根据处理阶段的周期长短又引出了子内存池的概念,与之对应的是父内存池以及根内存池的概念,它们的唯一区别就是存在的周期的不同而已。比如对于HTTP连接而言,包括两种内存池:连接内存池和请求内存池。由于一个连接可能包含多个请求,因此连接的生存周期总是比一个请求的周期长,为此连接处理中所需要的内存则从连接内存池中分配,而请求则从请求内存池中分配。而一个请求处理完毕后请求内存池被释放,一个连接处理后连接内存池被释放。根内存池在整个Apache运行期间都存在。Apache中一个内存池的层次结构图可以大致如下描述:
内存池的层次图
2.2 内存池分配结点
在了解内存池的概念之前,我们首先了解一些内存池分配结点的概念。为了能够方便的对分配的内存进行管理,Apache中使用了内存结点的概念来描述每次分配的内存块。其结构类型则描述为apr_memnode_t,该结构定义在文件Apr_allocator.h中,其定义如下:
/** basic memory node structure */
struct apr_memnode_t {
apr_memnode_t *next; /**< next memnode */
apr_memnode_t **ref; /**< reference to self */
apr_uint32_t index; /**< size */
apr_uint32_t free_index; /**< how much free */
char *first_avail; /**< pointer to first free memory */
char *endp; /**< pointer to end of free memory */
};
该结点类型是整个Apache内存管理的基石,在后面的部分我们将其称之为“内存结点类型”或者简称为“内存结点”或者“结点”。在该结构中,不同的结点之间通过next指针形成结点链表;另外当在结点内部的时候为了方便引用结点本身,成员变量中还引入了ref,该变量主要用来记录当前结点的首地址,即使身在结点内部,也可以通过ref指针得到该结点并对该结点进行操作。
从上面的结构中可以看出事实上在apr_memnode_t结构内部没有任何的“空闲空间”来容纳实际分配的内存,事实上,它从来不单独存在,总是依附于具体的分配的内存单元。通常情况下,一旦分配了实际的空间之后,Apache总是将该结构置于整个单元的最顶部,如图3.1所示。
图3.1 内存结点示意
在上图中,我们可能调用malloc函数分配了16K大小的空间,为了能够将该空间用Apache的结点进行记录,我们将apr_memnode_t置于整个空间的头部,此时剩下的可用空间大小应该为16K-sizeof(apr_memnode_t),同时结构中还提供了first_avail和end_p指针分别指向这块可用空间的首部和尾部。当这块可用空间被不断利用时,first_avail和end_p指针也不断随之移动,不过(end_p-first_avail)之间则永远是当前的空闲空间。上图的右边部分演示了这种布局。
通常情况下,其分配语句大致如下:
apr_memnode_t* node;
node=(apr_memnode_t*)malloc(size);
node->next = NULL;
node->index = index;
node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
node->endp = (char *)node + size;
Apache中对内存的分配大小并不是随意的,随意的分配可能会造成更多的内存碎片。为此Apache采取的则是“规则块”分配原则。Apache所支持的分配的最小空间是8K,如果分配的空间达不到8K的大小,则按照8K去分配;如果需要的空间超过8K,则将分配的空间往上调整为4K的倍数。为此我们在程序中很多地方会看到下面的宏APR_ALIGN,其定义如下:
/* APR_ALIGN() is only to be used to align on a power of 2 boundary */
#define APR_ALIGN(size, boundary) \
(((size) + ((boundary) - 1)) & ~((boundary) - 1))
该宏所做的无非就是计算出最接近size的boundary的整数倍的整数。通常情况下size大小为整数即可,而boundary则必须保证为2的倍数。比如APR_ALIGN(7,4)为8;APR_ALIGN(21,8)为24;APR_ALIGN(21,16)则为32。不过Apache中用的最多的还是APR_ALIGN_DEFAULT,其实际上是APR_ALIGN(size,8)。在以后的地方,我们将这种处理方式称之为“8对齐”或者“4K对齐”或者类似。
因此如果对于APR_ALIGN_DEFAULT(sizeof(apr_memnode_t)),其等同于APR_ALIGN(sizeof(apr_memnode_t),8)。与之对应,APR中为了处理方便,同时也将apr_memnode_t结构的大小从sizeof(apr_memnode_t)调整为APR_ALIGH_DEFAULT(sizeof(apr_memnode_t))。在前面的部分我们曾经描述过,对于一块16K的内存区域,如果其用apr_memnode_t进行记录的话,实际的可用空间大小并不是16K-sizeof(apr_memnode_t),更精确地则应该是16K-APR_ALIGN_DEFAULT(sizeof(apr_memnode_t))。
因此如果我们看到Apache中的下面的语句,我们就没有什么好惊讶的了。
size = APR_ALIGN(size + APR_MEMNODE_T_SIZE, 4096);
if (size <8192)
size = 8192;
在上面的代码中我们将实际的常量都替换成实际的整数。APR_MEMNODE_T是对sizeof(apr_memnode_t)进行调整后的值。上面的语句所作的正是我们前面所说的分配策略:如果需要分配的空间累计结点头的空间总和小于8K,则以8K进行分配,否则调整为4K的整数倍。按照这种分配策略,如果我们要求分配的size大小为4192,其按照最小单元分配,实际分配大小为8192;如果我们要求分配的空间为8192,由于其加上内存结点头,大于8192,此时将按照最小单元分配4k,此时实际分配的空间大小为8192+4996=12K。这样,每个结点的空间大小都不完全一样,为此分配结点本身必须了解本结点的大小,这个可以使用index进行记录。
不过Apache记录内存的大小有自己的独特的方法。如果空间为12K,那么Apache并不会直接将12K赋值给index变量。相反,index只是记录当前结点大小相对于4K的倍数,计算方法如下:
index = (size >> BOUNDARY_INDEX) - 1;
这样如果index =5,我们就可以知道该结点大小为20K;反过来也是如此。通过这样方法,可以节省一定的存储空间,另一方面,也方便了程序处理。在后面的部分,我们将通过这种方法计算出来的值称之为“索引大小”,因此在后面的部分,我们如果需要描述内存结点大小的时候,我们直接称之为“索引大小为n”或者“大小为n”,后面不再赘述。与此相同,free_index则是定义了当前结点中的可用的空间的大小。
关于作者
张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!
如果你觉得本文不错,请点击文后的“推荐本文”链接!!
分享到:
相关推荐
### Apache内存池内幕 #### 内存管理的重要性与挑战 在C语言编程中,内存管理一向被视为技术领域的一座难攻堡垒,尤其是对于像Apache这样的高性能服务器软件,内存管理的效率直接影响到系统的稳定性和响应速度。...
在Apache内存池内幕分析文档中,你可能会了解到以下关键点: 1. **内存池的概念**:内存池是一种预先分配一大块内存,然后从中按需分配小块内存的策略。它避免了频繁的小内存分配和释放带来的开销。 2. **内存池的...
文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 内存管理是计算机编程最为基本的...
### 数据库连接池-连接的关闭内幕 在深入探讨数据库连接池中连接关闭的问题之前,我们首先需要了解数据库连接池的基本概念以及它在现代应用程序中的重要性。数据库连接池是一种管理多个数据库连接的方法,旨在提高...
轴类零件加工工艺设计.zip
资源内项目源码是来自个人的毕业设计,代码都测试ok,包含源码、数据集、可视化页面和部署说明,可产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果、标签分布图。都是运行成功后才上传资源,毕设答辩评审绝对信服的保底85分以上,放心下载使用,拿来就能用。包含源码、数据集、可视化页面和部署说明一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.txt文件,仅供学习参考, 切勿用于商业用途。
seaborn基本绘图人力资源数据集
移动机器人(sw三维)
自制html网页源代码查看器
3吨叉车的液压系统设计().zip
1_实验三 扰码、卷积编码及交织.ppt
北京交通大学软件学院自命题科目考试大纲.pdf
雅鲁藏布江流域 shp矢量数据 (范围+DEM).zip
基于RUST的数据结构代码示例,栈、队列、图等
NIFD:2024Q1房地产金融报告
详细介绍及样例数据:https://blog.csdn.net/li514006030/article/details/146916652
【工业机器视觉定位软件Vision-Detect】基于C#的WPF与Halcon开发的工业机器视觉定位软件(整套源码),开箱即用 有用户登录,图片加载,模板创建,通讯工具,抓边抓圆,良率统计,LOG日志,异常管理,九点标定和流程加载保存等模块,功能不是很完善,适合初学者参考学习。 资源介绍请查阅:https://blog.csdn.net/m0_37302966/article/details/146912206 更多视觉框架资源:https://blog.csdn.net/m0_37302966/article/details/146583453
内容概要:本文档详细介绍了Java虚拟机(JVM)的相关知识点,涵盖Java内存模型、垃圾回收机制及算法、垃圾收集器、内存分配策略、虚拟机类加载机制和JVM调优等内容。首先阐述了Java代码的编译和运行过程,以及JVM的基本组成部分及其运行流程。接着深入探讨了JVM的各个运行时数据区,如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区等的作用和特点。随后,文档详细解析了垃圾回收机制,包括GC的概念、工作原理、优点和缺点,并介绍了几种常见的垃圾回收算法。此外,文档还讲解了JVM的分代收集策略,新生代和老年代的区别,以及不同垃圾收集器的工作方式。最后,文档介绍了类加载机制、JVM调优的方法和工具,以及常用的JVM调优参数。 适合人群:具备一定Java编程基础的研发人员,尤其是希望深入了解JVM内部机制、优化程序性能的技术人员。 使用场景及目标:①帮助开发人员理解Java代码的编译和执行过程;②掌握JVM内存管理机制,包括内存分配、垃圾回收等;③熟悉类加载机制,了解类加载器的工作原理;④学会使用JVM调优工具,掌握常用调优参数,提升应用程序性能。 其他说明:本文档内容详尽,适合用作面试准备材料和技术学习资料,有助于提高开发人员对JVM的理解和应用能力。
Android项目原生java语言课程设计,包含LW+ppt
戴德梁行&中国房地产协会:2021亚洲房地产投资信托基金研究报告