您的位置:首页 > 运维架构 > Nginx

Nginx源码分析-内存池

2014-05-15 22:37 183 查看
总的来说内存池基本都有一个宗旨:申请大块内存,避免“细水长流”。

Nginx在特定的生命周期统一建立内存池,需要内存时统一在内存池中申请内存,并且在适当的时候释放内存池的内存。

内存池主要由两个结构来维护,一个是头部一个是数据部。此处数据部就是用来分配小块内存的地方。

ngx_pool_data_t用来维护内存池数据块

下面来看一下这个结构体

<span style="font-size:12px;">typedef struct {
    u_char               *last;  //当前内存分配结束位置,即下一段可分配内存的起始位置
    u_char               *end;   //内存池结束位置
    ngx_pool_t           *next;  //链接到下一个内存池
    ngx_uint_t            failed;//统计该内存池不能满足分配请求的次数
} ngx_pool_data_t;</span>
ngx_pool_t维护整个内存池的头部信息,结构体如下

struct 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_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);

下面来分析这个函数

00015: ngx_pool_t *
00016: ngx_create_pool(size_t size, ngx_log_t *log)
00017: {
00018:    ngx_pool_t *p;
00019:
00020:    p = <span style="color:#ff0000;">ngx_memalign(NGX_POOL_ALIGNMENT, size, log)</span>;//此函数下面分析
00021:    if (p == NULL) {
00022:       return NULL;
00023:    }
00024:
00025:    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  //last指向ngx_pool_t结构体之后数据取起始位置
00026:    p->d.end = (u_char *) p + size;  //end指向分配的整个size大小的内存的末尾
00027:    p->d.next = NULL;
00028:    p->d.failed = 0;
00029:
00030:    size = size - sizeof(ngx_pool_t);
00031:    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;  //最大不超过4095B
00032:
00033:    p->current = p;
00034:    p->chain = NULL;
00035:    p->large = NULL;
00036:    p->cleanup = NULL;
00037:    p->log = log;
00038:
00039:    return p;
00040: }
调用这个函数就可以创建一个大小为size的内存池了,内存池的结构图如下



图中红色的字段表示上述的第一个结构,维护数据部分:end表示内存池的结束位置,所有内存分配不可以越过end。蓝色的max字段表示数据段最大大小是多少,如果申请的内存大于max,就认为申请的是大内存,就会在large字段下分配内存,如果小于max字段就在数据段进行内存分配,并且移动last指针

内存池物理结构图如下



紧接着,咱们就来分析下上面代码中所提到的:ngx_memalign()函数

void *

ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)

{

void *p;

int err;



err = posix_memalign(&p, alignment, size);

//该函数分配以alignment为对齐的size字节的内存大小,其中p指向分配的内存块。



if (err) {

ngx_log_error(NGX_LOG_EMERG, log, err,

"posix_memalign(%uz, %uz) failed", alignment, size);

p = NULL;

}



ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,

"posix_memalign: %p:%uz @%uz", p, size, alignment);



return p;

}

//从这个函数的实现体,我们可以看到p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);

//函数分配以NGX_POOL_ALIGNMENT字节对齐的size字节的内存,在src/core/ngx_palloc.h文件中:

view
plain

#define NGX_POOL_ALIGNMENT 16

因此,nginx的内存池分配,是以16字节为边界对齐的。

分配内存调用函数

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);

void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

我们主要来分析一下ngx_palloc函数

<span style="font-size:12px;"><span style="color:#333333;">00115: void *
00116: ngx_palloc(ngx_pool_t *pool, size_t size)
00117: {
00118:    u_char    *m;
00119:    ngx_pool_t *p;
00120:
00121:    if (size <= pool->max) {//判断待分配内存与max值
00122:
00123:       p = pool->current;   //小于max值,则从current节点开始遍历pool链表
00124:
00125:       do {
00126:          m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);//</span><span style="color:#ff0000;">对齐内存指针,加快存取速度</span><span style="color:#333333;">
00127:
00128:          if ((size_t) (p->d.end - m) >= size) {
00129:             p->d.last = m + size;  //在该节点指向的内存块中分配size大小的内存
00130:
00131:             return m;
00132:          }
00133:
00134:          p = p->d.next;
00135:
00136:       } while (p);
00137:
00138:       return </span><span style="color:#ff0000;">ngx_palloc_block(pool, size)</span><span style="color:#333333;">; //链表里没有能分配size大小内存的节点,则生成一个新的节点并在其中分配内存
00139:    }
00140:
00141:    return ngx_palloc_large(pool, size);  //大于max值,则在large链表里分配内存
00142: }</span></span>


上面函数中调用了ngx_palloc_block,下面来分析一下这个函数

<span style="font-size:12px;">00175: static void *
00176: ngx_palloc_block(ngx_pool_t *pool, size_t size)
00177: {
00178:    u_char    *m;
00179:    size_t    psize;
00180:    ngx_pool_t *p, *new, *current;
00181:
00182:    psize = (size_t) (pool->d.end - (u_char *) pool);      //计算pool的大小
00183:
00184:    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);//分配一块与pool大小相同的内存
00185:    if (m == NULL) {
00186:       return NULL;
00187:    }
00188:
00189:    new = (ngx_pool_t *) m;
00190:
00191:    new->d.end = m + psize; //设置end指针
00192:    new->d.next = NULL;
00193:    new->d.failed = 0;
00194:
00195:    m += sizeof(ngx_pool_data_t); //让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置
00196:    m = ngx_align_ptr(m, NGX_ALIGNMENT); //按4字节对齐
00197:    new->d.last = m + size;       //在数据区分配size大小的内存并设置last指针
00198:
00199:    current = pool->current;
00200:
00201:    for (p = current; p->d.next; p = p->d.next) {
00202:       if (p->d.failed++ > 4) {   //failed的值只在此处被修改
00203:          current = p->d.next;    //失败4次以上移动current指针
00204:       }
00205:    }
00206:
00207:    p->d.next = new;  //将这次分配的内存块new加入该内存池
00208:
00209:    pool->current = current ? current : new;
00210:
00211:    return m;
00212: }</span>
注意:该函数分配一块内存后,last指针指向的是ngx_pool_data_t结构体(大小16B)之后数据区的起始位置。而创建内存池时时,last指针指向的是ngx_pool_t结构体(大小40B)之后数据区的起始位置。






内存池的物理结构如下图



内存池第一块内存前40字节为ngx_pool_t结构,后续加入的内存块前16个字节为ngx_pool_data_t结构,这两个结构之后便是真正可以分配内存区域。

大块内存的分配不会在内存池中进行申请内存,而是直接向操作系统申请内存(与直接使用malloc申请一样),然后将申请到的内存挂到内存池头部large字段下。内存池的作用在于解决小块内存池的频繁申请问题的,对于这种大块内存是可以忍受直接申请的。



销毁内存池

接下来,咱们来看内存池的销毁函数,pool指向需要销毁的内存池

view
plain

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);

}

}

//前面讲到,cleanup指向析构函数,用于执行相关的内存池销毁之前的清理工作,如文件的关闭等,

//清理函数是一个handler的函数指针挂载。因此,在这部分,对内存池中的析构函数遍历调用。



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);

}

}

//这一部分用于清理大块内存,ngx_free实际上就是标准的free函数,

//即大内存块就是通过malloc和free操作进行管理的。



#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

*/



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;

}

}

//只有debug模式才会执行这个片段的代码,主要是log记录,用以跟踪函数销毁时日志记录。

#endif



for (p = pool, n = pool->d.next; /** void */; p = n, n = n->d.next) {

ngx_free(p);



if (n == NULL) {

break;

}

}

}

//该片段彻底销毁内存池本身。

该函数将遍历内存池链表,所有释放内存,如果注册了clenup(也是一个链表结构),亦将遍历该cleanup链表结构依次调用clenup的handler清理。同时,还将遍历large链表,释放大块内存。

cleanup资源



可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。

由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。即是说,内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。

比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。

内存的释放

nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。针对这个问题,nginx利用了web server应用的特殊场景来完成;

一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。

也就是说,创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,为此连接创建一个内存池;连接上到来一个request后,又为request创建起一个内存池。

这样,在request被处理完后,就会释放request的整个内存池,但是不释放connection的内存池,因为还可能又其他请求;连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。


小结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: