[原创]Apache apr中内存分配子的current_free_index成员作用

来源:互联网 发布:java scanner编码 编辑:程序博客网 时间:2024/04/30 12:26

1. 背景

这个问题最初起源于tingya的blog中apache代码分析系列文章 ( http://blog.csdn.net/tingya )。当对内存池部分进行分析时,涉及到了内存分配子(allocator)的内容。内存分配子结构体中包含一个叫做current_free_index的成员。对于该成员的具体作用,广大网友众说纷纭。相关的讨论可以在tingya的blog中看到( http://blog.csdn.net/tingya/archive/2005/12/23/559815.aspx )。

2. 历史

关于apr是何时把current_free_index成员放入内存分配子结构体中的,我google了一下,找到了这样一篇文章:

http://www.mail-archive.com/dev@apr.apache.org/msg06563.html

从文章中的补丁可以看出,在这之前,内存分配子结构体中是没有current_free_index和max_free_index这两个成员的。并且,那时的apache在内存分配子未被释放之前是不会释放内存分配子中保留的内存。而之所以加入这两个成员,目的就是为了控制内存分配子中保管内存的数量。至此,可以基本断定,current_free_index和max_free_index是协同工作来控制分配子中的内存。

3. current_free_index与max_free_index的作用

在tingya的blog上面的讨论中,有一个叫做allan 的网友对current_free_index的功能作了如下的描述:

current_free_index,是当前应该释放的index值(超过这个值就应该释放)
这个值是动态调整的,根据你系统中内存的使用情况来判断的
除了在设置max_free_index的函数中改变了这个值以外,
在其他的两个地方也有涉及到,分别是:
用分配子分配node的时候,current_free_index加上了该node的index值
用分配子释放node的时候,current_free_index减去了这个node的index值
在一般情况下,这个值一增一减是平衡的。
考虑一种极端情况:你调用max_free_set之后,max_free_index和current_free_index都不为0,然后一直向分配子申请内存
分配子的内存不够用了,需要向系统申请,这种内存只有在调用分配子释放的时候,归还到分配子的free链表中
当你不断释放的时候,current_free_index不断减少,相当于归还给系统的内存门槛在降低,因为这些内存是从操作系统申请而来的
如果不归还给操作系统,系统资源会耗尽(全部给了分配子)

这样,即使当你调用max_free_set设置了一个超大的内存归还门槛。你在向系统不断申请内存后调用分配子释放的时候
分配子也会逐渐调低current_free_index的门槛,把内存归还给操作系统
 
之后,allan又说了:
呵呵,其实刚才说的是表面现象,apr靠这样一种机制保证了,分配子只在内存中占有max_free_index这么多的空间,如果你向系统申请了超过这么的空间,apr只把不超过的部分留在自己的free链表中,超过的部分统统还给系统。

这就是靠我刚才说的使用current_free_index这样一个机制来实现的,呵呵。
 
至此,current_free_index的功能已经基本有了眉目。
而tingya在文章中对max_free_index的作用概括为:“如果结点的大小超过了完全释放的阙值max_free_index,那么我们就不能将其简单的归还到索引链表中,而必须将其完全归还给操作系统。
但是在通过阅读代码之后,可以发现,二人的说法虽然各有一定道理,但是对current_free_index和max_free_index的功能概括却并不准确。我在这里试着对这两个成员的功能作一个更加准确的概括:
max_free_index用于记录内存分配子中最多可以容纳的内存空间大小。
current_free_index用于记录内存分配子中当前可以继续容纳的内存空间的大小。
其实,allan已经基本对这两个功能进行了描述,但是current_free_index并非仅仅如他所说的,“是当前应该释放的index值(超过这个值就应该释放)
这两个变量控制了内存分配子内容量的大小。这样做是基于以下的事实:
内存分配子中保存的都是从操作系统申请来的,但是至今仍是闲置的内存空间。这些空间是以多个链表的形式保存的。这样的空间不能太大——否则大量的系统资源被占用却未被利用,影响了系统的效率;同时,这样的空间又不能太小——否则频繁地从操作系统内申请和释放内存同样会影响系统的效率。因此,内存分配子中保存的这些闲置内存的数量就要控制在一定范围之内。这样,通过max_free_index来记录内存分配子中最多可以容纳的内存空间大小。在调用apr_allocator_free函数释放一个分配子节点到分配子中时,会检查分配子中当前内存数,加上这个即将被释放的分配子节点携带的内存空间数之和是否超过了max_free_index。而因为current_free_index记录了内存分配子中当前可以继续容纳的内存空间大小,那么,只要检测这个即将释放的分配子节点携带的内存空间数是否大于current_free_index就好了。这个检测是通过判断 index > current_free_index 的值来完成的。
 
4. 关于current_free_index溢出的问题
 
在google的时候,我同样发现了这样的一篇文章:
http://marc2.theaimsgroup.com/?l=apache-httpd-dev&m=113600944405209&w=2
其中提到了:
We probably should have a check in apr_allocator_free() to
make sure that we're not causing an unsigned int overflow in
allocator->current_free_index, e.g.,
+ if (index > current_tree_index) {
+ current_tree_index = 0;
+ }
+ else {
current_free_index -= index;
+ }
按照如此说法,current_free_index对于溢出的检测的确存在问题。但是就此问题我也做了一个粗略的分析。
(1) 当max_free_index不等于0时
即,对于内存分配子中的内存大小作了限制。这时,当释放一个节点时,会执行以下if语句:
        if (max_free_index != APR_ALLOCATOR_MAX_FREE_UNLIMITED
            && index > current_free_index) {
            node->next = freelist;
            freelist = node;
        }
因为已经假设max_free_index不等于零,即,判断语句的前半部分为真,如果index > current_free_index,则会将分配子节点释放给操作系统而不对current_free_index进行修改。一旦index > current_free_index为假,那么执行current_free_index -= index也不会发生溢出,因为current_free_index - index >= 0。
(2) 当max_free_index等于0时
即,对于内存分配子中内存大小不做限制。那么很显然,既然不做限制,current_free_index也便没有了任何作用,它的溢出也就无所谓了。而如果在current_free_index溢出之后再通过apr_allocator_max_free_set调整回收门槛,由于current_free_index溢出变成了一个很大的整数,加上即将释放的节点的index后可能依然大于max_free_index,但是由于下面语句的存在:
    if (allocator->current_free_index > max_free_index)
        allocator->current_free_index = max_free_index;
所以current_free_index值会被设成max_free_index的值。
 
但是仍然存在一个问题:
假如通过apr_allocator_create创建了一个无回收门槛的分配子allocator,调用apr_allocator_alloc在allocator中申请了一个100K的节点。之后,通过调用apr_allocator_alloc将这个节点释放给allocator。此时,allocator->current_free_index已经溢出变为一个很大的正整数。最后再通过apr_allocator_max_free_set调整回收门槛为50K。调用结束,current_free_index和max_free_index值会相等,都表示与50K对应的索引值。但是,此时内存分配子中却存储着一个100K的节点。这样,current_free_index就不再表示当前分配子可以继续存储的内存空间大小。除非用户的再次申请大于或者等于100K的空间,否则分配子中的节点索引的和会大于max_free_index值。不过,这种调用方法(即,先申请 释放内存后再设置门槛)可以比较容易地避免。因此,这种内存浪费的问题也便不是很明显了。
 
注: 本人十一长假期间在家,手头的电脑不是自己的,只有windows系统且无开发环境,所以无法做相关的测试。因此所说的一些话仅仅是臆断,不能保证正确性。还望有相关测试条件的人员帮忙替我做完这个测试,特别是对我最后提出的那个疑问的测试。文章中不正确的地方,很可能是我自身疏忽的缘故,还望多多谅解。
5. 溢出问题的解决 
关于解决上述的current_free_index溢出问题,存在着两种方法(别着急,先别急着改代码,一会我会说原因)。
第一种,就是如第4节开头部分的补丁所示,修改allocator_free函数,在减少current_free_index前对减数和被减数进行检测,避免current_free_index溢出。
第二种,就是修改apr_allocator_max_free_set,在其中加入释放多余空间的功能。也就是说,如果max_free_index设置的值表示为a,则将内存分配子中的节点释放直到其中的总大小小于等于a为止。并调整相应的current_free_index值。
比较起来,第一种方案似乎容易实现些。
但是,为什么apache项目组在05年收到补丁后却迟迟不肯修改自己的代码呢?我个人认为是有以下原因的:
1. current_free_index溢出的可能性不是很大。正如我在前面一节说的,它的溢出只有在特定的调用顺序和参数的前提下才会发生,而且会随着下次调用apr_allocator_alloc而逐渐消除溢出对系统的影响。
2. apr_allocator_free这个函数调用的频率相对较高,它执行的效率直接影响着整个系统的效率。如果要在其中加入什么代码,必须要经过深思熟虑——哪怕是一个简单的if-else。
3. 正如前面一节所说的,current_free_index溢出是在特定的函数调用顺序和参数的情况下才发生。概括起来,可以说是在申请并释放大量内存后,调用apr_allocator_max_free_set,将本是无回收门槛的分配子设置上门槛。但是,凭借常识可以知道,在一系列大量申请并释放内存后,系统往往会逐渐地恢复到申请小片内存。换句话说,一般的系统,很少在申请大量内存过后,突然间改变成仅仅申请少量的内存而不再申请大片内存。这之间往往会存在一个过渡期,因此,在这个过渡期中,在分配子里面存储一些过剩的内存空间以备不时之需往往是有必要的。况且,这种内存溢出带来的问题可以在调用一次或几次apr_allocator_alloc之后得到消除。如果在调用apr_allocator_max_free_set时便一下子把内存分配子中的节点释放到相应的数量,那么既消耗了系统资源(因为多次调用了free,把节点占用的内存还给操作系统),又不能应付以后可能发生的过渡时期。因此,这么做显然不是非常理智。
如前所述,消除溢出的方法的确存在,但是比起现在的做法,不如让溢出继续保留——反正这种溢出发生的可能性不是很大,而且非常容易避免。
最后,感谢tingya对apache代码的分析,我受益匪浅。
感谢allan对current_free_index的总结。
感谢各位网友的积极讨论。
 
原创粉丝点击