分类: C/C++
2012-06-13 14:15:13
Nginx的内存模型实现得很精巧,代码也很简洁。总体来说,所有的内存池基本遵守一个宗旨:申请大块内存,避免“细水长流”。主要实现代码在ngx_palloc.h和ngx_palloc.c文件中。使用内存池的好处是:
(1)在大量的小块内存的申请和释放的时候,能更快地进行内存分配;
(2)减少内存碎片,防止内存泄露;
这种处理方式的缺点也是显而易见的,即申请一块大的内存必然会导致内存空间的浪费,但是比起频繁地malloc和free,这样做的代价是非常小的,这是典型的以空间换时间。
2、数据结构Nginx内存池主要有两个结构ngx_pool_data_t和ngx_pool_s来维护,他们分别维护了内存池的头部和数据部分。此处数据部分就是供用户分配小块内存的地方。
ngx_pool_sstruct ngx_pool_s {
ngx_pool_data_t d; //数据块
size_t max; //数据块的大小,即小块内存的最大值
ngx_pool_t *current; //保存当前内存池
ngx_chain_t *chain; //可以挂一个chain结构
ngx_pool_large_t *large; //分配大块内存用,即超过max的内存请求
ngx_pool_cleanup_t *cleanup; //挂载一些内存池释放的时候,同时释放的资源
ngx_log_t *log;
};
备注:该结构维护整个内存池的头部信息。
ngx_pool_data_ttypedef struct {
u_char *last; //当前内存分配结束位置,即下一段可分配内存的起始位置
u_char *end; //内存池结束位置
ngx_pool_t *next; //链接到下一个内存池
ngx_uint_t failed; //统计该内存池不能满足分配请求的次数
} ngx_pool_data_t;
备注:该结构用来维护内存池的数据块,供用户分配之用。
ngx_pool_large_s
struct ngx_pool_large_s {
ngx_pool_large_t *next; //用来链接大块内存
void *alloc; //malloc分配的内存空间
};
ngx_pool_cleanup_sstruct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; //操作data的方法
void *data; //内存区域
ngx_pool_cleanup_t *next; //链接到下个cleanup
};
3、具体实现 创建内存池 ngx_create_pool()Nginx用来创建一个内存池的接口是:ngx_create_pool(size_t size, ngx_log_t *log),调用这个函数就可以创建一个大小为size的内存池了。
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
//分配大小为size的空间,并且对齐
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
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;
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
ngx_create_pool函数就是分配如上图所示的一大块内存,然后初始化好各个头部字段,last是用户从内存池分配新内存的开始位置,end是这块内存池的结束位置,max是整个数据部分的长度,用户请求的内存大于max时,就认为用户请求的是一个大内存,此时需要large字段下面单独分配,用户请求的内存不大于max时,就是小内存申请,直接在数据部分分配,此时将会移动last指针。
分配内存 ngx_palloc()void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) { //分配小内存
p = pool->current;
do {
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
//ngx_pnalloc中没有此句,内存对齐
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); //free内存不够,分配next内存
}
return ngx_palloc_large(pool, size); //分配大内存
}
ngx_palloc和ngx_pnalloc都是从内存池里分配一个大小为size的内存,至于分得的是小块内存还是大块内存,将取决于size的大小,他们的不同之处在于,palloc取得的内存是对齐的,pnalloc则否。下面是分配小块内存的模型图:
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;
}
ngx_pcalloc是直接调用palloc分配好内存,然后进行一次零初始化操作。
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, *current;
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 = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
}
上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。
由图可见:所有的小内存池是由一个单向链表维护在一起的。这里还有两个字段需要关注,failed和current字段。
(1)failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,即是说:一个分配请求到来后,在这个内存池上分配不到想要的内存,那么就failed就会增加1;这个分配请求将会递交给下一个内存池去处理,如果下一个内存池也不能满足,那么它的failed也会加1,然后将请求继续往下传递,直到满足请求为止(如果没有现成的内存池来满足,会再创建一个新的内存池)。
(2)current字段会随着failed的增加而发生改变,如果current指向的内存池的failed达到了4的话,current就指向下一个内存池了。
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); //申请内存
if (p == NULL) {
return NULL;
}
n = 0;
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p; //挂在large下面
return p;
}
if (n++ > 3) {
break;
}
}
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
large->alloc = p; //采用的是先进后出的方式
large->next = pool->large;
pool->large = large;
return p;
}
大块内存的分配请求不会直接从内存池上分配来满足,而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题,对于这种大块内存,是可以忍受直接申请的,用图展示大块内存申请模型:
备注:每块大内存都对应有一个头部结构(next&alloc),这个头部结构是用来将所有大内存串成一个链表用的。这个头部结构不是直接向操作系统申请的,而是当做小块内存(头部结构没几个字节)直接在内存池里申请的。这样的大块内存在使用完后,可能需要第一时间释放,节省内存空间。
因此nginx提供了接口函数: ngx_pfree(ngx_pool_t *pool, void *p),此函数专门用来释放某个内存池上的某个大块内存,p就是大内存的地址。ngx_pfree只会释放大内存,不会释放其对应的头部结构,毕竟头部结构是当做小内存在内存池里申请的;遗留下来的头部结构会作下一次申请大内存之用。
ngx_pmemalign()void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
void *p;
ngx_pool_large_t *large;
p = ngx_memalign(alignment, size, pool->log); //分配空间
if (p == NULL) {
return NULL;
}
large = ngx_palloc(pool, sizeof(ngx_pool_large_t)); //被当做大块内存处理
if (large == NULL) {
ngx_free(p);
return NULL;
}
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
ngx_pmemalign()将在分配size大小的内存并按alignment对齐,然后挂到large字段下,当做大块内存处理。
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;
}
}
return NGX_DECLINED;
}
4.4 cleanup资源 ngx_pool_cleanup_add()ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
//cleanup的header
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size); //为data申请空间
if (c->data == NULL) {
return NULL;
}
} else {
c->data = 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;
}
可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。
即内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。
4.5 内存的释放nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。
针对这个问题,nginx利用了web server应用的特殊场景来完成,一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。
(1)创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;
(2)连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。这样,在request被处理完后,就会释放request的整个内存池,连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。
总结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题
4、接口函数 ngx_create_pool(size_t size, ngx_log_t *log);函数功能:创建一个大小为size的内存池。
ngx_palloc(ngx_pool_t *pool, size_t size);函数功能:分配小内存,并将内存对齐。
ngx_pnalloc(ngx_pool_t *pool, size_t size);函数功能:分配小内存,不需要内存对齐,这是与ngx_palloc()函数的主要区别。
ngx_pcalloc(ngx_pool_t *pool, size_t size);函数功能:用ngx_palloc()分配空间,并将里面的字节都初始化为零。
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);函数功能:申请内存空间并对齐,把该空间挂到pool->large指针下。
ngx_alloc(size_t size, ngx_log_t *log);函数功能:对malloc函数进行包装。
ngx_calloc(size_t size, ngx_log_t *log);函数功能:对malloc函数进行了包装,并将内存空间以零初始化。
ngx_palloc_large(ngx_pool_t *pool, size_t size);函数功能:申请大块内存,将它挂在pool->large指针下。
ngx_pfree(ngx_pool_t *pool, void *p);函数功能:释放large内存p。
ngx_reset_pool(ngx_pool_t *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);
}
}
pool->large = NULL;
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
}
函数功能:free large内存区域。
ngx_destroy_pool(ngx_pool_t *pool);void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
//对cleanup的data进行handler
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);
}
}
//对large区的内存进行free
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)
/*
* we could allocate the pool->log from this pool
* so we can not use this log while the free()ing the pool
*/
//打印log
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
//free next内存空间
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
函数功能:销毁内存池。
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); 函数功能:添加cleanup链表节点。