【全网首发】nginx内存分配与释放——堆(内存池)源码与原理(一)原创
Nginx堆的创建与小堆的使用
在我从事java的五年里,我从来没有想过在编写java代码的时候,需要处理空间分配和回收相关的问题,但是在c的世界里,调用函数malloc或者zalloc分配内存,在使用完毕后需要对内存进行free,这也是编写c相关代码最容易遗漏的一个问题就是内存释放,那么在nginx中对内存的分配和释放是一种怎样的用法呢?相信读完这篇文章,会对你了解nginx的内存使用有所帮助,有关这块的知识会分为2篇文章发布,因为写到一篇文章中,篇幅过长,请谅解。
当前篇:nginx内存分配与释放—堆(内存池)源码与原理(一)
下一篇:nginx内存分配与释放—堆(内存池)源码与原理(二)
这里先放一张图片,接下来会结合这张图片来讲解nginx堆内存的分配和回收,大家现在看不懂这张图片,不用焦虑或者担心看不懂,因为这个图片需要阅读完我写nginx堆系列的文章后回头看结合记忆。
分配内存
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;
}
这是的内存抽象的视图是这样的:
如果在堆内存中存储对象或数据
我们在上面已经讲了怎么创建一个堆,那么我们现在有一个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()函数,则此时的堆的内存结构抽象图可能会如下图:
对此文有任何疑惑或者纠错,可以添加作者微信,加入作者答疑解惑群进行提问。