数据结构之内存池

C语言中内存管理是一直是程序员头痛的问题。对于小型程序,少许的内存问题,比如内存泄露和内存碎片还能忍受,但是大型软件或者服务器而言,内存的问题变得尤其重要,因为丝毫的内存泄露以及频繁的内存分配都可能导致服务器的效率下降甚至崩溃。内存分配号称性能四大杀手之一,其重要性不言而喻。一个好的内存分配策略,可以节省内存,减少内存碎片,提高性能和减少内存出错的可能性。
众所周知,c语言中分配内存的函数是malloc/free,使用这一对函数非常容易出问题:申请了忘记释放、重复释放导致内存错误、小内存分配导致的内存碎片。基于以上种种原因,Apache实现了自己一套内存池管理方案,并且移到APR通用库中,使之非常容易使用和移植。

内存池层次结构

在APR内存池中,核心概念是池,比如:用来分配请求的叫请求池,用来分配socket叫套接字池等。在Apache中不同的池组成一颗内存池树,与之对应就出现了跟内存池、父内存池和子内存子,他们的唯一区别只是生命周期不同而已。
为什么要这么设计,那就要从Apache的内存池的设计理念说起了,Apache内存池只有分配函数,没有释放函数,只有当内存池生命周期结束内存池释放,其分配的内存一并释放。如果只有一个内存池,那就是跟内存池,其分配的内存只有等Apache关闭才能释放,这样显然是不合理的。
比如 对于HTTP连接而言,包括两种内存池:连接内存池和请求内存池。由于一个连接可能包含多个请求,因此连接的生存周期总是比一个请求的周期长,为此连接处 理中所需要的内存则从连接内存池中分配,而请求则从请求内存池中分配。而一个请求处理完毕后请求内存池被释放,一个连接处理后连接内存池被释放。
Apache源码阅读:数据结构之内存池(未完待续)

内存节点

在介绍内存池前,首先要了解以下内存节点和分配子。内存节点是为了管理分配的内存而设计的数据结构,因此内存节点包含了内存的元信息。
内存节点apr_memnode_t数据结构定义:

	  typedef struct apr_memnode_t apr_memnode_t;
      /** basic memory node structure
      struct apr_memnode_t {
          apr_memnode_t *next;            /**< 指向下一个内存节点 */
          apr_memnode_t **ref;            /**< 指向前一个节点next,方便链表操作 */
          apr_uint32_t   index;           /**< 节点大小 */
          apr_uint32_t   free_index;      /**< 节点剩余空间大小*/
          char          *first_avail;     /**< 可用内存的第一个字节 */
          char          *endp;            /**< 指向内存最后一个字节 */
      };

为了方便管理,所有分配的节点通过next连接成链表,ref指向前一个元素的next,这样可以极大方便链表操作(老司机都懂)。有同学问,我没看见实际分配的内存啊,其实内存节点从来不是单独的,其是随着内存的分配而分配,说明白点,内存节点总是位于已分配内存的顶部,看结构图就懂了。
Apache源码阅读:数据结构之内存池(未完待续)
Apache内存池中,内存大小单位是4K,称为索引index大小,也叫索引、大小。比如index=1,则表示内存大小为index<<12 == 4K,并且内存分配大小也不是随意的,随意大小的内存分配会导致内存碎片,Apache采用的规则是“规则块”,简单来说就是最低分配8K(为什么是8K后面会解释,这里先卖个关子),大于8K则向上4K对齐即可。所以当你看到分配内存时:

if (size < 8192) {
    size = 8192;
}

也就不用奇怪了。

内存分配子

apr内存池并不是直接调用malloc/free进行内存分配和释放内存节点,而是通过一个叫内存分配子(以下称为分配子)的数据结构,内存池直接向分配子请求和释放内存,外部函数不得访问分配子,分配子则负责与系统调用内存分配交互,在linux下就是malloc/free了。
分配子apr_allocator_t数据结构定义:

   struct apr_allocator_t {
       /** free 存在内存节点的元素的最大下标,用于加快搜索 */
       apr_size_t        max_index;
	   /*
	  * 用于控制分配子拥有的内存大小
	  */
       apr_size_t        max_free_index;
       apr_size_t        current_free_index;
   // 使用线程时需要互斥锁
   #if APR_HAS_THREADS
       apr_thread_mutex_t *mutex;
   #endif /* APR_HAS_THREADS */
       // 指向内存池,后面会说
       apr_pool_t         *owner;
        /*
		*指向内存节点的数组,数组下标表示了内存节点的大小
		*元素0用于存放超出MAX_INDEX大小的节点,下标和节点大小关系如下
		* index = (size >> 12) -1
		* slot  0: nodes larger than 81920
        * slot  1: size  8192
        * slot  2: size 12288
        * ...
        * slot 19: size 81920
		*/
       apr_memnode_t      *free[MAX_INDEX];
   };

结构中最重要的结构就是free了,它是一个指针数组,元素指向了内存节点apr_memnode_t,元素下标有两层含义:
1 元素在free数组中的索引
2 表明了指向的内存节点的大小,索引index和内存节点大小size关系:
index = (size >>12)- 1
这条公式也就解释上面内存分配最少8k的疑问了,因为分配8K的内存空间,index刚好是最小值 1(0用作特殊用途)
MAX_INDEX定义为20,则index最大为19,所以规则块最大为:
size = (index + 1)<<12 = 80K
大于80K的规则块都会放到元素0处
max_index则是为了加速节点搜索,其存放的是free数组的下标,这个下标的元素不为空,且是非空元素的最大下标。
owner指向了内存池,这个内存池使用这个分配子来分配和释放内存。
没看懂,没关系,看结构图:
Apache源码阅读:数据结构之内存池(未完待续)
分配子内存分配的时候从free数组搜索一个可用的分配子,如果free链表中没有适合大小的节点则重新分配一个节点,释放的时候才挂载到free数组适当的位置上。
这样会导致一个问题,如果一个内存池分配大量的内存,当内存池释放的时候把内存节点返回给分配子,但是分配子只是把内存节点挂载到free上,并没有返回给操作系统,导致其他分配子内存不足。Apache后来增加了max_free_index变量和current_free_index变量来控制分配子拥有的内存。这两个变量是比较难理解的,我当初看的时候也比较懵。
current_free_index表示分配子当前分配出去内存大小,max_free_index表示分配子最多拥有的内存大小,满足关系current_free_index <= max_free_index
分配内存伪语言:

current_free_index += index(本次分配的内存大小)
if current_free_index > max_free_index:
	current_free_index = max_free_index;

释放内存伪语言:

if index(本次释放内存大小) <  current_free_index:
	current_free_index -= index;
	释放index到free数组
else:
	调用free释放index给操作系统

这样就可以控制分配子的内存始终在max_free_index范围内,max_free_index设为0表示不限制大小。

分配子操作函数

重要的宏定义

#define SIZEOF_ALLOCATOR_T  APR_ALIGN_DEFAULT(sizeof(apr_allocator_t))
// 8字节对齐
#define APR_ALIGN_DEFAULT(size) APR_ALIGN(size, 8)
#define APR_ALIGN(size, boundary) \
(((size) + ((boundary) - 1)) & ~((boundary) - 1))

创建内存分配子

    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 = 0;
    
        *allocator = new_allocator;
    
        return APR_SUCCESS;
    }

函数会把max_free_index置0,不限制内存大小

销毁分配子

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);
}

函数会释放free数组所有内存节点和自身

内存分配函数

 static APR_INLINE
    apr_memnode_t *allocator_alloc(apr_allocator_t *allocator, apr_size_t in_size)
    {
        apr_memnode_t *node, **ref;
        apr_uint32_t max_index;
        apr_size_t size, i, index;
    
        // 计算实际需要分配的内存大小,最少为8K,且是4K倍数
        size = allocator_align(in_size);
        if (!size) {
            return NULL;
        }
    
        /* 计算在free数组索引大小
         */
        index = (size >> BOUNDARY_INDEX) - 1;
        if (index > APR_UINT32_MAX) {
            return NULL;
        }
		// 如果free中已经存在满足大小的节点
        if (index <= allocator->max_index) {
    #if APR_HAS_THREADS
            if (allocator->mutex)
                apr_thread_mutex_lock(allocator->mutex);
    #endif /* APR_HAS_THREADS */
            /* 
             * 这里查找最合适大小的节点
             */
            max_index = allocator->max_index;
            ref = &allocator->free[index];
            i = index;
            while (*ref == NULL && i < max_index) {
               ref++;
               i++;
            }
    
            if ((node = *ref) != NULL) {
                /* node后面没有元素,且index >= max_index,
                * 则把当前元素分配出去之后,max_index的值需要更新为新的值
                 */
                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 + 1;
                if (allocator->current_free_index > allocator->max_free_index)
                    allocator->current_free_index = allocator->max_free_index;
    
    #if APR_HAS_THREADS
                if (allocator->mutex)
                    apr_thread_mutex_unlock(allocator->mutex);
    #endif /* APR_HAS_THREADS */
    
                goto have_node;
            }
    #if APR_HAS_THREADS
            if (allocator->mutex)
                apr_thread_mutex_unlock(allocator->mutex);
    #endif /* APR_HAS_THREADS */
        }
        // 如果在规则块中没有找到合适的就在元素0里面找
        else if (allocator->free[0]) {
    #if APR_HAS_THREADS
            if (allocator->mutex)
                apr_thread_mutex_lock(allocator->mutex);
    #endif /* APR_HAS_THREADS */
            //循环查询是否有合适大小的节点
            ref = &allocator->free[0];
            while ((node = *ref) != NULL && index > node->index)
                ref = &node->next;
            // 如果找到
            if (node) {
                *ref = node->next;
                allocator->current_free_index += node->index + 1;
                if (allocator->current_free_index > allocator->max_free_index)
                    allocator->current_free_index = allocator->max_free_index;
    
    #if APR_HAS_THREADS
                if (allocator->mutex)
                    apr_thread_mutex_unlock(allocator->mutex);
    #endif /* APR_HAS_THREADS */
    
                goto have_node;
            }
    /* 如果没有找到合适的节点,则向操作系统
     *  申请,这里不会立即大节点挂载到free数组,
     *  而是在释放内存的时候才挂载
     */
    if ((node = malloc(size)) == NULL)
        return NULL;
    node->index = index;
    node->endp = (char *)node + size;
    
    have_node:
        node->next = NULL;
        node->first_avail = (char *)node + APR_MEMNODE_T_SIZE;
  
        return node;
    }

分配函数可以清楚地看到max_index的用法,用过缓存这个最大值,在搜索合适节点时就可以少循环几次,在分配小节点的时候效果尤其明显。需要注意的是函数分配的是不会把新分配分配到的节点挂载到free数组,而是在释放内存的时候挂载。

内存释放函数

static APR_INLINE
void allocator_free(apr_allocator_t *allocator, apr_memnode_t *node)
{
    apr_memnode_t *next, *freelist = NULL;
    apr_uint32_t index, max_index;
    apr_uint32_t max_free_index, current_free_index;

#if APR_HAS_THREADS
    if (allocator->mutex)
        apr_thread_mutex_lock(allocator->mutex);
#endif /* APR_HAS_THREADS */

    max_index = allocator->max_index;
    max_free_index = allocator->max_free_index;
    current_free_index = allocator->current_free_index;

    do {
        next = node->next;
        index = node->index;
        // 是否需要释放内存到操作系统
        if (max_free_index != APR_ALLOCATOR_MAX_FREE_UNLIMITED
            && index + 1 > current_free_index) {
            node->next = freelist;
            freelist = node;
        }
        else if (index < MAX_INDEX) {
			// 放到合适的位置,并且更新max_index
            if ((node->next = allocator->free[index]) == NULL
                && index > max_index) {
                max_index = index;
            }
            allocator->free[index] = node;
            if (current_free_index >= index + 1)
                current_free_index -= index + 1;
            else
                current_free_index = 0;
        }
        else {
			// 挂载到元素0处
            node->next = allocator->free[0];
            allocator->free[0] = node;
            if (current_free_index >= index + 1)
                current_free_index -= index + 1;
            else
                current_free_index = 0;
        }
    } while ((node = next) != NULL);

    allocator->max_index = max_index;
    allocator->current_free_index = current_free_index;

#if APR_HAS_THREADS
    if (allocator->mutex)
        apr_thread_mutex_unlock(allocator->mutex);
#endif /* APR_HAS_THREADS */
    // 释放内存到操作系统
    while (freelist != NULL) {
        node = freelist;
        freelist = node->next;
        free(node);
    }
}

可以看到内存节点确实在释放时挂载到free数组

外部函数

外部并不是直接调用上述函数,而是再封装了一层:

APR_DECLARE(apr_memnode_t *) apr_allocator_alloc(apr_allocator_t *allocator,
                                                 apr_size_t size)
{
    return allocator_alloc(allocator, size);
}

APR_DECLARE(void) apr_allocator_free(apr_allocator_t *allocator,
                                     apr_memnode_t *node)
{
    allocator_free(allocator, node);
}

内存池代码在下一篇文章分析,未完待续。。。

相关文章:

  • 2022-01-04
  • 2021-09-12
  • 2021-08-27
  • 2022-12-23
  • 2021-07-01
  • 2021-10-22
  • 2021-05-14
  • 2021-10-11
猜你喜欢
  • 2021-11-27
  • 2022-12-23
  • 2021-08-17
  • 2021-10-07
  • 2021-08-27
  • 2021-12-25
  • 2021-11-15
相关资源
相似解决方案