目录
nginx内存池源码剖析
一、nginx介绍
Nginx 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名,其特点是占有内存少,并发能力强,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
二、重要的宏
//最大内存 ngx_pagesize:一个页面4k(4096)
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
//默认内存池的大小,16k
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
//内存字节对齐的数字
#define NGX_POOL_ALIGNMENT 16
//最小内存池的大小
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
//将内存的开辟调整到临近的a的倍数
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
三、重要类型定义
//nginx内存池的数据头信息
typedef struct {
u_char *last;
u_char *end;
ngx_pool_t *next;
ngx_uint_t failed;
} ngx_pool_data_t;
// nginx内存池的主结构体类型
struct ngx_pool_s {
ngx_pool_data_t d; // 内存池的数据头
size_t max; // 小块内存分配的最大值
ngx_pool_t *current; // 小块内存池入口指针
ngx_chain_t *chain;//链接所有内存池
ngx_pool_large_t *large; // 大块内存分配入口指针
ngx_pool_cleanup_t *cleanup; // 清理函数handler的入口指针
ngx_log_t *log;//日志
};
typedef struct ngx_pool_s ngx_pool_t;
// 小块内存数据头信息
typedef struct {
u_char *last; // 可分配内存开始位置
u_char *end; // 可分配内存末尾位置
ngx_pool_t *next; // 保存下一个内存池的地址
ngx_uint_t failed; // 记录当前内存池分配失败的次数
} ngx_pool_data_t;
typedef struct ngx_pool_large_s ngx_pool_large_t;
// 大块内存类型定义
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存
void *alloc; // 记录分配的大块内存的起始地址
};
typedef void (*ngx_pool_cleanup_pt)(void *data); // 清理回调函数的类型定义
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
// 清理操作的类型定义,包括一个清理回调函数,传给回调函数的数据和下一个清理操作的地址
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 清理回调函数
void *data; // 传递给回调函数的指针
ngx_pool_cleanup_t *next; // 指向下一个清理操作
};
四、函数接口分析
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log); // 创建内存池
void ngx_destroy_pool(ngx_pool_t *pool); // 销毁内存池
void ngx_reset_pool(ngx_pool_t *pool); // 重置内存池
void *ngx_palloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,不支持内存对齐
void *ngx_pcalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存初始化0
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p // 内存释放(大块内存)
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); // 添加清理handler
1、创建内存池
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);//调用malloc函数
if (p == NULL) {
return NULL;
}
p->d.last = (u_char *) p + sizeof(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;//比一个页面小用size,比一个页面大用一个页面
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
总结:新开辟一块内存池,让last指向新申请的可用内存的起始地址,end指向末尾地址,next置为空,failed赋值成0,current指向内存池的起始地址,chain、large和cleanup都置为空。
2、小块内存分配
申请内存的3个主要函数
//考虑内存对齐
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);
}
//开辟成功后,内存全部进行清0操作
void *ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
void *p;
p = ngx_palloc(pool, size);
if (p) {
ngx_memzero(p, size);
}
return p;
}
总结:如果当前内存块够用的话,直接通过last指针偏移size开辟内存;如果把当前所有的小块内存都检测了一遍并且都不够用的话,再开辟同样大小的带ngx_pool_data_t头信息的内存块,再偏移size开辟内存。遍历前面的内存块时,fail记录内存分配失败的次数,如果超过4次,则将当前内存块指向下一个内存块的起始地址,下一次分配从此内存块开始查找是否有合适的内存,再将新生成的内存块接到原来的内存块后面。
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;
if (align) {
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
if ((size_t) (p->d.end - m) >= size) {//内存池的内存空间大于申请的内存空间,可分配
p->d.last = m + size;//偏移size个字节
return m;
}
p = p->d.next;//第一次为空,直接跳出循环,执行ngx_palloc_block
} while (p);
return ngx_palloc_block(pool, size);
}
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);//开辟一块跟pool一样大小的新空间
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);//m指向新空间的起始地址
if (m == NULL) {
return NULL;
}
new = (ngx_pool_t *) m;//new指向新空间的起始地址
new->d.end = m + psize;//指向末尾
new->d.next = NULL;
new->d.failed = 0;
m += sizeof(ngx_pool_data_t);//m指向新开辟的可用内存空间,第二块内存开始,只有ngx_pool_data_t的头信息
m = ngx_align_ptr(m, NGX_ALIGNMENT);//内存对齐
new->d.last = m + size;//指向分配内存的末尾地址,空闲内存的起始地址
//连续进行内存申请,申请内存比较大的时候开辟新的一块内存,每次从第一个内存块查找是否够用,该内存块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;
}
3、大块内存分配
内存头信息
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {//记录大块内存头的信息
ngx_pool_large_t *next;//下一个大块内存的地址
void *alloc;//保存大块内存的内存起始地址
};
总结:首先通过malloc开辟一个大块内存,在原有的小块内存中查找是否有被释放过的大块内存,有则将大块内存的头信息记录在此(即记录可用大块内存的起始地址),查找三次都没有找到的话,直接在小块内存里重新分配大块内存头,接着将alloc指向新开辟的内存空间,最后通过头插法和next指针将所有大块内存连接起来。
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;
for (large = pool->large; large; large = large->next) {//找内存头的空alloc域
if (large->alloc == NULL) {
large->alloc = p;//找到了,直接将它指向刚刚新开辟的内存
return p;
}
if (n++ > 3) {//没有找到
break;
}
}
large = ngx_palloc_small(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;
}
4、重置内存池
小块内存分配没有提供任何的内存释放函数,因为小块内存是通过指针偏移的方式开辟的,所以它也没法进行小块内存的回收,只能重置。因此只试用于短连接的http web服务器(通过重置释放内存),nginx本质是http服务器,它处理完客户端的request请求之后再给客户端发送一个response响应,此后服务器再等待客户端发来请求,如果一定时间内没有收到请求,则主动断开连接,并且调用ngx_reset_pool重置内存池,等待下一个客户端的请求。
总结:遍历大块内存,将alloc下面的大块内存全部释放;遍历小块内存,将小块内存头信息赋值成初始状态,对于小块内存重置,这里有个小错误,因为从第二个内存块开始,小块内存只有数据头信息而没有数据操作头信息,所以重置操作应分成两类,否则会有内存浪费。
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);
}
}
/* 除了第一块内存池,其他内存池只有ngx_pool_data_t的头部信息,此操作浪费空间
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
}
*/
//纠正
//处理第一块内存池
p = pool;
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
//第二块内存池开始循环到最后一块内存池
for (p = p->d.next; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_data_t);
p->d.failed = 0;
}
pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}
5、内存释放(大块内存)
总结:找到大块内存的alloc域,再将其通过free释放并置成空。
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;
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;
}
6、内存池外部资源释放和销毁
问题引入:对于Nginx内存池而言,大块内存的释放是通过调用ngx_reset_pool函数里面的ngx_free,其底层就是调用free,但是这只能做到大块内存的释放,相当于只是把对象本身占用的资源释放掉了,而其中的成员变量占用的外部资源并没有释放,所以会导致内存泄漏。
解决:参考析构函数的思想,在释放这个大块内存之前,对象里面先调用析构函数,把这个外部资源给释放掉就可以使问题得到解决。这个函数需要用户预先将其设置成一个回调函数(ngx_pool_cleanup_add 即通过函数指针实现),先把大块内存中成员变量占用的外部资源释放,再释放这个大块内存本身的内存。
ngx_pool_cleanup_t 类型如下:
typedef void (*ngx_pool_cleanup_pt)(void *data);//回调函数类型
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;//保存回调函数
void *data;//传入回调函数的外部资源的地址
ngx_pool_cleanup_t *next;//链表的下一个域
};
总结:ngx_pool_cleanup_add函数先是创建 ngx_pool_cleanup_t 清理函数的内存头信,即在大块内存释放之前,需要先进行外部资源的清理工作。然后对ngx_pool_cleanup_t 结构体的类型变量做一下初始化操作,最后返回的就是创建的内存头信息的起始地址。
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));//分配,清理外部资源的内存头信息
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;//开辟回掉函数的内存头信息,尚未对Handler进行赋值呢
c->next = p->cleanup;//把这个提前分配的预置回调函数的内存头连接在cleanup链表上
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
问题:若是先清理小块内存,那么ngx_pool_cleanup_t 结构体的类型变量 和大块内存的内存头信息一样也是保存在小块内存块上的内存池里面(通过ngx_palloc小块内存分配函数)就全部失效了。
所以ngx_destroy_pool函数释放顺序:先释放大块内存上成员变量占用的外部资源,即执行用户提供的回调资源清理函数把data传给handler,接下来遍历large指针,释放大块内存,最后遍历pool 把这个小块内存池释放掉。
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);//调用回调函数,并把当前头里面的 data作为参数传入
}
}
#if (NGX_DEBUG)
/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
}
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
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);//释放大块内存
}
}
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);//释放小块内存
if (n == NULL) {
break;
}
}
}
五、后记
通过内存的分配和释放可以看出,nginx将小块内存的申请聚集到了一起,然后再一起释放,这避免了频繁申请小内存,达到降低内存碎片的产生等问题。分析nginx内存池源码可以发现,它处处都在考虑效率的问题,在以后的代码编写过程中,我也应该多多学习这种设计理念,让自己写出的内容更加高效健壮。