04Nginx源码分析之内存池(ngx_palloc.c)
前言:
Nginx的内存管理是通过内存池来实现的。Nginx的内存池的设计非常的精巧,很多场景下,我们可以将Nginx的内存池实现抽象出来改造成我们开发中的内存池。
1 为何使用内存池
一般我们使用malloc/alloc/free等函数来分配和释放内存。但是直接使用这些函数会有一些弊端:
1)虽然系统自带的ptmalloc内存分配管理器,也有自己的内存优化管理方案(申请内存块以及将内存交还给系统都有自己的优化方案,具体可以研究一下ptmalloc的源码),但是直接使用malloc/alloc/free,仍然会导致内存分配的性能比较低。
2)频繁使用这些函数分配和释放内存,会导致内存碎片,不容易让系统直接回收内存。典型的例子就是大并发频繁分配和回收内存,会导致进程的内存产生碎片,并且不会立马被系统回收。
3)容易产生内存泄露。
使用内存池分配内存有几点好处:
1)提升内存分配效率。不需要每次分配内存都执行malloc/alloc等函数。
2)让内存的管理变得更加简单。内存的分配都会在一块大的内存上,回收的时候只需要回收大块内存就能将所有的内存回收,防止了内存管理混乱和内存泄露问题。
2 数据结构的定义
1)ngx_pool_t 内存池主结构。
/**
* Nginx 内存池数据结构
* 注:typedef ngx_pool_s ngx_pool_t
*/
struct ngx_pool_s {
ngx_pool_data_t d; /* 内存池的数据区域*/
size_t max; /* 最大每次可分配内存 */
ngx_pool_t *current; /* 指向当前的内存池指针地址,注意:它是会动的,可看block函数。*/
ngx_chain_t *chain; /* 缓冲区链表 */
ngx_pool_large_t *large; /* 存储大数据的链表 */
ngx_pool_cleanup_t *cleanup; /* 可自定义回调函数,清除内存块分配的内存 */
ngx_log_t *log; /* 日志 */
};
2)ngx_pool_data_t 数据区域结构。
typedef struct {
u_char *last; /* 内存池中未使用内存的开始节点地址 */
u_char *end; /* 内存池的结束地址 */
ngx_pool_t *next; /* 指向下一个内存池 */
ngx_uint_t failed;/* 失败次数 */
} ngx_pool_data_t;
3)ngx_pool_large_t 大数据块结构。
/**
* 注:typedef ngx_pool_large_s ngx_pool_large_t
*/
struct ngx_pool_large_s {
ngx_pool_large_t *next; /* 指向下一个存储地址 通过这个地址可以知道当前块长度 */
void *alloc; /* 当前大数据块指针地址 */
};
4)ngx_pool_cleanup_t 自定义清理回调的数据结构。
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; /* 清理的回调函数 */
void *data; /* 指向存储的数据地址 */
ngx_pool_cleanup_t *next; /* 下一个ngx_pool_cleanup_t */
};
5)ngx_pool_cleanup_file_t包含文件描述符的结构。
typedef struct {
ngx_fd_t fd;
u_char *name;
ngx_log_t *log;
} ngx_pool_cleanup_file_t;
3 数据结构图
这图画得不错,理解该图后很方便你了解Nginx的内存池数据结构。只不过作者在画左边的ngx_pool_t应该括起last~log的结构体部分,因为该结构体是包含ngx_pool_data_t结构的。
说明:
1)Nginx的内存池会放在ngx_pool_t的数据结构上(ngx_pool_data_t用于记录内存块block的可用地址空间和内存块尾部)。当初始化分配的内存块大小不能满足需求的时候,Nginx就会调用ngx_palloc_block函数来分配一个新的内存块,通过链表的形式连接起来。
2)当申请的内存大于pool->max的值的时候,Nginx就会单独分配一块large的内存块,会放置在pool->large的链表结构上。
3)pool->cleanup的链表结构主要存放需要通过回调函数清理的内存数据。(例如文件描述符)
4 具体函数实现
1)内存分配 ngx_alloc和ngx_calloc。
ngx_alloc和ngx_calloc 主要封装了Nginx的内存分配函数malloc,非常简单,前面也讲了。
/**
* 封装了malloc函数,并且添加了日志
*/
void *
ngx_alloc(size_t size, ngx_log_t *log)
{
void *p;
p = malloc(size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"malloc(%uz) failed", size);
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
return p;
}
/**
* 调用ngx_alloc方法,如果分配成,则调用ngx_memzero方法,将内存块设置为0
* #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
*/
void *
ngx_calloc(size_t size, ngx_log_t *log)
{
void *p;
p = ngx_alloc(size, log);
if (p) {
//将内存块全部设置为0
ngx_memzero(p, size);
}
return p;
}
2)创建内存池ngx_create_pool
调用ngx_create_pool这个方法就可以创建一个内存池。
/**
* 创建一个内存池,调用该函数的为注内存池,调用block开辟的称为次内存池,因为次内存池只有ngx_pool_data_t和数据,其余clean_up这些成员空间被数据占用。
*/
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log) {
ngx_pool_t *p;
/**
* ngx_memalign内部调用了C库的posix_memalign(void **memptr, size_t alignment, size_t size)
* 参1为开辟的内存返回首地址;参2为字节对齐的倍数即要求返回的首地址为该倍数,必须为2的幂次方;参3为开辟空间大小
*/
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
/**
* Nginx会分配一块大内存,其中内存头部存放ngx_pool_t本身内存池的数据结构
* ngx_pool_data_t p->d 存放内存池的数据部分(适合小于p->max的内存块存储)
* p->large 存放大内存块列表
* p->cleanup 存放可以被回调函数清理的内存块(该内存块不一定会在内存池上面分配)
*/
p->d.last = (u_char *) p + sizeof(ngx_pool_t); //实际存储数据的开始地址,指向ngx_pool_t结构体之后数据取起始位置
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0; //失败次数
size = size - sizeof(ngx_pool_t); //减去结构体大小,即实际的内存池大小
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;//内存池存储的数据空间开辟的最大值不能超过该宏,超过则以该宏为最大。方便将需要开大内存的处理交给ngx_pool_large_t
/* 只有缓存池的父节点,才会用到下面的这些,子节点只挂载在p->d.next,并且只负责p->d的数据内容,有兴趣的可以去nginx-hls模块探索*/
p->current = p; //保存当前内存池的指向
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
3)销毁内存池ngx_destroy_pool
/**
* 销毁内存池。
* 它销毁内存池也是比较简单的,先是将内存池中的cleanup链表和large链表先清理,最后再将内存池中的数据data清除。
* 实际上nginx的内存池也就是主要维护这三个链表。
*/
void ngx_destroy_pool(ngx_pool_t *pool) {
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
/* 首先清理pool->cleanup链表 */
for (c = pool->cleanup; c; c = c->next) {
/* handler 为一个清理的回调函数 */
if (c->handler) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"run cleanup: %p", c);
c->handler(c->data);
}
}
/* 清理pool->large链表(pool->large为单独的大数据内存块) */
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
if (l->alloc) {
ngx_free(l->alloc);
}
}
#if (NGX_DEBUG)
/*
* 只有等级为debug才会打印这里
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p, unused: %uz", p, p->d.end - p->d.last);
if (n == NULL) {
break;
}
}
#endif
/*
* 依次对每个内存池的data数据区域进行释放
* 下面for循环意思是:p先指向第一个内存池并且n指向下一个,然后清除p;对p自增也就是指向n,n继续指向下一个内存池......循环下去
*/
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
4)重设内存池ngx_reset_pool
/**
* 重设内存池
* 重设内存池实际做的工作是不对cleanup链表的数据处理;然后将大数据块链表free清除掉;
* 最后使每个内存池的有效数据指针d->last指向内存池开始的位置(跳过内存池的结构体大小)。
*/
void ngx_reset_pool(ngx_pool_t *pool) {
ngx_pool_t *p;
ngx_pool_large_t *l;
/* 清理pool->large链表(pool->large为单独的大数据内存块) */
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
pool->large = NULL;
/* 循环重新设置内存池data区域的 p->d.last;data区域数据并不擦除*/
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);//只是将last指向存储数据的开始位置,即开始图片log和use的线
}
}
5)使用内存池分配一块内存ngx_palloc和ngx_pnalloc
两者基本一样,唯一的区别是ngx_palloc会内存对齐,ngx_pnalloc不会内存对齐,即传0和传1的区别。
/*
* 在内存池中分配一块内存。
* 下面看到:palloc和pnalloc调用ngx_palloc_small时开辟的内存必定属于内存池链表。
* 而调用ngx_palloc_large开辟的内存必定属于大内存块链表。
* 所以调用两者返回的内存可能属于内存池或者大内存块。
*/
/*
* 函数逻辑很简单:
* 1)若开辟的内存小于max,则调用small获取内存
* 2)否则调用small获取大内存块
*/
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);//进行内存对齐
}
#endif
return ngx_palloc_large(pool, size);
}
void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 0);//不会进行内存对齐
}
#endif
return ngx_palloc_large(pool, size);
}
6)ngx_palloc_small函数
/*
* ngx_palloc_small函数的函数逻辑:
* 在已有内存池链表中寻找小内存块,如果没有则重新利用ngx_palloc_block开辟新的内存池从中获取。所以返回的内存必定属于内存池链表。
*/
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;
do {
m = p->d.last;
/*
* 内存对齐提高效率,但会消耗更多内存
* ngx_align_ptr为一个宏函数,每次返回对齐后的首地址,m是ngx_memalign返回一定倍数的首地址,这对大内存结构对齐是关键,后面会写一篇关于该宏的文章
*/
if (align) {
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
/*
* 在本内存池寻找是否有足够的内存,有则返回其首地址,即m
* 没有则遍历下一个内存池中继续寻找
*/
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m;
}
p = p->d.next;
} while (p);
//内存池没有可用的小内存块则开辟新的内存池
return ngx_palloc_block(pool, size);
}
7)ngx_palloc_block函数
/*
* 开辟一个交由内存池结构管理的内存。注:该函数开辟的内存就已经挂在内存池上。只不过返回值为新开辟的内存。
*
* 函数逻辑:
* 1)通过上一块内存池的大小复制新的内存池,但是除了父节点,子节点的内存池都只有ngx_pool_data_t一个数据块,其余内存只存储数据。
* 2)然后更新pool->current,使内存池的循环大大减少。
*/
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new;
psize = (size_t) (pool->d.end - (u_char *) pool);
//复制上一次的内存池,该函数会返回与NGX_POOL_ALIGNMENT成倍数的首地址,和上面的对齐宏是使大数据内存块对齐的关键
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//格式化为内存池结构,new为新的内存池首地址
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
/*
* 这里可以看出:
* new是只有ngx_pool_data_t成员的内存池,其余均用作存储数据即m+size
*/
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
//遍历内存池链表,每当超过4个就更新current,使得每次循环最多4次
//例如,有1,2,3,4,5,current此时指向5,当第6个来临时,只需要遍历2次链表即可而非遍历6次
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
//将新开辟的内存池放在链表末尾。注意:经过上面遍历,p->d.next此时指向尾部
p->d.next = new;
//返回刚刚申请的内存池有效内存首地址
return m;
}
8)ngx_palloc_large函数
/*
* 开辟一个大内存检查后交给大内存链表管理。所以开辟的内存必定在大内存链表上。
* 函数逻辑:
* 1)判断pool->large链表上查询是否有NULL的,只在链表上往下查询3次,主要判断大数据块是否有被释放的,有就给它赋值,如果没有则只能跳出。
* 2)然后往下新创建一个pool->large结构体,将刚开辟的空间赋给该新结构体管理。
*/
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
p = ngx_alloc(size, pool->log);//注意该函数是单独调用malloc,所以它的内存与内存池链表的内存是不连续的或者叫无关。
if (p == NULL) {
return NULL;
}
n = 0;
// 1)判断pool->large链表上查询是否有NULL的,只在链表上往下查询3次,主要判断大数据块是否有被释放的,有就给它赋值,如果没有则只能跳出
// 为何只查3次,因为插入是头插法,所以开发者认为开头比较容易出错,需要检查且最多三次,否则认为后面是都赋有内存。笔者认为这一步可以适当省略。(结果是真的有一个函数省略了该步骤的即ngx_pmemalign函数(上篇讲过))
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
//实际上第一次调用会走下面的逻辑,因为第一次pool->large=NULL,会跳出上面的循环
// 2)新建pool->large结构体管理新内存,注意:是创建结构体的大小,属于小内存块(不要以为调用ngx_palloc_small后会造成递归调用)
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
ngx_free(p);
return NULL;
}
large->alloc = p;//刚开辟的内存交给pool->large链表管理
large->next = pool->large;//插入新的pool->large结构体。注:插法是每次新的pool->large结构体插进第一个pool->large的后面。即pool->large->la3->la2->la1->NULL
pool->large = large;
return p;
}
9)ngx_pfree函数
/**
* 功能:指定释放大内存块链表的某一块大内存。
* 这里可以看到大内存的管理是支持释放某一块大内存的,所以上面的ngx_palloc_large函数每一次都检查前三个是否为空,确保前三个有内存空间可用,至于后面是否为空就只能不怎么关心了。
*/
ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;
//遍历大内存链表,若找到想要释放的大内存则释放,否则返回错误NGX_DECLINED
for (l = pool->large; l; l = l->next) {
if (p == l->alloc) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p", l->alloc);
ngx_free(l->alloc);
l->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
下面为cleanup机制,可以使用回调函数清理数据:
Nginx的内存池cleanup机制,设计的非常巧妙。pool->cleanup本身是一个链表,每个ngx_pool_cleanup_t的数据结构上,保存着内存数据的本身cleanup->data和回调清理函数cleanup->handler。
通过cleanup的机制,我们就可以在内存池上保存例如文件句柄fd的资源。当我们调用ngx_destroy_pool方法销毁内存池的时候,首先会来清理pool->cleanup,并且都会执行c->handler(c->data)回调函数,用于清理资源。
Nginx的这个机制,最显著的就是让文件描述符和需要自定义清理的数据的管理变得更加简单
10)ngx_pool_cleanup_add函数
/**
* 功能:分配一个可以用于回调函数清理内存块的内存。内存块仍旧在p->d或p->large上(因为调用的是ngx_palloc)
*
* 步骤:
* 1)创建一个新的ngx_pool_cleanup_t结构体并给其内部成员开辟内存空间。
* 2)使用头插法将新的结构体插入清理链表。
*
* 注意:初始化时回调c->handler设为NULL,并且返回值为返回当前结构体,所以该内存可以由用户自定义并且自行处理,非常灵活。
*
* 实际上该函数注意是用来添加以下两个内容:
* 1. 文件描述符
* 2. 外部自定义回调函数可以来清理内存
*/
*/
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
// 1)创建新的清理结构体和开辟空间
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size);//该函数调用samll或者large,所以内存块仍旧在p->d或p->large上
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
// 2)使用头插法插入清理链表,并且回调设为NULL等待用户设置。
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
11)ngx_pool_run_cleanup_file函数
/*
* 功能:清除p->cleanup链表上的某个已打开的文件描述符fd占用的内存块(或者叫清除指定的文件描述符)
* 下面看到,遍历清理链表,如果回调函数是清理文件描述符函数并且存在该打开的文件描述符fd,那么就删除该文件描述符占用的内存。
*/
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd) {
ngx_pool_cleanup_t *c;
ngx_pool_cleanup_file_t *cf;
// 遍历清理链表
for (c = p->cleanup; c; c = c->next) {
if (c->handler == ngx_pool_cleanup_file) {
cf = c->data;//因为为清理文件描述符,此时的c->data应为ngx_pool_cleanup_file_t的结构体类型
// 判断是否是指定要删除的fd
if (cf->fd == fd) {
c->handler(cf); /* 调用ngx_pool_cleanup_file回调函数清理指定内存cf */
c->handler = NULL;
return;
}
}
}
}
12)ngx_pool_cleanup_file函数
/*
* 该函数为ngx_pool_run_cleanup_file的回调函数。就是通过ngx_close_file里面去调用底层的close关闭掉对应的文件描述符。
* 这是ngx官方写的回调函数,只要是关于文件描述符的清理都调用该回调函数。而自定义内存的则由我们自己定义。
*/
void ngx_pool_cleanup_file(void *data) {
ngx_pool_cleanup_file_t *c = data;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
c->fd);
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
13)ngx_pool_delete_file函数
该函数是一个删除文件的回调函数,上面是清除文件描述符占用的内存的回调而已。
/*
* 相关定义:
* src/os/unix/ngx_files.h
* #define ngx_delete_file(name) unlink((const char *) name)
* #define ngx_close_file close
*
* 功能:上面是清理文件描述符的回调函数,这里是删除文件的回调函数,是连文件也删除。
* 该函数调用了ngx_delete_file和ngx_close_file进行删除文件。
* 1)调用了ngx_delete_file宏,而该宏调用底层的unlink删除文件。
* 2)调用了ngx_close_file宏,而该宏调用底层的close删除文件
*
*/
void
ngx_pool_delete_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_err_t err;
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
c->fd, c->name);
if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
err = ngx_errno;
if (err != NGX_ENOENT) {
ngx_log_error(NGX_LOG_CRIT, c->log, err,
ngx_delete_file_n " \"%s\" failed", c->name);
}
}
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}