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;
 
    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,则重复前面的步骤;

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

如果你觉得本文不错,请点击文后的“推荐本文”链接!!
阅读更多

没有更多推荐了,返回首页