Apache内存池内幕1-7

Apache内存池内幕(1)       
分类:            Apache源代码分析 14988人阅读 评论(33) 收藏 举报
对于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与之联系!
 

2.3 内存池分配子allocator

2.3.1分配子概述

尽管我们可以通过malloc函数直接分配apr_memnode_t类型的结点,不过Apache中并不推荐这种做法。事实上Apache中的大部分的内存的分配都是由内存分配子allocator完成的。它隐藏了内部的实际的分配细节,对外提供了几个简单的接口供内存池函数调用。内存分配子属于内部数据结构,外部程序不能直接调用。内存分配子(以后简称为分配子)在文件apr_pools.c中进行定义如下:
struct apr_allocator_t {
      apr_uint32_t        max_index;
      apr_uint32_t        max_free_index;
      apr_uint32_t        current_free_index;
#if APR_HAS_THREADS
      apr_thread_mutex_t  *mutex;
#endif /* APR_HAS_THREADS */
      apr_pool_t          *owner;
      apr_memnode_t     *free[MAX_INDEX];
};
该结构中最重要的无非就是free数组,数组的每个元素都是apr_memnode_t类型的地址,指向一个apr_memnode_t类型的结点链表。内存分配的时候则从实际的结点中进行分配,使用完毕后同时返回给分配子。
不过free中的链表中结点的大小并不完全相同,其取决于当前链表在free数组中的索引。此处free数组的索引index具有两层次的含义:第一层,该结点链表在数组中的实际索引,这是最表层的含义;另外,它还标记了当前链表中结点的大小。索引越大,结点也就越大。同一个链表中的所有结点大小都完全相等,结点的大小与结点所在链表的索引存在如下的关系:
结点大小 =  8K + 4K*(index-1)
因此如果链表索引为2,则该链表中所有的结点大小都是12K;如果索引为MAX_INDEX,即20,则结点大小应该为8K+4K*(MAX_INDEX-1)=84K,这也是Apache中能够支持的“规则结点”的最大数目。不过这个公式仅仅适用于数组中1到MAX_INDEX的索引,对于索引0则不适合。当且仅当用户申请的内存块太大以至于超过了规则结点所能承受的84K的时候,它才会到索引为0的链表中去查找。该链表中的结点通常都大于84K,而且每个结点的大小也不完全相同。
在后面的部分,我们将索引1到MAX_INDEX所对应的链表统称为“ 规则链表”,而每一个链表则分开称之为“ 索引n链表”,与之对应,规则链表中的结点则统称为“ 规则结点”,或者称则为“ 索引n结点”,这是因为它们的大小有一定的规律可遵循;而索引0对应的链表则称之为“ 索引0链表”,结点则称之为“ 索引0结点”。
根据上面的描述,我们可以给出分配子的内存结构如图3.2所示。
图3.2 分配子内存结构示意
理论上,分配子中的最大的结点大小应该为8K+4K*(MAX_INDEX-1),但实际却未必如此,如果从来没有分配过8K+4K*(MAX_INDEX-1)大小的内存,那么MAX_INDEX索引对应的链表很可能是空的。此时在分配子中我们用变量max_index表示实际的最大结点。另外如果结点过大,则占用内存过多,此时有必要将该结点返回给操作系统,分配子将max_free_index作为内存回收的最低门槛。如果该结点小于max_free_index,则不做任何处理,否则使用后必须进行释放给操作系统。current_free_index则是…。除此之外,mutex用户保证多线程访问时候的互斥,而owner则记录了当前分配子所属于的内存池。
针对分配子,Apache中提供了几个相关的函数,函数名称和作用简要概述如表格3.1。
表3.1 Apache中提供了分配子相关函数
分配子操作
函数名称
函数功能简单描述
创建
apr_allocator_create
创建一个新的分配子
销毁
apr_allocator_destroy
销毁一个已经存在的分配子
空间分配
apr_allocator_alloc
调用分配子分配一定的空间
空间释放
apr_allocator_free
释放分配子已经分配的空间,将它返回给分配子
其余设置
apr_allocator_owner_set
apr_allocator_owner_get
设置和获取分配子所属的内存池
apr_allocator_max_free_set
apr_allocator_set_max_free
设置和获取分配子内部的互斥变量

2.3.2分配子创建与销毁

分配子的创建是所有的分配子操作的前提,正所谓“毛之不存,皮将焉附”。分配子创建使用函数apr_allocator_create实现:
APR_DECLARE(apr_status_t) apr_allocator_create(apr_allocator_t **allocator)
{
    apr_allocator_t *new_allocator;
    *allocator = NULL;
    if ((new_allocator = malloc(SIZEOF_ALLOCATOR_T)) == NULL)
        return APR_ENOMEM;
    memset(new_allocator, 0, SIZEOF_ALLOCATOR_T);
    new_allocator->max_free_index = APR_ALLOCATOR_MAX_FREE_UNLIMITED;
    *allocator = new_allocator;
    return APR_SUCCESS;
}
分配子的创建非常的简单,它使用的函数则是最通常的malloc,分配大小为SIZEOF_ALLOCATOR_T即APR_ALIGN_DEFAULT(sizeof(apr_allocator_t))大小。当然这块分配的空间也包括了MAX_INDEX个指针变量数组。一旦分配完毕,函数将max_free_index初始化为APR_ALLOCATOR_MAX_FREE_UNLIMITED,该值实际为0,表明分配子对于回收空闲结点的大小并不设门槛,意味着即使结点再大,系统也不会回收。
创建后,结构中的max_inde,current_free_index都被初始化为0,这实际上是由memset函数隐式初始化的。一旦创建完毕,函数将返回创建的分配子。只不过此时返回的分配子中的free数组中不包含任何的实际的内存结点链表。
对分配子使用的正常的下一步就应该是对结构成员进行初始化。主要的初始化工作就是设置系统资源归还给操作系统的门槛max_free_index。在后面我们会看到,对于使用malloc分配的内存,如果其大小小于该门槛值,那么这些资源并不释放,而是归还给内存池,当内存池本身被释放的时候,这些内存才真正释放给操作系统;如果内存的大小大于这个门槛值,那么内存将直接释放给操作系统。这个门槛值的设置由函数apr_allocator_max_free_set完成:
APR_DECLARE(void) apr_allocator_max_free_set(apr_allocator_t *allocator,
                                             apr_size_t in_size)
{
    apr_uint32_t max_free_index;
    apr_uint32_t size = (APR_UINT32_TRUNC_CAST)in_size;
    max_free_index = APR_ALIGN(size, BOUNDARY_SIZE) >> BOUNDARY_INDEX;
    allocator->current_free_index += max_free_index;
    allocator->current_free_index -= allocator->max_free_index;
    allocator->max_free_index = max_free_index;
    if (allocator->current_free_index > max_free_index)
        allocator->current_free_index = max_free_index;
}
参数中的size经过适当的对齐调整赋值给分配子结构中的max_free_index。除了max_free_index之外,另外一个重要的成员就是current_free_index,该成员记录当前内存池中实际的最大的内存块大小。当然,它的值不允许超出max_free_index的范围。
与分配子的创建对应的则是分配子的销毁,销毁使用的是函数apr_allocator_destroy。当分配子被销毁的时候,我们需要确保下面两方面的内容都被正确的销毁:
(1)、分配子本身的内存被释放,这个可以直接调用free处理
(2)、由于分配子中内嵌的free数组都指向一个实际的结点链表,因此必须保证这些链表都被正确的释放。在释放链表的时候,通过一旦得到头结点,就可以沿着next遍历释放链表中的所有结点。
必须需要注意的是两种释放之前的释放顺序问题。正确的释放顺序应该是链表释放最早;其次才是分配子本身内存的释放。Apache中对应该部分是释放代码如下:
APR_DECLARE(void) apr_allocator_destroy(apr_allocator_t *allocator)
{
    apr_uint32_t index;
    apr_memnode_t *node, **ref;
    for (index = 0; index < MAX_INDEX; index++) {
        ref = &allocator->free[index];
        while ((node = *ref) != NULL) {
            *ref = node->next;
            free(node);
        }
    }
    free(allocator);
}
目录(?) [-]
  1. 分配子内存分配
  2. 分配子内存释放
  3. 分配子内存管理流程

2.3.3分配子内存分配

使用分配子分配内存是最终的目的。Apache对外提供的使用分配子分配内存的函数是apr_allocator_alloc,然而实际在内部,该接口函数调用的则是allocator_alloc。
allocator_alloc函数原型声明如下:
apr_memnode_t *allocator_alloc(apr_allocator_t *allocator, apr_size_t size)
函数的参数非常简单,allocator则是内存分配的时候调用的分配子,而size则是需要进行分配的大小。如果分配成功,则返回分配后的apr_memnode_t结构。
{
    apr_memnode_t *node, **ref;
    apr_uint32_t max_index;
    apr_size_t i, index;
    size = APR_ALIGN(size + APR_MEMNODE_T_SIZE, BOUNDARY_SIZE);
    if (size < MIN_ALLOC)
        size = MIN_ALLOC;
    index = (size >> BOUNDARY_INDEX) - 1;
    if (index > APR_UINT32_MAX) {
        return NULL;
    }
函数所做的第一件事情就是按照我们前面所说的分配原则调整实际分配的空间大小:如果不满8K,则以8K计算;否则调整为4K的整数倍。同时函数还将计算该与该结点对应的索引大小。一旦得到索引大小,也就知道了结点链表。至此Apache可以去寻找合适的结点进行内存分配了。
从分配子中分配内存必须考虑下面三种情况:
(1)、如果需要分配的结点大小分配子中的“ 规则结点”能够满足,即index<=allocator->max_index。此时,能够满足分配的最小结点就是index索引对应的链表结点,但此时该索引对应的链表可能为空,因此函数将沿着数组往后查找直到找到第一个可用的不为空结点或者直到数组末尾。同时程序代码中还给出了另外一种策略以及不使用的原因:
NOTE: an optimization would be to check allocator->free[index] first and if no node is present, directly use allocator->free[max_index].  This seems like overkill though and could cause memory waste.
另外一种方案就是首先直接检查allocator->free[index],一旦发现不可用,直接使用最大的索引allocator->free[max_index],不过这种策略可能导致内存的浪费。Apache采用的则是“最合适”原则,按照这种原则,找到的第一个内存肯定是最合适的。下面的斜体代码所作的无非如此:
    if (index <= allocator->max_index) {
        max_index = allocator->max_index;
        ref = &allocator->free[index];
        i = index;
        while (*ref == NULL && i < max_index) {
           ref++;
           i++;
        }
当循环退出的时候,意味着遍历结束,这时候可能产生两种结果:第一,找到一个非空的链表,这时候我们可以进入链表内部进行实际的内存分配;第二,从index开始往后的所有的链表都是空的,至此,循环退出的时候i=max_index。这两种情况可以用下面的简图描述:
对于第一种情况,处理如下:
        if ((node = *ref) != NULL) {
            if ((*ref = node->next) == NULL && i >= max_index) {
                do {
                    ref--;
                    max_index--;
                }
                while (*ref == NULL && max_index > 0);
                allocator->max_index = max_index;
            }
            allocator->current_free_index += node->index;
            if (allocator->current_free_index > allocator->max_free_index)
                allocator->current_free_index = allocator->max_free_index;
            node->next = NULL;
            node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
            return node;
        }
(2)、如果分配的结点大小超过了 “规则结点”中的最大结点,函数将考虑索引0链表。索引0链表中的结点的实际大小通过成员变量index进行标记。
在通过next遍历索引0链表的时候,函数将需要的大小index和实际的结点的大小node->index进行比较。如果index>node->index,则明显该结点无法满足分配要求,此时必须继续遍历。一旦找到合适的可供分配的结点大小,函数将调整node->first_avail指针指向实际可用的空闲空间。另外还需要调整分配子中的current_free_index为新的分配后的值。
(3)、如果在free[0]链表中都找不到合适的空间供分配,那么此时只能“另起炉灶”了。函数能做的事情无非就是调用malloc分配实际大小的空间,并初始化结点的各个变量,并返回,代码如下:
if ((node = malloc(size)) == NULL)
        return NULL;
node->next = NULL;
node->index = index;
node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
node->endp = (char *)node + size;
下面我们来看一个Apache中典型的调用分配子分配空间的情况,下面的代码你可以在worker.c中找到:
apr_allocator_t *allocator;
apr_allocator_create(&allocator);
apr_allocator_max_free_set(allocator, ap_max_mem_free);
apr_pool_create_ex(&ptrans, NULL, NULL, allocator);
apr_allocator_owner_set(allocator, ptrans);
当我顺着这段代码往下阅读的时候,我曾经感觉到很困惑。当一个分配子创建初始,内部的free数组中的索引链表都为空,因此当我们在apr_pool_create_ex中调用node = allocator_alloc(allocator, MIN_ALLOC - APR_MEMNODE_T_SIZE)) == NULL的时候,所需要的内存就不可能来自索引链表内的结点中,而只能就地分配,这些结点一旦分配后,它们就作为内存池的结点而被使用,但是分配后的结点却并没有立即与free数组进行关联,即并没有对free数组中的元素进行赋值。这样,如果不将结点与free数组进行“挂接”,那么将永远都不可能形成图一所示链表结构。
那么它们什么时候才挂接到free数组中的呢?原来所有的挂接过程都是在结点释放的时候才进行的。

2.3.4分配子内存释放

正如前面所描述的,在分配内存的时候,Apache首先尝试到现有的链表中去查找适合的空间,如果没有适合的内存区域的话,Apache必须按照上述的分配原则进行实际的内存分配并使用。但是实际的内存块并不会立即挂接到链表中去,只有释放的时候,这些区域才挂接到内存中。所以从这个角度而言,分配子内存的释放并不是真正的将内存调用free释放,而将其回收到分配链表池中。
Apache中提供的内存释放函数是apr_allocator_free。不过该函数仅仅是对外提供的接口而已,在函数内存调用的则实际上是allocator_free。allocator_free函数的原型如下:
static APR_INLINE void allocator_free(apr_allocator_t *allocator, apr_memnode_t *node)
函数中,node是需要释放的内存结点,其最终归还给分配子allocator。
{
    apr_memnode_t *next, *freelist = NULL;
    apr_uint32_t index, max_index;
    apr_uint32_t max_free_index, current_free_index;
    max_index = allocator->max_index;
    max_free_index = allocator->max_free_index;
    current_free_index = allocator->current_free_index;
由于node不仅仅可能是一个结点,而且可能是一个结点链表,因此如果需要完全释放该链表中的结点,则必须通过结点中的next进行依次遍历,因此下面的循环就是整个释放过程的框架结构:
    do {
        next = node->next;
        index = node->index;
        ……
    } while ((node = next) != NULL);
对于每个结点,我们将根据它的索引大小(即内存大小)采取不同的处理策略:
(1)、如果结点的大小超过了完全释放的阙值max_free_index,那么我们就不能将其简单的归还到索引链表中,而必须将其完全归还给操作系统。函数将所有的这样的需要完全释放的结点保存在链表freelist中,待所有的结点遍历完毕后,只需要释放freelist就可以释放所有的必须释放的结点,如下所示:
        if (max_free_index != APR_ALLOCATOR_MAX_FREE_UNLIMITED
            && index > current_free_index) {
            node->next = freelist;
            freelist = node;
        }
如果max_free_index为APR_ALLOCATOR_MAX_FREE_UNLIMITED则意味着没有回收门槛。任何内存,不管它有多大,APR都不会将其归还给操作系统。
(2)、如果index<MAX_INDEX,则意味着该结点属于“规则结点”的范围。因此可以将该结点返回到对应的“规则链表”中。如果需要释放的结点的索引大小为index,则该结点将挂接于free[index]链表中。如果当前的free[index]为空,表明该大小的结点是第一个结点,此时还必须比较index和max_index。如果index>max_index,则必须重新更新max_index的大小,同时将该结点插入链表的首部,作为首结点,代码可以描述如下:
        else if (index < MAX_INDEX) {
            if ((node->next = allocator->free[index]) == NULL
                && index > max_index) {
                max_index = index;
            }
            allocator->free[index] = node;
            current_free_index -= index;
        }
(3)、如果结点超过了“规则结点”的范围,但是并没有超出回收结点的范围,此时我们则可以将其置于“索引0”链表的首部中。代码如下:
        else {
            node->next = allocator->free[0];
            allocator->free[0] = node;
            current_free_index -= index;
        }
待所有的结点处理完毕后,我们还必须调整分配子中的各个成员变量,包括max_index和current_free_index。同时不要忘记释放freelist链表。
    allocator->max_index = max_index;
    allocator->current_free_index = current_free_index;
    while (freelist != NULL) {
        node = freelist;
        freelist = node->next;
        free(node);
    }
当上面的工作都完成后,整个结点的释放也就完毕了。事实上整个内存池中的内存就是通过上面的不断地释放而构建起来的。一旦构建了内存池,下一次的时候则可以直接去内存池中获取了。

2.3.5分配子内存管理流程

根据上面的描述,我们现在来串起来看一些整个分配子工作的流程。假如存在下面一段代码:
1.  apr_allocator_t *allocator;
2.  apr_allocator_create(&allocator);
3.  apr_allocator_max_free_set(allocator, 0);//简单起见,不进行任何回收
4.  apr_memnode_t *memnode1 = apr_allocator_alloc(allocator, 3000);
5.  apr_allocator_free(memnode1);
6.  apr_memnode_t *memnode2 = apr_allocator_alloc(allocator, 3000);
7.  apr_allocator_free(memnode2);
8.  apr_memnode_t *memnode3 = apr_allocator_alloc(allocator, 3000);
9.  apr_allocator_free(memnode3);
当第一行执行完毕后,创建的分配子示意图如下图所以,该图中尚未有任何的内存块可供分配:
在第四行中,系统需要内存分配子分配2000字节的空间,但此时没有任何空间可供分配(index > allocator->max_index,同时allocator->free[0]==NULL),因此分配子将直接向操作系统索取8K的空间,剔除结构头的大小,实际可用的内存大小为8k-APR_MEMNODE_T_SIZE。
当执行完第五行的时候,该内存将被归还给分配子,同时保存在索引1链表中。下图中的虚线剔除后为释放前的状态,反之为释放后的状态。结果如下图:
现在我们来考虑第六行和第七行的执行结果。当再次向分配子申请3000K的内存的时候,经过计算发现,该内存必须到索引为1链表中去获取。如果索引1链表为NULL,则重复前面的步骤;
目录(?) [-]
  1. 内存池
    1. 内存池概述
    2. 内存池的初始化

2.4 内存池

2.4.1内存池概述

在了解了内存分配子的概念之后,我们其实已经了解了Apache中内存分配的细节了。不过Apache中内存的层次结构关系则是由内存池负责组织,其数据结构apr_pool_t定义在apr_pools.c中,定义如下:
struct apr_pool_t {
    apr_pool_t           *parent;
    apr_pool_t           *child;
    apr_pool_t           *sibling;
    apr_pool_t          **ref;  //用于指向内存池本身
    cleanup_t            *cleanups;
    apr_allocator_t      *allocator;
    struct process_chain *subprocesses;
    apr_abortfunc_t       abort_fn;
    apr_hash_t           *user_data;
    const char           *tag;
#if !APR_POOL_DEBUG
    apr_memnode_t        *active;
    apr_memnode_t        *self; /* The node containing the pool itself */
    char                 *self_first_avail;
#else /* APR_POOL_DEBUG */
    debug_node_t         *nodes;
    const char           *file_line;
    apr_uint32_t          creation_flags;
    unsigned int          stat_alloc;
    unsigned int          stat_total_alloc;
    unsigned int          stat_clear;
#if APR_HAS_THREADS
    apr_os_thread_t       owner;
    apr_thread_mutex_t   *mutex;
#endif /* APR_HAS_THREADS */
#endif /* APR_POOL_DEBUG */
#ifdef NETWARE
    apr_os_proc_t         owner_proc;
#endif /* defined(NETWARE) */
};
Apache中存在的内存池个数通常多于一个,它们之间形成树型层次结构。每个内存池所存储的内容以及其存储周期都不一样,比如连接内存池在整个HTTP连接期间存在,一旦连接结束,内存池也就被释放;请求内存池则周期要相对短,它仅仅在某个请求周期内存存在,一旦请求结束,请求内存池也就释放。不过每个内存池都具有一个apr_pool_t结构。
整个内存池层次树通过parent、child以及sibling三个变量构建起来。parent指向当前内存池的父内存池;child指向当前内存池的子内存池;而sibing则指向当前内存池的兄弟内存池。因此整个内存池树结构可以用图3.3描述:
无
图3.3 内存池层次树结构图
在上面的图中,我们只是表示了层次结构,因此只是用了child和sibling两个成员,而忽略的parent的变量。从上面的图中我们可以看出根结点具有n个孩子结点:child1,child2,child3…childn。而child1,child2,child3以及childn它们属于同一个父亲,而且处于层次树的同一层次,因此它们通过链表连接,互为兄弟结点。同样child10和child11都是child1的子内存池结点,互为兄弟结点。child21是child2的唯一的子结点。其余结点类似。
除此之外apr_pool_t结构中最重要的成员变量无非就是active了。
n0
                                                                                             图3.4
Apache中提供了大量的内存池管理函数,它们的功能和名称归纳在表格3.2中。

内存池操作

函数名称

函数功能简单描述

初始化

apr_pool_initialize

对内存池使用中需要的内部变量进行初始化

销毁

apr_pool_terminate

主要在终止内存池使用时销毁内部的结构

创建

apr_pool_create_ex

apr_pool_create_ex_debug

创建一个新的内存池,另外还包括一个调试版本

清除

apr_pool_clear

apr_pool_clear_debug

清除内存池中的所有的内存,另外包括一个调试版本

apr_pool_destroy

2.4.2内存池的初始化

内存池的初始化是通过函数apr_pool_initialize实现的,在内部函数完成了下面几件事情:
APR_DECLARE(apr_status_t) apr_pool_initialize(void)
{
    apr_status_t rv;
    if (apr_pools_initialized++)
        return APR_SUCCESS;
(1)、确保Apache中只创建一个全局内存池,为此,Apache中使用apr_pools_initialized进行记录。apr_pools_initialized初始值为0,初始化后该值更改为1。每次初始化之前都检查该值,只有值为0的时候才允许继续执行初始化操作,否则直接返回。通过这种手段可以确保只有一个全局内存池存在。
    if ((rv = apr_allocator_create(&global_allocator)) != APR_SUCCESS) {
        apr_pools_initialized = 0;
        return rv;
    }
    if ((rv = apr_pool_create_ex(&global_pool, NULL, NULL,
                                 global_allocator)) != APR_SUCCESS) {
        apr_allocator_destroy(global_allocator);
        global_allocator = NULL;
        apr_pools_initialized = 0;
        return rv;
    }
    apr_pool_tag(global_pool, "apr_global_pool");
(2)、创建了全局的分配子global_allocator,并使用全局分配子global_allocator创建了全局内存池global_pool,该内存池是所有的内存池的祖先。所有的内存池都从该内存池继承而来。它在整个Apache的生存周期都存在,即使重启机器,该内存池也不会释放。除非你把Apache彻底关闭。该内存池在系统中命名为“apr_gloabl_pool”。
    if ((rv = apr_atomic_init(global_pool)) != APR_SUCCESS) {
        return rv;
    }
#if APR_HAS_THREADS
    {
        apr_thread_mutex_t *mutex;
        if ((rv = apr_thread_mutex_create(&mutex,APR_THREAD_MUTEX_DEFAULT,global_pool)) != APR_SUCCESS) {
            return rv;
        }
        apr_allocator_mutex_set(global_allocator, mutex);
    }
#endif /* APR_HAS_THREADS */
    apr_allocator_owner_set(global_allocator, global_pool);
(3)、如果当前的操作系统允许多线程,为了确保内存池结构被多线程访问的时候的线程安全性,还必须设置apr_pool_t结构内的互斥锁变量mutex。最后的任务就是将内存分配子和内存池进行关联。

2.4.3内存池的创建
勿庸置疑,内存池的创建是内存池的核心操作之一。内存池创建函数的原型如下所示:
APR_DECLARE(apr_status_t) apr_pool_create_ex(apr_pool_t **newpool,
                                             apr_pool_t *parent,
                                             apr_abortfunc_t abort_fn,
                                             apr_allocator_t *allocator)
其中,newpool是需要创建的新的内存池,并且创建后的内存池通过该参数返回。parent则是当前创建的内存池的父亲;abort_fn指明了当创建失败的时候所调用的处理函数;allocator则是真正进行内存分配的分配子。
{
    apr_pool_t *pool;
    apr_memnode_t *node;
    *newpool = NULL;
    if (!parent)
        parent = global_pool;
    if (!abort_fn && parent)
        abort_fn = parent->abort_fn;
    if (allocator == NULL)
        allocator = parent->allocator;
在创建过程中,我们没有指定当前创建的内存池的父亲,则将其默认为父亲为根内存池global_pool,同时如果内存池关联的abort_fn和分配子allocator没有定义,那么也直接继承父辈的相关信息。
    if ((node = allocator_alloc(allocator,MIN_ALLOC - APR_MEMNODE_T_SIZE)) == NULL) {
        if (abort_fn)
            abort_fn(APR_ENOMEM);
        return APR_ENOMEM;
    }
    node->next = node;
    node->ref = &node->next;
    pool = (apr_pool_t *)node->first_avail;
    node->first_avail = pool->self_first_avail = (char *)pool + SIZEOF_POOL_T;
    pool->allocator = allocator;
    pool->active = pool->self = node;
    pool->abort_fn = abort_fn;
    pool->child = NULL;
    pool->cleanups = NULL;
    pool->free_cleanups = NULL;
    pool->subprocesses = NULL;
    pool->user_data = NULL;
    pool->tag = NULL;
在一切就绪之后,函数将必须首先创建apr_pool_t结构。但是前面我们曾经说过Apache中对所有内存的分配都是以内存结点apr_memnode_t进行分配的,而且每次分配的最小单元为8K,这对于创建apr_pool_t结构也不例外。因此函数将首先调用分配子allocator分配8K的内存,然后将最顶端的内存分配给apr_memnode_t结构。此时接着apr_memnode_t结构下面的内存才能继续分配给apr_pool_t,用来表示内存池结构,apr_pool_t结构之后才是真正可用的空间。在整个8K内存中,结点头和内存池头部分别占用的空间大小为APR_MEMNODE_T_SIZE和SIZEOF_POOL_T,因此用户真正可用的空间实际上只有(8k-APR_MEMNODE_T_SIZE-SIZEOF_POOL_T)大小了,至此我们还必须要调整apr_memnode_t中的first_avail指针和apr_pool_t结构中的self_first_avail指针指向真正可用空间。
经过两轮分配之后,8K内存的布局如图3.5所示:

一旦完成了内存池结点的分配工作,我们必须将其挂结到内存池层次树上。挂结的过程无非就是设置parent,child以及sibling的过程。
    if ((pool->parent = parent) != NULL) {
        if ((pool->sibling = parent->child) != NULL)
            pool->sibling->ref = &pool->sibling;
        parent->child = pool;
        pool->ref = &parent->child;
    }
    else {
        pool->sibling = NULL;
        pool->ref = NULL;
    }
    *newpool = pool;
挂结的过程可以分为下面几个步骤:
(1)、将当前的结点的parent指针指向父结点,即pool->parent = parent。
(2)、设定当前结点的sibing。sibing应该指向那些与当前结点处于同一层次,并且父结点也相同的结点,新的结点总是被插入到子结点链表的首部,插入通过下面的两句实现:
pool->sibling = parent->child;
parent->child = pool;
不过如果父结点为空,意味着该结点未有兄弟结点,故pool->sibling = NULL。
(3)、设置ref成员。在apr_pool_t中,ref用于指向
在内存池结点创建的过程中,我们可以看到,内存池创建后active仍然为空。因此当前内存池中能够被使用的内存仅仅为8k- APR_MEMNODE_T_SIZE-SIZEOF_POOL_T大小。如果用户从内存池中申请更多的内存的时候,很明显,此时必须通过active去扩展该内存池对应的内存结点。这一点我们可以在内存池的内存分配中看出来。
2.4.4内存池的内存分配
    从内存池中分配内存通过两个函数实现:apr_pcalloc和apr_palloc,这两个函数唯一的区别就是apr_pcalloc分配后的内存全部自动清零,而apr_palloc则省去了这一步的工作。
apr_palloc的函数原型如下所示:
APR_DECLARE (void *) apr_palloc (apr_pool_t *pool, apr_size_t size)
函数中pool是需要分配内存的内存池,size则是需要分配的内存的大小。一旦分配成功则返回分配内存的地址。
      在了解内存池的内存分配之前,我们应该对active链表有所了解。顾名思义,active链表中保存的都是曾经被使用或者正在被使用的apr_memnode_t内存结点。这些结点都是由分配子进行分配,之所以被使用,一个重要的原因就是它们有足够空闲的空间。将这些结点保存在active上,这样下次需要内存的时候只要首先遍历active链表即可,只有在active链表中的结点不能够满足分配要求的时候才会重新跟分配子申请新的内存。另一方面,一旦某个结点被选中进入active链表,那么它就不能在原先的分配子链表中存在。
     对于每一个apr_memnode_t内存结点,它的实际可用空间为endp-first_avail的大小。但是正如前面所说,Apache中衡量空间通常使用索引的方法,对于所有的结点,它的空闲空间用free_index描述。为了加快查找速度,active链表中的所有的结点按照其空间空间的大小进行反向排序,为此空闲空间大得总是排在前面,空闲空间最小的则肯定排在最末尾。对于指定的分配空间,只要将其与第一个结点的空闲空间进行比较,如果第一个空闲都不满足,那么此时必须向分配子重新申请空间,否则直接从第一个结点中分配空间,同时调整分配后的结点次序。
    apr_memnode_t *active, *node;
    void *mem;
    apr_size_t free_index;
    size = APR_ALIGN_DEFAULT(size);
    active = pool->active;
    if (size < (apr_size_t)(active->endp - active->first_avail)) {
        mem = active->first_avail;
        active->first_avail += size;
        return mem;
    }
分配首先计算需要分配的实际空间,这些空间都是使用对齐算法调整过的。Apache首先尝试到active链表的第一个结点中去分配空间,正如前面所言,这个是链表中空闲最多的结点,如果它能够满足需要,Apache直接返回size大小的空间,同时调整新的first_avail指针。不过这里需要注意的是对于空链表的情况。当一个内存池使用apr_create_pool_ex新创建以后,它的active链表为空,不过此时active并不为NULL,事实上active=node,意味着active指向内存池所在的内存结点。因此这种情况下,空间的分配并不会失败。
    node = active->next;
    if (size < (apr_size_t)(node->endp - node->first_avail)) {
        list_remove(node);
    }
    else {
        if ((node = allocator_alloc(pool->allocator, size)) == NULL) {
            if (pool->abort_fn)
                pool->abort_fn(APR_ENOMEM);
            return NULL;
        }
    }
如果active链表中的结点都不能满足分配需求,那么此时唯一能够做的就是直接向分配子申请更多的空间。至于分配子如何去分配,是从池中获取还是直接调用malloc分配,此处不再讨论。
    node->free_index = 0;
    mem = node->first_avail;
    node->first_avail += size;
    list_insert(node, active);
    pool->active = node;
    free_index = (APR_ALIGN(active->endp - active->first_avail + 1,
                            BOUNDARY_SIZE) - BOUNDARY_SIZE) >> BOUNDARY_INDEX;
    active->free_index = (APR_UINT32_TRUNC_CAST)free_index;
    node = active->next;
    if (free_index >= node->free_index)
        return mem;
一旦获取到分配的新的结点,那么下一步就是从该结点中分配需要的size大小的空间,由mem指针指向该空间首地址。同时将该结点立即插入到actie链表中作为首结点。插入通过宏list_insert实现:
#define list_insert(node, point) do {           /
    node->ref = point->ref;                     /
    *node->ref = node;                          /
    node->next = point;                         /
    point->ref = &node->next;                   /
} while (0)
前面我们说过,active链表的第一个结点肯定是空闲空间最大的结点。尽管从池中刚分配的时候,node结点的空闲空间确实是最大,但是一旦分配了size大小之后则情况未必,因此node作为第一个结点存在可能是不合适的,为此必须进行适当的调整。
1)、如果新插入结点node的空闲空间确实比后继结点node->next的空间大,那么此时,毫无疑问,node是所有结点中空闲空间最大的结点,物归其所,不需要再调整。
    do {
        node = node->next;
    }
    while (free_index < node->free_index);
    list_remove(active);
    list_insert(active, node);
2)、如果node的空间比node->next的空间小,那么意味着node放错了地方,为此必须从node->next开始往后遍历找到合适的位置,并从原位置移出,插入新位置。
2.4.5内存池的销毁
由于Apache中所有的内存都来自内存池,因此当内存池被销毁的时候,所有从内存池中分配的空间都将受到直接的影响——被释放。但是不同的数据类型可能导致不同的释放结果,目前Apache中支持三种不同的数据类型的释放:
1)、普通的字符串数据类型
这类数据类型是最简单的数据类型,对其释放可以直接调用free而不需要进行任何的多余的操作
2)、带有析构功能的数据类型
这类数据类型类似于C++中的对象。除了调用free释放之外还需要进行额外的工作,比如apr_socket_t结构,它是与套接字的描述结构,除了释放该结构之外,还必须close套接字。
3)、进程数据类型
APR中的进程数据类型用结构apr_proc_t进行描述,当然它的分配内存也来自内存池。通常一个apr_proc_t对应一个正在运行的进程,因此从内存池中释放apr_proc_t结构的时候必然影响到正在运行的进程,如果处理释放和进程的关系是内存释放的时候必须考虑的问题。
下面我们详细描述每一个中内存销毁策略
2.4.5.1 带有析构功能的数据类型的释放
Apache2.0内存池中目前存放的数据种类非常繁多,既包括最普通的字符串,又包含各种复杂的数据类型,比如套接字、进程和线程描述符、文件描述符、甚至还包括各种复杂的自定义数据类型。事实上这是必然的结果。Apache中倡导一切数据都尽量从内存池中分配,而实际需要的数据类型则千变万化,因此内存池中如果出现下面的内存布局则不应该有任何的惊讶:

在上面的图示中,从内存池中分配内存的类型包括apr_bucket_brigade,apr_socket_t,apr_file_t等等。一个很明显而且必须解决的问题就是如何释放这些内存。当内存池被释放的时候,内存池中的各种数据结构自然也就被释放,这些都很容易就可以实现,比如free(apr_socket_t)、free(apr_dir_t)。不过有的时候情况并不是这么简单。比如对于apr_socket_t,除了释放apr_socket_t结构之外,更重要的是必须关闭该socket。这种情况对于apr_file_t也类似,除了释放内存外,还必须关闭文件描述符。这项工作非常类似于对象的释放,除了释放对象本身的空间,还需要调用对象的析构函数进行资源的释放。
因此正确的资源释放方式必须是能够识别内存池中的数据类型,在释放的时候完成与该类型相关的资源的释放工作。某一个数据结构除了调用free释放它的空间之外,其余的应该采取的释放措施用数据结构cleanup_t描述,其定义如下:
struct cleanup_t {
struct cleanup_t *next;
const void *data;
apr_status_t (*plain_cleanup_fn)(void *data);
apr_status_t (*child_cleanup_fn)(void *data);
};
该数据结构通常简称为清除策略数据结构。每一个结构对应一个处理策略,Apache中允许一个数据类型对应多个策略,各个处理策略之间通过next形成链表。data则是清除操作需要的额外的数据,由于数据类型的不确定性,因此只能定义为void*,待真正需要的时候在进行强制类型转换,通常情况下,该参数总是为当前操作的数据类型,因为清除动作总是与具体类型相关的。另外两个成员则是函数指针,指向真正的清除操作函数。child_cleanup_fn用于清除该内存池的子内存池,plain_cleanup_fn则是用于清除当前的内存池。
为了能够在释放的时候调用对应的处理函数,首先必须在内存池中注册指定类型的处理函数。注册使用函数apr_pool_cleanup_register,注册函数原型如下:
APR_DECLARE(void) apr_pool_cleanup_register(apr_pool_t *p, const void *data,
                      apr_status_t (*plain_cleanup_fn)(void *data),
                      apr_status_t (*child_cleanup_fn)(void *data))
p是需要注册cleanup函数的内存池,当p被释放时,所有的cleanup函数将被调用。Data是额外的数据类型,通常情况下是注册的数据类型,plain_cleanup_fn和child_cleanup_fn的含义与cleanup_t结构中对应成员相同,因此假如需要在全局内存池pglobal中注册类型apr_socket_t类型变量sock的处理函数为socket_cleanup,则注册过程如下:
apr_pool_cleanup_register(pglobal,(void*)sock,socket_cleanup,NULL);
    cleanup_t *c;
    if (p != NULL) {
        if (p->free_cleanups) {
            c = p->free_cleanups;
            p->free_cleanups = c->next;
        } else {
            c = apr_palloc(p, sizeof(cleanup_t));
        }
        c->data = data;
        c->plain_cleanup_fn = plain_cleanup_fn;
        c->child_cleanup_fn = child_cleanup_fn;
        c->next = p->cleanups;
        p->cleanups = c;
    }
注册过程非常简单,无非就是将函数的参数赋值给cleanup_t结构中的成员,同时将该结点插入到cleanup_t链表的首部。
apr_pool_cleanup_kill函数与apr_pool_cleanup_register相反,用于将指定的cleanup_t结构从链表中清除。
因此如果需要对一个内存池进行销毁清除操作,它所要做的事情就是遍历该内存池对应的cleanup_t结构,并调用plain_cleanup_fn函数,该功能有静态函数run_cleanups完成,其对应的代码如下:
static void run_cleanups(cleanup_t **cref){
cleanup_t *c = *cref;
while (c) {
*cref = c->next;
(*c->plain_cleanup_fn)((void *)c->data);
c = *cref;
}
}
2.4.5.2进程描述结构的释放
尽管关于APR进程的描述我们要到后面的部分才能详细讨论,不过在这部分,我们还是首先触及到该内容。APR中使用apr_proc_t数据结构来描述一个进程,同时使用apr_procattr_t结构来描述进程的属性。通常一个apr_proc_t对应系统中一个正在运行的进程。
由于Apache的几乎所有的内存都来自内存池,apr_proc_t结构的分配也毫不例外,比如下面的代码将从内存池p中分配apr_proc_t和apr_procattr_t结构:
apr_proc_t   newproc;
apr_pool_t   *p;
apr_procattr_t *attr;
rv = apr_pool_initialize();
rv = apr_pool_create(&p, NULL);
rv = apr_procattr_create(&attr, p);
问题是当内存池p被销毁的时候,newproc和attr的内存也将被销毁。系统应该如何处理与newproc对应的运行进程。Apache中支持五种处理策略,这五种策略封装在枚举类型apr_kill_conditions_e中:
typedef enum {
    APR_KILL_NEVER,             /**< process is never sent any signals */
    APR_KILL_ALWAYS,            /**< process is sent SIGKILL on apr_pool_t cleanup */
    APR_KILL_AFTER_TIMEOUT,     /**< SIGTERM, wait 3 seconds, SIGKILL */
    APR_JUST_WAIT,              /**< wait forever for the process to complete */
    APR_KILL_ONLY_ONCE          /**< send SIGTERM and then wait */
} apr_kill_conditions_e;
APR_KILL_NEVER:该策略意味着即使进程的描述结构apr_proc_t被释放销毁,该进程也不会退出,进程将忽略任何发送的关闭信号。
APR_KILL_ALWAYS:该策略意味着当进程的描述结构被销毁的时候,对应的进程必须退出,通知进程退出使用信号SIGKILL实现。
APR_KILL_AFTER_TIMEOUT:该策略意味着当描述结构被销毁的时候,进程必须退出,不过不是立即退出,而是等待3秒超时候再退出。
APR_JUST_WAIT:该策略意味着描述结构被销毁的时候,进城必须退出,但不是立即退出,而是持续等待,直到该进程完成为止。
APR_KILL_ONLY_ONCE:该策略意味着当结构被销毁的时候,只发送一个SIGTERM信号给进程,然后等待,不再发送信号。
现在我们回过头来看一下内存池中的subprocesses成员。该成员定义为process_chain类型:
struct process_chain {
    /** The process ID */
    apr_proc_t *proc;
    apr_kill_conditions_e kill_how;
    /** The next process in the list */
    struct process_chain *next;
};
对于内存池p,任何一个进程如果需要从p中分配对应的描述数据结构apr_proc_t,那么它首先必须维持一个process_chain结构,用于描述当p被销毁的时候,如何处理该进程。Process_chain的成员很简单,proc是进程描述,kill_how是销毁处理策略,如果存在多个进程都从p中分配内存,那么这些进程的process_chain通过next形成链表。反过来说,process_chain链表中描述了所有的从当前内存池中分配apr_proc_t结构的进程的销毁策略。正因为进程结构的特殊性,因此如果某个程序中需要使用进程结构的话,那么第一件必须考虑的事情就是进程的退出的时候处理策略,并将其保存在subprocess链表中,该过程通过函数apr_pool_note_subprocess完成:
APR_DECLARE(void) apr_pool_note_subprocess(apr_pool_t *pool, apr_proc_t *proc,
                                           apr_kill_conditions_e how)
{
    struct process_chain *pc = apr_palloc(pool, sizeof(struct process_chain));
    pc->proc = proc;
    pc->kill_how = how;
    pc->next = pool->subprocesses;
    pool->subprocesses = pc;
}
apr_pool_note_subproces的实现非常简单,无非就是对process_chain的成员进行赋值,并插入到subprocess链表的首部。
比如,在URI重写模块中,需要将客户端请求的URI更改为新的URI,如果使用map文件进行映射的话,那么根据请求的URI到map文件中查找新的URI的过程并不是由主进程完成的,相反而是由主进程生成子进程,然后由子进程完成的,下面是精简过的代码:
static apr_status_t rewritemap_program_child(…)
{
    ……
    apr_proc_t *procnew;
    procnew = apr_pcalloc(p, sizeof(*procnew));
    rc = apr_proc_create(procnew, argv[0], (const char **)argv, NULL,procattr, p);
    if (rc == APR_SUCCESS) {
            apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT);
            ……
    }
    ……
}
从上面的例子中可以看出,即使描述结构被删除,子进程也必须3秒后才被中止,不过3秒已经足够完成查询操作了。
同样的例子可以在下面的几个地方查找到:Ssl_engine_pphrase.c文件的577行、Mod_mime_magic.c文件的2164行、mod_ext_filter.c文件的478行、Log.c的258行、Mod_cgi.c的465行等等。
现在我们来考虑内存池被销毁的时候处理进程的情况,所有的处理由free_proc_chain完成,该函数通常仅仅由apr_pool_clear或者apr_pool_destroy调用,函数仅仅需要一个参数,就是process_chain链表,整个处理框架就是遍历process_chain链表,并根据处理策略处理对应的进程,描述如下:
static void free_proc_chain(struct process_chain *procs)
{
    struct process_chain *pc;
    if (!procs)
        return; /* No work. Whew! */
    for (pc = procs; pc; pc = pc->next) {
        if(pc->kill_how == APR_KILL_AFTER_TIMEOUT)
               处理APR_KILL_AFTER_TIMEOUT策略;
        else if(pc->kill_how == APR_KILL_ALWAYS)
               处理APR_KILL_ALWAYS策略;
        else if(pc->kill_how == APR_KILL_NEVER)
               处理APR_KILL_NEVER策略;
        else if(pc->kill_how == APR_JUST_WAIT)
               处理APR_JUST_WAIT策略;
        else if(pc->kill_how == APR_KILL_ONLY_ONCE)
               处理APR_KILL_ONLY_ONCE策略;
    }
}
2.4.5.3 内存池释放
在了解了cleanup函数之后,我们现在来看内存池的销毁细节。Apache中对内存池的销毁是可以通过两个函数和apr_pool_clear和apr_pool_destroy进行的。我们首先来看apr_pool_clear的细节:
APR_DECLARE(void) apr_pool_clear(apr_pool_t *pool)
{
    apr_memnode_t *active;
内存池的销毁不仅包括当前内存池,而且包括当前内存池的所有的子内存池,对于兄弟内存池,apr_pool_destroy并不处理。销毁按照深度优先的原则,首先从最底层的销毁,依次往上进行。函数中通过递归调用实现深度优先的策略,代码如下:
while (pool->child)
apr_pool_destroy(pool->child);
apr_pool_destroy的细节在下面描述。对于每一个需要销毁的内存池,函数执行的操作都包括下面的几个部分:
    run_cleanups(&pool->cleanups);
    pool->cleanups = NULL;
    pool->free_cleanups = NULL;
(1)、执行run_cleanups函数遍历与内存池关联的所有的cleanup_t结构,并调用各自的cleanup函数执行清除操作。
    free_proc_chain(pool->subprocesses);
    pool->subprocesses = NULL;
    pool->user_data = NULL;
(2)、调用free_proc_chain处理使用当前内存池分配apr_proc_t结构的进程。
    active = pool->active = pool->self;
    active->first_avail = pool->self_first_avail;
    if (active->next == active)
        return;
    *active->ref = NULL;
    allocator_free(pool->allocator, active->next);
    active->next = active;
    active->ref = &active->next;
(3)、处理与当前内存池关联的active链表,主要的工作就是调用allocator_free将active链表中的所有的结点归还给该内存池的分配子。
apr_pool_destroy函数与apr_pool_clear函数前部分工作非常的相似,对于给定内存池,它的所有的子内存池将被完全释放,记住不是归还给分配子,而是彻底归还给操作系统。两者的区别是对给定当前内存池节点的处理。apr_pool_clear并不会释放内存中的任何内存,而apr_pool_destroy则正好相反:
    if (pool->parent) {
        if ((*pool->ref = pool->sibling) != NULL)
          pool->sibling->ref = pool->ref;
    }
    allocator = pool->allocator;
    active = pool->self;
    *active->ref = NULL;
    allocator_free(allocator, active);
    if (apr_allocator_owner_get(allocator) == pool) {
        apr_allocator_destroy(allocator);
    }
如果当前内存池存在父内存池,那么函数将自己从父内存池的孩子链表中脱离开来。然后调用apr_allocator_free将内存归还给关联分配子。如果被释放的内存池正好是分配子的属主,那么属于该内存池的所有的分配子也应该被完全的销毁返回给操作系统。因此函数将调用apr_allocator_owner_get(allocator)函数进行判断。
在销毁分配子的时候有一点需要注意的是,由于需要判断当前分配子的是否属于当前的内存池,而内存池结构在检测之前已经被释放,因此,在释放内存池之前必须将其记录下来以备使用。如果缺少了这一步,allocator可能造成内存泄漏。
现在我们看一下run_cleanups函数,该函数很简单,无非就是遍历cleanup_t链表,并逐一调用它们的plain_cleanup_fn函数。
static void run_cleanups(cleanup_t **cref)
{
    cleanup_t *c = *cref;
    while (c) {
        *cref = c->next;
        (*c->plain_cleanup_fn)((void *)c->data);
        c = *cref;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值