这里只介绍nginx_pool主要的大小内存申请、回收及其高效的内存分配机制具体的实现。
1.nginx_create_pool(size_t size, ngx_log_t *log)
这个函数是内存池的创建函数。 第一个参数是内存池的大小(一次最大可申请的小块空间大小),其实实际的小块空间单次最大可申请大小还需要用size减去sizeof(ngx_pool_t)(内存池头部结构体的大小):
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; //析构函数链表
ngx_log_t *log; //日志信息
};
/*内存池数据块信息*/
typedef struct {
u_char *last; //这一块内存块中可以分配出去的内存地址
u_char *end; //指向这一块内存最后
ngx_pool_t *next; //下一个内存块
ngx_uint_t failed; //在这一块内存块上分配失败的次数
} ngx_pool_data_t;
所以size最小应该为sizeof(ngx_pool_t),其最大不能超过NGX_MAX_ALLOC_FROM_POOL:
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
也就是最大不可以超过ngx_pagesize – 1。第二个参数是日志信息参数。
(1)p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
ngx_memalign()是为内存池的创建申请内存的函数,它实际调用的是ngx_alloc()函数:
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
ngx_alloc()函数的实现:
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;
}
与ngx_alloc()函数相似的一个函数ngx_calloc(),比ngx_alloc多了一步初始化的操作,这一点和malloc和calloc比较相似。
(2)内存池信息初始化(这里直接看代码比较容易):
p->d.last = (u_char *) p + sizeof(ngx_pool_t);// 1
p->d.end = (u_char *) p + size;// 2
p->d.next = NULL;// 3
p->d.failed = 0;// 4
size = size - sizeof(ngx_pool_t);// 5
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;// 6
p->current = p;// 7
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
1:数据块last指针指向ngx_pool_s结构体下方,表示可用空间的开头。
2:数据块end指针指向内存块底部,表示可用空间的边界。
3:下一个可用内存块指针置NULL,因为内存池是增长的,刚开始只有一块小内存,内存块链是一个不够再申请一个,这样一个个增加上去的。所以这里就是NULL。
4:failed这一块内存块上分配失败的次数置零,当次数超过一定值时,这一块的内存上的current就会指向一个下一个失败次数低于限制的可用的内存块。下一次直接可以从可用的内存块上分配,减少遍历内存块链的时间,我认为这是ngx_pool在内存分配上采取的一个高效的措施。
5:这个size是表示当前内存块上实际可用的空间大小。减去ngx_pool_s的大小就是剩下的实际可以用来分配的大小。
6:max用来区分申请的大小是小块内存还是大块内存。这里对它的大小做了一个限制,就是最大为ngx_pagesize – 1。
7:current表示下一个可以分配的内存块,初始化为当前的内存块,因为当前内存块是可用的。
nginx_create_pool其实就做了两件事,申请空间和数据初始化,也就是把内存池的头部做好,为以后的操作做铺垫。
2.ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align) 小块内存分配函数
第一个参数是内存池头部结构体指针,第二个是要申请的大小,第三个参数是一个内存对齐的标志,1就是需要按内存对齐申请,0就是不内存对齐。
(1)ngx_align_ptr(p, a)
这是内存对齐函数,实际上是一个宏:
#define ngx_align_ptr(p, a)
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
其中a是sizeof(unsigned long),也就是说32位系统按照四字节对齐,64位系统按照8字节对齐。这个对齐方式是这样的,拿32位系统,按照四字节对齐方式:p + 3(0000 0000 0000 0011),如果p的最低两位不是0(没有对齐),便会向上进位,然后再与上~(0000 0000 0000 0011) = (1111 1111 1111 1100),这样最低位的全为0且会大于原来p,不会越界。如果最低两位是0,那么加上3再与上~(3),就还是原来的值。这样就实现了内存对齐。那么为什么要内存对齐呢?因为内存对齐,可以大大增加cpu读取内存的效率,减少cpu不必要的I/O次数。
(2)再内存块上循环找一个可以分配的内存块,从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); //新申请的内存块大小应该跟原来的一样大小
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 = ngx_align_ptr(m, NGX_ALIGNMENT); //内存对齐
new->d.last = m + size; //分配申请的小内存
/*这里的循环就是把current移到failed次数低于四次的小内存块上*/
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.static void ngx_palloc_large(ngx_pool_t pool, size_t size)
大块内存分配函数。第一个参数是内存池头pool,第二个参数是大小。
(1)第一步申请大块内存,通过ngx_alloc(),其实底层是通过malloc申请的。
p = ngx_alloc(size, pool->log);
(2)第二步是将申请的大块内存地址放到ngx_pool_large_t结构体中。
n = 0;
for (large = pool->large; large; large = large->next) {
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; //当前结构体插入到大块内存链
这一步是在大块内存链上找一个空的大块内存结构体把当前的大块内存地址放到里面进行管理,这里的n,就是又一个高效的地方,为了减少遍历链表的时间,只要遍历超过三次,便不再查找,会在小块内存上申请一个小空间来管理这块大内存,然后把大块内存结构体插入到当前的链表上。
4.ngx_int_t ngx_pfree(ngx_pool_t pool, void p)
大块内存释放函数。第一个参数是内存池pool,第二个是要释放的内存地址。
这里的大块内存是用malloc申请的,所以释放要用free。释放之后,大块内存的结构体不用释放,因为是从小块内存上申请的空间,所以是没办法释放的,这样也有一个好处就是以后的大块内存可以节省申请空间的时间,更加高效。
5.void ngx_destroy_pool(ngx_pool_t *pool)
内存池销毁函数。如果说内存池是一个对象,那么销毁函数就是一个析构函数,把所有资源释放,但是还是需要把小块内存上的所有对象都析构。然后再free大块的内存,最后就是释放所有小块内存。下面是源码:
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);
}
}
/*debugger模式下对内存池的跟踪*/
#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;
}
}
}
6.void ngx_reset_pool(ngx_pool_t *pool)
内存池重置函数。参数只有一个要重置的内存池。
reset和destory的区别在于,reset不用释放所有小块内存,重置之后可以继续使用;destory之后,这个内存池的所有资源都释放,内存池已经不存在了。所以reset需要释放所有大块内存释放,然后把所有的小块重置为可分配状态。
源码:
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);
}
}
/*把所有小块内存*/
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
}
pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}