性能文章>【全网首发】nginx内存分配与释放——堆(内存池)源码与原理(二)>

【全网首发】nginx内存分配与释放——堆(内存池)源码与原理(二)原创

2年前
5831415

nginx大堆的使用

​ 在上一篇文中,我们讲解了nginx的堆如果创建,如何从nginx堆中申请空间,其中讲解了基于nginx小堆分配内存,今天讲解的则是基于大堆的内存分配。

​ 老规矩,放一张nginx堆结构全览图,这张图会贯穿2篇文章,如果没有阅读前一篇的文章可以移步阅读nginx关于小堆的分配:

上一篇:nginx内存分配与释放—堆(内存池)源码与原理(一)
当前篇:nginx内存分配与释放—堆(内存池)源码与原理(二)

nginx架构图nginx数据结构之堆.drawio.png

在大堆中分配内存

​ nginx的堆中包括了大堆和小堆,那么怎么在大堆中分配空间的呢?其实在core包ngx_palloc.c中提供了在大堆中分配空间的函数。

//大堆内存的描述对象
struct ngx_pool_large_s {
    //用户构成链表
    ngx_pool_large_t     *next;
    //指向真正的大堆内存
    void                 *alloc;
};


/**
 * 从大堆中分配空间,其实所谓的大堆,就是超过小堆最大的分配空间大小的空间请求
 * 在ngx_pool_s的max存放了小堆能分配的最大空间,超过max则在大堆分配
 * 大堆也是连续的链表,每个大堆只能分配一次空间,也就是说用户请求size大小的空间,就建立一个size的大堆给用户
 * pool是当前的堆,size是需要分配的空间大小
 */
static void * ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;
    //从内存中申请size大小的内存
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;
    //遍历大内存堆对象,将创建的大堆空间加入到链表中
    for (large = pool->large; large; large = large->next) {
        //这里为什么存在large->alloc是空的呢,这里其实就是大堆的释放,
      	//释放大堆时,并不会将大堆的描述对象释放,只是释放了alloc所指向的空间
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        //如果遍历了3个大堆对象都存在实际堆内存,则结束当前循环
      	//如果不加以限制,每次都将新的大堆空间加入到链表末尾,那么当链表过长,效率就会越低
        if (n++ > 3) {
            break;
        }
    }
    //走到这一步,则说明大堆链表中前3个节点都是分配了实际的大堆空间的,并且没被回收
  	//那么,就新建一个描述大堆的对象,或者叫结构体实例,注意,这个ngx_pool_large_t实例存放在小堆上
  	//ngx_palloc_**all是在小堆上分配内存,没看过ngx_palloc_**all的可以看我前面写的一篇关于小堆的分配
    large = ngx_palloc_**all(pool, sizeof(ngx_pool_large_t), 1);
    //如果分配失败,则释放原来申请的大堆内存
    if (large == NULL) {
				//如果小堆分配不了内存,则说明没有内存了,只有释放了大堆
        ngx_free(p);
        return NULL;
    }
    //指向分配的大堆地址
    large->alloc = p;
    //将大堆的描述对象实例作为头节点,加入到堆的大堆链表中
    large->next = pool->large;
    pool->large = large;
  
    return p;
}

​ 老规矩,我们也来根据上面代码编写一个简易的java版本:

//大堆对象,=ngx_pool_large_s
class LargeHeap{
  	Large_Heap next;
  	memory [] alloc;
}

//堆,=ngx_pool_s
class Heap{
  //小堆链表的头节点
  LittleHeap d;
  //能在小堆分配空间的最大大小限制,超过max的则会在大堆分配
  int max;
  //当前可用的小堆
  LittleHeap current;
 	//大堆链表的头节点
  LargeHeap large;
}



//从heap中分配size大小的大堆
public static byte[] ngx_palloc_large(Heap heap,int size){
  	//申请size大小的内存
		memory[] p=Unsafe.allocateMemory(size);
  	//获取堆中的大堆链表头节点
  	LargeHeap large = heap.large;
  	int n=0;
  	for(;large!=null;large = large.next){
      	if(large.alloc==null){
          large.alloc=p;
          return p;
        }
      	if(n++ > 3){
          break;
        }
    }
  	//从小堆中分配一个可以存储LargeHeap对象大小的空间
		large = ngx_palloc_**all(heap, LargeHeap.class);
  	large.alloc = p;
  	//从头插入大堆
  	large.next = heap.large;
  	heap.large = large;
  	return p;
}

​ 如果经过多次的大堆内存分配,此时堆的内存结构会如下图:

nginx架构图创建多个大堆后的内存结构.drawio.png

实际上nginx如果分配内存

​ 我们已经讲解了从小堆和大堆中分配内存,但从大堆分配还是从小堆分配内存并不是由我们调用这些函数决定,因为我们现在讲解的函数前面都使用了static关键字修饰,在c语言中,static修饰的函数或者变量具有文件作用域,什么事文件作用域呢,文件作用域就是只能当前文件内进行使用,可以映射为在java中私有方法。在nginx堆的使用中提供的公开的函数名字叫ngx_palloc和ngx_pnalloc,这两个函数一个分配地址对齐的内存一个分配地址无需对齐的内存,我们来看下这两个函数。


/**
 * 分配内存
 */
void * ngx_palloc(ngx_pool_t *pool, size_t size){
  	//如果请求的内存超过小堆能分配的最大内存,则在大堆分配
    if (size <= pool->max) {
      	//使用地址对齐的方式在小堆分配内存
        return ngx_palloc_**all(pool, size, 1);
    }
		//在大堆分配
    return ngx_palloc_large(pool, size);
}

/**
 * 分配一个大堆
 */
void *ngx_pnalloc(ngx_pool_t *pool, size_t size){
  	//如果请求的内存超过小堆能分配的最大内存,则在大堆分配
    if (size <= pool->max) {
      	//不用地址对齐
        return ngx_palloc_**all(pool, size, 0);
    }
		//在大堆分配
    return ngx_palloc_large(pool, size);
}

内存释放与回收

​ 在上面的学习中,我们得知,小堆的内存分配是通过移动last指针来实现的,这种指针移动的方式也决定了nginx小堆无法实现随机回收,nginx堆如此设计也与nginx的应用场景有关,nginx堆的生命周期往往与request和tcp连接保持一致,所以当请求结束或连接关闭时,直接销毁堆。

释放大堆内存

​ 大堆内存是可以随机回收的,nginx提供了下面的函数来实现大堆的回收

//释放大堆内存,p为需要释放的大堆内存地址
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;
    //遍历大堆内存对象
    for (l = pool->large; l; l = l->next) {
        //如果大堆内存地址等于传入的p的地址,则释放这个大堆内存
      	//找到释放的大堆,这里只是将执行的大堆内存释放,并没有释放大堆头对象
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            //将大堆内存释放,并置为NULL
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}
重置内存池

​ nginx提供了重置堆的函数,代码如下:


/**
 * 重置堆,大堆释放,小堆移动last指针
 */
void ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;
		//遍历大堆链表,将大堆头指向的大堆内存释放
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }
		//遍历小堆,将小堆的last指针还原到未分配的状态
    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
      	//分配失败次数还原为0
        p->d.failed = 0;
    }

    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

销毁堆
//释放内存池
void ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;
    //遍历销毁时的回调函数
    for (c = pool->cleanup; c; c = c->next) {
        if (c->handler) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }
    //遍历大堆对象,并释放大堆的内存
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }
    //遍历小堆对象,并释放小堆的内存,这里并不是重置last指针,而是释放了小堆内存,这里也会把大堆的头释放
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}

写到最后: nginx堆或者说内存池的原理与核心api在这就已经讲完了,其中还有部分非核心的功能,包括销毁堆的回调函数,包括对堆的读写辅助器,可以讲堆变成一个buf缓冲区,类似netty中的ByteBuf的功能,这些功能实现留给喜欢阅读的人自己研究。

对此文有任何疑惑或者纠错,可以添加作者微信,加入作者答疑解惑群进行提问。

image.png

点赞收藏
阿译长官

如果批评都不自由,那么赞美还有什么意义

请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

Redis stream 用做消息队列完美吗?

Redis stream 用做消息队列完美吗?

Netty源码解析:writeAndFlush

Netty源码解析:writeAndFlush

15
4