Apache内存池内幕(7)

来源:互联网 发布:智能聊天女友软件 编辑:程序博客网 时间:2024/06/03 17:08
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;
    }
}

关于作者
张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!

如果你觉得本文不错,请点击文后的“推荐本文”链接!!
原创粉丝点击