logo资料库

Apache源代码全景分析.pdf

第1页 / 共228页
第2页 / 共228页
第3页 / 共228页
第4页 / 共228页
第5页 / 共228页
第6页 / 共228页
第7页 / 共228页
第8页 / 共228页
资料共228页,剩余部分请下载后查看
2.3 内存池分配子allocator
2.3.1分配子概述
2.3.2分配子创建与销毁
2.3.3分配子内存分配
2.3.4分配子内存释放
2.3.5分配子内存管理流程
2.4 内存池
2.4.1内存池概述
2.4.2内存池的初始化
6.1 Apache进程概述
6.1.1进程数据结构描述
6.1.2进程属性描述
6.1.2.1Unix平台
6.1 多进程并发处理概述
6.1.1 概述
6.2 MPM公共数据结构
6.2.1记分板
6.2.1.1记分板概述
6.1.1.2记分板处理函数
6.1.1.2.1创建记分板
6.1.1.2.2记分板插槽管理
6.1.1.2.3记分板内存释放
Apache 内存池内幕(1) 对于 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 内存池内幕(2) 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); } Apache 内存池内幕(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;
分享到:
收藏