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

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

2年前
5470614

Nginx堆的创建与小堆的使用

​ 在我从事java的五年里,我从来没有想过在编写java代码的时候,需要处理空间分配和回收相关的问题,但是在c的世界里,调用函数malloc或者zalloc分配内存,在使用完毕后需要对内存进行free,这也是编写c相关代码最容易遗漏的一个问题就是内存释放,那么在nginx中对内存的分配和释放是一种怎样的用法呢?相信读完这篇文章,会对你了解nginx的内存使用有所帮助,有关这块的知识会分为2篇文章发布,因为写到一篇文章中,篇幅过长,请谅解。

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

​ 这里先放一张图片,接下来会结合这张图片来讲解nginx堆内存的分配和回收,大家现在看不懂这张图片,不用焦虑或者担心看不懂,因为这个图片需要阅读完我写nginx堆系列的文章后回头看结合记忆。
nginx架构图nginx数据结构之堆.drawio.png

分配内存

​ nginx的内存主要由ngx_pool_t这个结构体进行管理,我们可以简单理解为ngx_pool_t映射为java的堆,只是nginx可以有多个堆,当然这里的堆也没有年轻代,年老代的概念,这是jvm自动垃圾收集才会产生的概念。

如何创建一个堆

​ 在core包里的ngx_palloc.c源文件中,提供了创建堆的函数,下面我们就来看下这个函数。


//小堆的结构体
typedef struct {
    //内存池最后使用的位置,也就是空闲的内存开始位置
    u_char               *last;
    //内存池内存区域的结束位置
    u_char               *end;
    //下一个内存池对象,源码中的结构体是ngx_pool_s,但是实际指向的事ngx_pool_data_t
    ngx_pool_data_t      *next;
    //从当前堆分配内存失败的次数,如果次数超过4次,则下次将不再尝试从此内存池分配内存
    ngx_uint_t            failed;
} ngx_pool_data_t;

//堆
struct ngx_pool_s {
    //小堆内存头节点
    ngx_pool_data_t       d;
    //小堆的最大空间,max会用来划分分配的空间在小堆还是大堆,超过max的则在大堆分配
    size_t                max;
    //当前内存池的指针
    ngx_pool_t           *current;
    ngx_chain_t          *chain;
    //大堆内存对象头节点
    ngx_pool_large_t     *large;
    //内存池销毁时的回调函数链表
    ngx_pool_cleanup_t   *cleanup;
    //日志对象
    ngx_log_t            *log;
};

/**
 * 创建堆,堆的大小为size,log是nginx日志对象
 */
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log){
    ngx_pool_t  *p;
    //申请size大小的内存,如果系统支持内存对齐,则默认申请16字节对齐的地址,
    // 这里的ngx_memalign使用的是posix标准接口来分配内存,关于posix可以百度,这是一个可移植操作系统标准
    //nginx为很多基础类型和函数做了封装,以保证跨平台,NGX_POOL_ALIGNMENT=16
  	//看不懂就理解为申请了一块size字节大小的空间
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    //因为申请失败==NULL,否则是不等于NULL会是申请的空间的内存地址
    if (p == NULL) {
        return NULL;
    }
    //p->d 这里的d其实是一个ngx_pool_data_t的结构体,也可以理解为ngx_pool_data_t类型对象
    //也就是说在ngx_pool_t这个堆对象里面,有一个属性d,这个d是ngx_pool_data_t类型的
    //ngx_pool_data_t这个结构体或者说这个类型主要用户描述一个小堆,这里的小堆不等于堆
    //实际上ngx_pool_t堆由数个小堆和数个大堆ngx_pool_large_t组成,这里不理解大堆,没关系,稍后会详细讲解大堆
  	//下面的操作实际都是对小堆这个对象的属性赋值,这里的last就是这个小堆可用空间的地址
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    //小堆的可用空间的结束地址,因为内存是连续的,这里其实last可以理解为数组0而end就是数组的.length-1
    p->d.end = (u_char *) p + size;
    //下一个小堆的指针
    p->d.next = NULL;
    //当前小堆的分配内存失败次数,分配失败是因为,我们要的内存>(d.end-d.last)空闲的内存,那么d.failed就会+1
    p->d.failed = 0;
    //堆区实际可用大小
    size = size - sizeof(ngx_pool_t);
    //堆的最大内存
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
    //当前堆或者小堆的指针
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    //日志对象
    p->log = log;
    //返回堆指针
    return p;
}

​ 如果用java的视角去讲解这个代码,就可以翻译成如下代码:

//分配的内存空间
memory[size]

//小堆,为什么有小堆,因为nginx的堆由数个小堆和数个大堆组成,小堆负责小内存的分配,大堆负责大内存的分配
class LittleHeap{
  //最后分配的空间下标+1,例如0空间被分配出去,那么last=0+1
  int last;
  //空间的最大下标,size-1
  int end;
  //下一个小堆
  LittleHeap next;
  //内存分配失败的次数,当前小堆分配不了,就会通过next找下一个小堆,看是否能够分配
  int failed;
  	
}

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

//创建堆,其实就是创建了一个size大小的小堆
public Heap create(int size){
  Memory memory =new Memory[size];
  if(memory==null){
    return null;
  }
  //注意,Heap对象占用的内存是上面的memory给的
  //sizeOf(Heap)就是获取这个Heap类型的对象要多内存,包括他的属性LittleHeap也会算进去、
  //0~(sizeof(Heap)-1),表示从0到Heap存储所需的下标
  memory[0~(sizeof(Heap)-1)] p=new Heap();
  //这里减去sizeof(Heap),是因为Heap是存到这里面的
  p.d.last = size-sizeof(Heap);
  p.d.end = size-1;
  p.d.failed=0;
  p.current=p.d;
  p.max=size;
  return p;
}

​ 这是的内存抽象的视图是这样的:

nginx架构图创建堆的内存抽象图.drawio.png

如果在堆内存中存储对象或数据

​ 我们在上面已经讲了怎么创建一个堆,那么我们现在有一个int类型的数据或对象需要存储,但是我们并不想存储在栈上,我们想存到堆上,我们该怎么办呢。

在堆中开辟空间

​ 在core包里的ngx_palloc.c源文件中,提供了分配一块内存的函数,下面我们就来看下这个函数

int main(int argc, char **argv) {
  //创建一个堆,并且堆中的第一个小堆大小为1MB
	ngx_pool_t * heap = ngx_create_pool(1024*1024,log);
  //从堆中分配一块小内存,注意这里的4是4byte,ngx_palloc_small在下面有解释
  int * p = ngx_palloc_small(heap,4,1);
  //这里的*p是解引用,因为*p是指针,所以p作为值类型的话,实际是一串内存地址值,*p就是通过p存放的地址值找到对应的空间
  *p=10;
  //输出p中的值
  printf("%d",*p);
	return 0;
}


//小块内存分配,size为申请的内存大小,ngx_uint_t是一个无符号long类型,align=1表示地址对齐
//如果所有的小堆都都没有空间分配,则新建一个内存池并追加到内存池最尾部
static ngx_inline void *ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char      *m;
    ngx_pool_t  *p;
    //获取当前有空间的小堆
    p = pool->current;
    //从p开始,遍历小堆链表,如果某个小堆可以分配,则返回此小堆空闲空间首地址,作为新分配的的内存开始
    do {
        //获取遍历小堆最后使用的位置
        m = p->d.last;
        //如果align=1,则需用地址对齐
        if (align) {
          	//如果是64位架构,这里会将m变成8的倍数,假设这里m原来的值26,指向下面宏(不知道宏就当成函数),m就会变成32
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
        //判断当前遍历小堆的空闲内存是否大于size,即d.end-(m=p->d.last)
        if ((size_t) (p->d.end - m) >= size) {
          	//移动d.last指针,需要移动size个位置,因为分配了size大小内存出去,不要跳着看,d.last上面有讲解
            p->d.last = m + size;
            //将空闲开始地址m返回(注意,这里的m其实可以被些越界,因为m是一个无类型指针,没学过c的可以不看本括号的内容)
            return m;
        }
        //获取下一个小堆
        p = p->d.next;
		//循环条件p!=null,c中指针==null就是false,不等于null就是true
    } while (p);
		//如果执行到这里,那就说明当前的小堆链表没有size大小的空间,那么就会调用下面的函数
    return ngx_palloc_block(pool, size);
}


//这个方法会新建一个和第一个小堆相同大小的小堆,并分配size大小空间返回分配空间的首地址
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size){
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;
    //获取第一个小堆的总大小,从这里可以看出,在创建堆的时候,传入的size就决定了所有的小堆大小都为size
    psize = (size_t) (pool->d.end - (u_char *) pool);
    //分配一块和第一个小堆相同大小的空间
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
  	//m==NULL则说明分配失败
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;
    //新的小堆的结束位置,psize是小堆的大小,m是小堆的开始地址
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
		//抛出ngx_pool_data_t大小,这部分存储的事小堆的头即new->d
    m += sizeof(ngx_pool_data_t);
    //地址对齐,和上面一样
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    //分配size大小空间,更新last指针位置
    new->d.last = m + size;
    //遍历所有小堆,并将failed++,能到这里都是因为没有小堆可以分配内存,所以当前的小堆链表都要把分配失败次数+1,并且将当前堆的current指针指向可以分配空间的小堆,有可能不是当前创建的小堆,因为只要分配失败次数failed小于4就认为还可以分配
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }
    //将新的小堆放到末尾
    p->d.next = new;
		//返回分配的空间开始地址
    return m;
}

如果看不懂上面的代码,我也写了一个简易的java版本的代码帮助理解:

public static void main(String [] args){
  	Heap heap = create(1024*1024);
  	byte [] intObj = ngx_palloc_small(heap,4);
  	//10的二进制数1010,这里没有考虑大端模式还是小端模式,如果懂大小端的,不要喷,不同系统大小端不一样,所以不考虑这个问题
  	intObj[0]=0b00000000;
  	intObj[1]=0b00000000;
  	intObj[2]=0b00000000;
  	intObj[3]=0b00001010;
  	System.out.println((int)intObj);
}

//使用到的类,请看上面,出于篇幅考虑,不重复编写
//从heap中分配size字节的空间数组
public static byte[] ngx_palloc_small(Heap heap,int size){
  	LittleHeap p = heap.current;
  	do{
      	//获取空闲空间开始的位置
      	int last = p.last;
      	//有空余空间大于等于size
      	if(size <= p.end-last){
          	p.last = last+size;
          	//返回从last到size的空间
          	return memory[last~size];
        }
      	p=p.next;
    }while(p!=null);
  	//执行到这里,则说明,小堆链表中,没有超过size的空间分配
  	//新建小堆,分配空间
  	return ngx_palloc_block(heap,size);
}

public static byte[] ngx_palloc_block(Heap heap,int size){
  	//获取第一个小堆的大小
  	int psize = heap.end-heap的开始地址;
  	//分配一块psize大小的内存块,这里的Unsafe是java提供的native的方法,主要通过系统调用来实现分配内存
  	memory = Unsafe.allocateMemory(psize);
  	//这里将这块内存分出一部分存储LittleHeap对象,c中实现结构化编程都需要这样,对象只是作为头,可以理解为tcp报文中的报文头,报文头后面才是存储的实际传输的数据
  	memory[0~sizeof(LittleHeap)] newLittle = new LittleHeap();
  	//这里去掉的就是newLittle占用的内存
  	newLittle.last=memory+sizeof(LittleHeap);
  	newLittle.failed=0;
  	LittleHeap p = heap.current;
  	for(;p.next!=null;p=p.next){
      if(p.failed++ > 4){
        //这里主要将分配失败次数超过4次的小堆移除
        heap.current=p.next;
      }
    }
  	//将新的小堆加入链表
  	p.next = newLittle;
  	return memory[last~size];
}

如果连续使用内存分配,并且都会调用到ngx_palloc_block()函数,则此时的堆的内存结构抽象图可能会如下图:

nginx架构图创建多个小堆后的内存抽象图.drawio.png

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

image.png

点赞收藏
分类:标签:
阿译长官

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

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

为你推荐

服务器之 ECC 内存的工作原理

服务器之 ECC 内存的工作原理

14
6