【全网首发】nginx内存分配与释放——堆(内存池)源码与原理(二)原创
nginx大堆的使用
在上一篇文中,我们讲解了nginx的堆如果创建,如何从nginx堆中申请空间,其中讲解了基于nginx小堆分配内存,今天讲解的则是基于大堆的内存分配。
老规矩,放一张nginx堆结构全览图,这张图会贯穿2篇文章,如果没有阅读前一篇的文章可以移步阅读nginx关于小堆的分配:
上一篇:nginx内存分配与释放—堆(内存池)源码与原理(一)
当前篇:nginx内存分配与释放—堆(内存池)源码与原理(二)
在大堆中分配内存
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如果分配内存
我们已经讲解了从小堆和大堆中分配内存,但从大堆分配还是从小堆分配内存并不是由我们调用这些函数决定,因为我们现在讲解的函数前面都使用了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的功能,这些功能实现留给喜欢阅读的人自己研究。
对此文有任何疑惑或者纠错,可以添加作者微信,加入作者答疑解惑群进行提问。