Nginx源码分析-内存池

来源:互联网 发布:淘宝网广场舞裙子套装 编辑:程序博客网 时间:2024/06/06 14:13

总的来说内存池基本都有一个宗旨:申请大块内存,避免“细水长流”。

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;  //最大不超过4095B00032: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()函数

  1. void *  
  2. ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)  
  3. {  
  4.     void  *p;  
  5.     int    err;  
  6.       
  7.     err = posix_memalign(&p, alignment, size);  
  8.     //该函数分配以alignment为对齐的size字节的内存大小,其中p指向分配的内存块。  
  9.       
  10.     if (err) {  
  11.         ngx_log_error(NGX_LOG_EMERG, log, err,  
  12.             "posix_memalign(%uz, %uz) failed", alignment, size);  
  13.         p = NULL;  
  14.     }  
  15.       
  16.     ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,  
  17.         "posix_memalign: %p:%uz @%uz", p, size, alignment);  
  18.       
  19.     return p;  
  20. }  
  21. //从这个函数的实现体,我们可以看到p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);  
  22. //函数分配以NGX_POOL_ALIGNMENT字节对齐的size字节的内存,在src/core/ngx_palloc.h文件中:  
view plain
  1. #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
  1. void  
  2. ngx_destroy_pool(ngx_pool_t *pool)  
  3. {  
  4.     ngx_pool_t          *p, *n;  
  5.     ngx_pool_large_t    *l;  
  6.     ngx_pool_cleanup_t  *c;  
  7.       
  8.     for (c = pool->cleanup; c; c = c->next) {  
  9.         if (c->handler) {  
  10.             ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,  
  11.                 "run cleanup: %p", c);  
  12.             c->handler(c->data);  
  13.         }  
  14.     }  
  15.     //前面讲到,cleanup指向析构函数,用于执行相关的内存池销毁之前的清理工作,如文件的关闭等,  
  16.     //清理函数是一个handler的函数指针挂载。因此,在这部分,对内存池中的析构函数遍历调用。  
  17.       
  18.     for (l = pool->large; l; l = l->next) {  
  19.         ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);  
  20.           
  21.         if (l->alloc) {  
  22.             ngx_free(l->alloc);  
  23.         }  
  24.     }  
  25.     //这一部分用于清理大块内存,ngx_free实际上就是标准的free函数,  
  26.     //即大内存块就是通过malloc和free操作进行管理的。  
  27.       
  28. #if (NGX_DEBUG)  
  29.       
  30.     /** 
  31.     * we could allocate the pool->log from this pool 
  32.     * so we can not use this log while the free()ing the pool 
  33.     */  
  34.       
  35.     for (p = pool, n = pool->d.next; /** void */; p = n, n = n->d.next) {  
  36.         ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,  
  37.             "free: %p, unused: %uz", p, p->d.end - p->d.last);  
  38.           
  39.         if (n == NULL) {  
  40.             break;  
  41.         }  
  42.     }  
  43.     //只有debug模式才会执行这个片段的代码,主要是log记录,用以跟踪函数销毁时日志记录。  
  44. #endif  
  45.       
  46.     for (p = pool, n = pool->d.next; /** void */; p = n, n = n->d.next) {  
  47.         ngx_free(p);  
  48.           
  49.         if (n == NULL) {  
  50.             break;  
  51.         }  
  52.     }  
  53. }  
  54. //该片段彻底销毁内存池本身。  
   该函数将遍历内存池链表,所有释放内存,如果注册了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只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题。


0 0
原创粉丝点击