Nginx是轻量级web服务器/反向代理服务器及电子邮件代理服务器。特点是占有内存少,并发能力强。现在主要分析其内存池的实现的原理:
Nginx内存池
内存池结构有一个的头部,其中包含一个数据部,头部(除数据部)主要是用来为用户分配大块内存(通过链表来实现)、管理外部资源、日志信息等。而数据部而用于分配小块内存,和指向下一个内存池。
头部信息结构为:
struct ngx_pool_s {
ngx_pool_data_t d;//指向数据部
size_t max;//内存池小块的内存最大值
ngx_pool_t *current;//指向当前内存池
ngx_chain_t *chain;//挂接一个ngx_chain_t结构体
ngx_pool_large_t *large;//指向大块内存链表,当需要分配的内存大于max是就需要使用大块内存分配
ngx_pool_cleanup_t *cleanup;//指向一个回调函数结构体,以链表的形式存储,当销毁内存池时,就需要使用回调函数的链表来进行数据的清理
ngx_log_t *log;//日志信息
};
数据部信息结构为:
typedef struct {
u_char *last;//指向内存池分配内存的尾部
u_char *end;//指向小块内存的尾部
ngx_pool_t *next;//指向下一小块内存
ngx_uint_t failed;//分配小块内存失败的次数
} ngx_pool_data_t;
两个结构构成的结构的模型图:
内存池的创建 :ngx_create_pool
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);
//底层封装 ngx_alloc(size, log);实际上也是封装了malloc为内存池开辟size大小空间
/*
void *
ngx_alloc(size_t size, ngx_log_t *log)
{
void *p;
p = malloc(size);//实际上也是使用malloc开辟内存
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;
}
*/
if (p == NULL) {
return NULL;
}
//将新建的指向内存池的数据部进行初始化
p->d.last = (u_char *) p + sizeof(ngx_pool_t);//last指向内存池分配内存的尾部
p->d.end = (u_char *) p + size;//指向小块内存块的尾部
p->d.next = NULL;//指向下一个小块
p->d.failed = 0;//小块内存分配失败的次数
size = size - sizeof(ngx_pool_t);//减去头部为小块内存的大小 ngx-pool_t 是ngx_pool_s别名
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
//#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) 这是一个宏定义,ngx_pagesize 是4k大小,一个页大小,给小块内存最大只能是一块页大小的内存
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
//初始化其它成员
return p;
}
由此可以看出,内存池的创建实际上就是,开辟内存池的内存和内存池头部数据的初始化,且,一旦内存池创建完成了,就不能修改内存池的大小,需要拓展的话,需要在创建一个新的内存池。且内存池的实现离不开Glibc的malloc函数,就离不开ptmalloc的内存管理机制。
内存申请:ngx_palloc
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);//小块内存分配,传1表示需要内存对齐
}
#endif
return ngx_palloc_large(pool, size);//大块内存分配
}
//可以看出来,内存池的分配有两种,当所需要的内存大小大于max的时候,就就进行大块内存的分配,如果小于等于的话,就进行小块内存的分配
//ngx_pnalloc函数不需要内存对齐
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);//传0表示需要内存对齐
}
#endif
return ngx_palloc_large(pool, size);
}
内存分配的方式有两种,分别是大块内存和小块内存,分配的方式的采用是通过所需分配的内存大小来决定,且还为是否需要内存对齐,给出了两个函数。
ngx_pcalloc函数是将开辟的内存并初始化
void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
void *p;
p = ngx_palloc(pool, size);
if (p) {
ngx_memzero(p, size);//对内存初始化为0
//这是一个宏函数 #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
}
return p;
}
小块内存的分配: ngx_palloc_small
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);//宏函数,进行内存对齐
}
/*
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。
*/
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m;
}//如果剩余的内存足够的话,就直接将last指针往下移动size大小。
p = p->d.next;//否则就前往下一块内存
} while (p);//当p不为空的时候进行
return ngx_palloc_block(pool, size);//没有找到有剩余空间的块,就在开辟一个新块,
}
有此可知,小块内存的分配是在内存池结构的里面进行的,通过移动last指针来分配空间,如果没有空间或者是空间不足的话,就在开辟新块。
分配新块:ngx_palloc_block
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);//获取当前内存池大小
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);//开辟一个和当前内存池大小相同的内存池
if (m == NULL) {
return NULL;
}
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
//为新开辟的内存池进行成员初始化
m += sizeof(ngx_pool_data_t);//m指向数据部的尾部
m = ngx_align_ptr(m, NGX_ALIGNMENT);//进行内存对齐
new->d.last = m + size;//last指向当前内存的尾部
//每一个p的failed都要+1,因为这些块的内存都不够分配,都失败了一次
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;
//当分配内存失败超过5次后,改变current的指向,就不要每次都从第一个块开始遍历
}
}
p->d.next = new;
return m;
}
分配的内存池块是和第一块一样的大的,然后放在内存池链表的后面。
分配大块内存 :ngx_palloc_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);//分配size大小的内存
if (p == NULL) {
return NULL;
}
n = 0;
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}//找到空闲的large块,将新开辟的内存交给它
if (n++ > 3) {
break;
}//为提高效率,三次没找到就创建一个新的
}
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
//新创建的large块使用的是小块分配机制,创建一个大块内存大小的空间
if (large == NULL) {
ngx_free(p);//创建失败就释放掉刚开辟的内存
return NULL;
}
large->alloc = p;//内存域指向p
large->next = pool->large;
pool->large = large;
//采用头插法
return p;
}
大块内存的分配是需要开辟内存的通过malloc在堆上开辟,先是寻找空闲的块,然后将新开辟的内存给它,如果三次没有找到,就使用小块内存分配的方式,来开辟内存。然后使用头插法插入到大块链表中。
大致的内存分配图:
内存重置:ngx_reset_pool
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_free 是一个宏 实质就是free,释放掉大块内存
}
}
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);//重置小块内存就是重置last指针和failed
p->d.failed = 0;
}
pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}
内存清理:ngx_pfree
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;
}//寻找地址为P的大块内存将其释放
}
return NGX_DECLINED;
}
重置和清理都是对大块内存进行清除,因为我们大块内存的分配过程就是检查3次有没有空闲的内存块, 没有就直接调用malloc去堆上直接申请。所以系统资源使用要使用后尽快返回,小块内存就不需要释放,保留等下次调用
回调函数结构体:
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;//回调函数
void *data;//数据域
ngx_pool_cleanup_t *next;//指向链表的下一结点
};
nginx内存池还通过回调函数对外部资源的清理。ngx_pool_cleanup_t是外部资源管理的结构体节点,内存池中有一个链表用来链接这些节点。我们可以通过ngx_pool_cleanup_add获取一个新的节点的指针,然后通过该指针来设置要管理的外部资源地址以及清理这些资源要用到的方法(回调函数)
ngx_pool_cleanup_add函数:
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));//开辟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;//这里暂时设为NULL,返回ngx_pool_cleanup_t后在外部设置回调函数
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
大致模型图:
内存池的销毁:ngx_destroy_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);//循环调用handler函数对外部资源进行清理
}
}
#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;
}
}//对小块内存进行释放内存
}
先调用回调函数,对外部资源进行释放,然后先后对大块内存和小块内存进行释放。