Nginx源碼剖析之內存池,與內存管理

fmms 12年前發布 | 39K 次閱讀 Nginx Web服務器

引言    

    Nginx(發音同 engine x)是一款輕量級的Web 服務器/反向代理服務器及電子郵件(IMAP/POP3)代理服務器,并在一個BSD-like 協議下發行。由俄羅斯的程序設計師Igor Sysoev所開發,最初供俄國大型的入口網站及搜尋引擎Rambler(俄文:Рамблер)使用。  

    其特點是占有內存少,并發能力強,事實上nginx的并發能力確實在同類型的網頁服務器中表現較好,目前中國大陸使用nginx網站用戶有:新浪、網易、 騰訊,另外知名的微網志Plurk也使用nginx,以及諸多暫不曾得知的玩意兒。
 

    讀者可以到此處下載Nginx最新版本的源碼:http://nginx.org/en/download.html。同時,本文本不想給源碼太多注釋,因為這不像講解算法,算法講解的越通俗易懂越好,而源碼剖析則不同,緣由在于不同的讀者對同一份源碼有著不同的理解,或深或淺,所以,更多的是靠讀者自己去思考與領悟。

    ok,本文之中有任何疏漏或不正之處,懇請批評指正。謝謝。

Nginx源碼剖析之內存池
 

1、內存池結構

    內存相關的操作主要在文件 os/unix/ngx_alloc.{h,c} 和 core/ngx_palloc.{h,c} 中實現,ok,咱們先來看內存管理中幾個主要的數據結構:
 

 
</div>
  1. typedef struct {    //內存池的數據結構模塊   
  2.     u_char               *last;    //當前內存分配結束位置,即下一段可分配內存的起始位置   
  3.     u_char               *end;     //內存池的結束位置   
  4.     ngx_pool_t           *next;    //鏈接到下一個內存池,內存池的很多塊內存就是通過該指針連成鏈表的   
  5.     ngx_uint_t            failed;  //記錄內存分配不能滿足需求的失敗次數   
  6. } ngx_pool_data_t;   //結構用來維護內存池的數據塊,供用戶分配之用。  
</div>

 
</div>
  1. struct ngx_pool_s {  //內存池的管理分配模塊   
  2.     ngx_pool_data_t       d;         //內存池的數據塊(上面已有描述),設為d   
  3.     size_t                max;       //數據塊大小,小塊內存的最大值   
  4.     ngx_pool_t           *current;   //指向當前或本內存池   
  5.     ngx_chain_t          *chain;     //該指針掛接一個ngx_chain_t結構   
  6.     ngx_pool_large_t     *large;     //指向大塊內存分配,nginx中,大塊內存分配直接采用標準系統接口malloc   
  7.     ngx_pool_cleanup_t   *cleanup;   //析構函數,掛載內存釋放時需要清理資源的一些必要操作  
  8.     ngx_log_t            *log;       //內存分配相關的日志記錄   
  9. };  
</div>

再來看看大塊數據分配的結構體:
 

 
</div>
  1. struct ngx_pool_large_s {  
  2.     ngx_pool_large_t     *next;  
  3.     void                 *alloc;  
  4. };  
</div>

其它的數據結構與相關定義:
 

 
</div>
  1. typedef struct {  
  2.     ngx_fd_t              fd;  
  3.     u_char               *name;  
  4.     ngx_log_t            *log;  
  5. } ngx_pool_cleanup_file_t;  
</div>

 
</div>
  1. #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)  //在x86體系結構下,該值一般為4096B,即4K  
</div>

上述這些數據結構的邏輯結構圖如下:

Nginx源碼剖析之內存池,與內存管理
 

1.1、ngx_pool_t的邏輯結構

    再看一下用UML繪制的ngx_pool_t的邏輯結構圖:

Nginx源碼剖析之內存池,與內存管理

    在下一節,我們將會深入分析內存管理的主要函數。
 

Nginx源碼剖析之內存管理

2、內存池操作

2.1、創建內存池

 
</div>
  1. ngx_pool_t *  
  2. ngx_create_pool(size_t size, ngx_log_t *log)  
  3. {  
  4.     ngx_pool_t  *p;  
  5.       
  6.     p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);  
  7.     //ngx_memalign()函數執行內存分配,該函數的實現在src/os/unix/ngx_alloc.c文件中(假定NGX_HAVE_POSIX_MEMALIGN被定義):   
  8.       
  9.     if (p == NULL) {  
  10.         return NULL;  
  11.     }  
  12.       
  13.     p->d.last = (u_char *) p + sizeof(ngx_pool_t);  
  14.     p->d.end = (u_char *) p + size;  
  15.     p->d.next = NULL;  
  16.     p->d.failed = 0;  
  17.       
  18.     size = size - sizeof(ngx_pool_t);  
  19.     p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;   
  20.     //最大不超過4095B,別忘了上面NGX_MAX_ALLOC_FROM_POOL的定義   
  21.       
  22.     p->current = p;  
  23.     p->chain = NULL;  
  24.     p->large = NULL;  
  25.     p->cleanup = NULL;  
  26.     p->log = log;  
  27.       
  28.     return p;  
  29. }  
</div>

    例如,調用ngx_create_pool(1024, 0x80d1c4c)后,創建的內存池物理結構如下圖:

Nginx源碼剖析之內存池,與內存管理

</blockquote>

    緊接著,咱們就來分析下上面代碼中所提到的:ngx_memalign()函數。
 

 
</div>
  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文件中:  
</div>

 
</div>
  1. #define NGX_POOL_ALIGNMENT       16  
</div>

    因此,nginx的內存池分配,是以16字節為邊界對齊的。

2.1、銷毀內存池
    接下來,咱們來看內存池的銷毀函數,pool指向需要銷毀的內存池
 

 
</div>
  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. //該片段徹底銷毀內存池本身。  
</div>

    該函數將遍歷內存池鏈表,所有釋放內存,如果注冊了clenup(也是一個鏈表結構),亦將遍歷該cleanup鏈表結構依次調用clenup的handler清理。同時,還將遍歷large鏈表,釋放大塊內存。 

2.3、重置內存池

void ngx_reset_pool(ngx_pool_t *pool)
    重置內存池,將內存池恢復到剛分配時的初始化狀態,注意內存池分配的初始狀態時,是不包含大塊內存的,因此初始狀態需要將使用的大塊內存釋放掉,并把內存池數據結構的各項指針恢復到初始狀態值。代碼片段如下:
 

 
</div>
  1. void  
  2. ngx_reset_pool(ngx_pool_t *pool)  
  3. {  
  4.     ngx_pool_t        *p;  
  5.     ngx_pool_large_t  *l;  
  6.       
  7.     for (l = pool->large; l; l = l->next) {  
  8.         if (l->alloc) {  
  9.             ngx_free(l->alloc);  
  10.         }  
  11.     }  
  12.     //上述片段主要用于清理使用到的大塊內存。   
  13.       
  14.     pool->large = NULL;  
  15.       
  16.     for (p = pool; p; p = p->d.next) {  
  17.         p->d.last = (u_char *) p + sizeof(ngx_pool_t);  
  18.     }  
  19. }  
</div>

    這里雖然重置了內存池,但可以看到并沒有釋放內存池中被使用的小塊內存,而只是將其last指針指向可共分配的內存的初始位置。這樣,就省去了內存池的釋放和重新分配操作,而達到重置內存池的目的。
    上面我們主要闡述了內存池管理的幾個函數,接下來我們深入到如何從內存池中去申請使用內存。

 

2.4、分配內存(重點)

2.4.1、ngx_palloc 與ngx_pnalloc函數
    這兩個函數的參數都為(ngx_pool_t *pool, size_t size),且返回類型為void*,唯一的區別是ngx_palloc從pool內存池分配以NGX_ALIGNMENT對齊的內存,而ngx_pnalloc分配適合size大小的內存,不考慮內存對齊。
    我們在這里只分析ngx_palloc,對于ngx_pnalloc其實現方式基本類似,便不再贅述。
文件:src/core/ngx_palloc.c
 

 
</div>
  1. void *  
  2. ngx_palloc(ngx_pool_t *pool, size_t size)  
  3. {  
  4.     u_char      *m;  
  5.     ngx_pool_t  *p;  
  6.       
  7.     //判斷待分配內存與max值   
  8.     //1、小于max值,則從current結點開始遍歷pool鏈表   
  9.     if (size <= pool->max) {  
  10.           
  11.         p = pool->current;   
  12.           
  13.         do {  
  14.             //執行對齊操作,   
  15.             //即以last開始,計算以NGX_ALIGNMENT對齊的偏移位置指針,   
  16.             m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);  
  17.               
  18.             //然后計算end值減去這個偏移指針位置的大小是否滿足索要分配的size大小,   
  19.             //如果滿足,則移動last指針位置,并返回所分配到的內存地址的起始地址;   
  20.             if ((size_t) (p->d.end - m) >= size) {  
  21.                 p->d.last = m + size;    
  22.                 //在該結點指向的內存塊中分配size大小的內存   
  23.                   
  24.                 return m;  
  25.             }  
  26.               
  27.             //如果不滿足,則查找下一個鏈。   
  28.             p = p->d.next;  
  29.               
  30.         } while (p);  
  31.           
  32.         //如果遍歷完整個內存池鏈表均未找到合適大小的內存塊供分配,則執行ngx_palloc_block()來分配。   
  33.           
  34.         //ngx_palloc_block()函數為該內存池再分配一個block,該block的大小為鏈表中前面每一個block大小的值。   
  35.         //一個內存池是由多個block鏈接起來的。分配成功后,將該block鏈入該poll鏈的最后,   
  36.         //同時,為所要分配的size大小的內存進行分配,并返回分配內存的起始地址。   
  37.         return ngx_palloc_block(pool, size);    //2.4.1節分析   
  38.           
  39.     }  
  40.     //2、如果大于max值,則執行大塊內存分配的函數ngx_palloc_large,在large鏈表里分配內存  
  41.     return ngx_palloc_large(pool, size);        //2.4.2節分析   
  42. }  
</div>

    例如,在2.1節中創建的內存池中分配200B的內存,調用ngx_palloc(pool, 200)后,該內存池物理結構如下圖:

Nginx源碼剖析之內存池,與內存管理

</blockquote>

a、待分配內存小于max值的情況
    同樣,緊接著,咱們就來分析上述代碼中的ngx_palloc_block()函數:
 

 
</div>
  1. static void *  
  2. ngx_palloc_block(ngx_pool_t *pool, size_t size)  
  3. {  
  4.     u_char      *m;  
  5.     size_t       psize;  
  6.     ngx_pool_t  *p, *new, *current;  
  7.       
  8.     psize = (size_t) (pool->d.end - (u_char *) pool);  
  9.     //計算pool的大小,即需要分配的block的大小   
  10.       
  11.     m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);  
  12.     if (m == NULL) {  
  13.         return NULL;  
  14.     }  
  15.     //執行按NGX_POOL_ALIGNMENT對齊方式的內存分配,假設能夠分配成功,則繼續執行后續代碼片段。   
  16.       
  17.     //這里計算需要分配的block的大小   
  18.     new = (ngx_pool_t *) m;  
  19.       
  20.     new->d.end = m + psize;  
  21.     new->d.next = NULL;  
  22.     new->d.failed = 0;  
  23.     //執行該block相關的初始化。   
  24.       
  25.     m += sizeof(ngx_pool_data_t);  
  26.     //讓m指向該塊內存ngx_pool_data_t結構體之后數據區起始位置   
  27.     m = ngx_align_ptr(m, NGX_ALIGNMENT);  
  28.     new->d.last = m + size;  
  29.     //在數據區分配size大小的內存并設置last指針   
  30.       
  31.     current = pool->current;  
  32.       
  33.     for (p = current; p->d.next; p = p->d.next) {  
  34.         if (p->d.failed++ > 4) {  
  35.             current = p->d.next;  
  36.             //失敗4次以上移動current指針   
  37.         }  
  38.     }  
  39.       
  40.     p->d.next = new;  
  41.     //將分配的block鏈入內存池   
  42.       
  43.     pool->current = current ? current : new;  
  44.     //如果是第一次為內存池分配block,這current將指向新分配的block。   
  45.       
  46.     return m;  
  47. }  
</div>

    注意:該函數分配一塊內存后,last指針指向的是ngx_pool_data_t結構體(大小16B)之后數據區的起始位置,而創建內存池時時,last指針指向的是ngx_pool_t結構體(大小40B)之后數據區的起始位置。 結合2.8節的內存池的物理結構,更容易理解。
b、待分配內存大于max值的情況
    如2.4.1節所述,如果分配的內存大小大于max值,代碼將跳到ngx_palloc_large(pool, size)位置,
ok,下面進入ngx_palloc_large(pool, size)函數的分析:
 

 
</div>
  1. //這是一個static的函數,說明外部函數不會隨便調用,而是提供給內部分配調用的,   
  2. //即nginx在進行內存分配需求時,不會自行去判斷是否是大塊內存還是小塊內存,   
  3. //而是交由內存分配函數去判斷,對于用戶需求來說是完全透明的。   
  4. static void *  
  5. ngx_palloc_large(ngx_pool_t *pool, size_t size)  
  6. {  
  7.     void              *p;  
  8.     ngx_uint_t         n;  
  9.     ngx_pool_large_t  *large;  
  10.       
  11.     p = ngx_alloc(size, pool->log);  //下文緊接著將分析此ngx_alloc函數   
  12.     if (p == NULL) {  
  13.         return NULL;  
  14.     }  
  15.       
  16.     n = 0;  
  17.       
  18.     //以下幾行,將分配的內存鏈入pool的large鏈中,   
  19.     //這里指原始pool在之前已經分配過large內存的情況。   
  20.     for (large = pool->large; large; large = large->next) {  
  21.         if (large->alloc == NULL) {  
  22.             large->alloc = p;  
  23.             return p;  
  24.         }  
  25.           
  26.         if (n++ > 3) {  
  27.             break;  
  28.         }  
  29.     }  
  30.       
  31.     //如果該pool之前并未分配large內存,則就沒有ngx_pool_large_t來管理大塊內存   
  32.     //執行ngx_pool_large_t結構體的分配,用于來管理large內存塊。   
  33.     large = ngx_palloc(pool, sizeof(ngx_pool_large_t));  
  34.     if (large == NULL) {  
  35.         ngx_free(p);  
  36.         return NULL;  
  37.     }  
  38.       
  39.     large->alloc = p;  
  40.     large->next = pool->large;  
  41.     pool->large = large;  
  42.       
  43.     return p;  
  44. }  
</div>

    上述代碼中,調用ngx_alloc執行內存分配:
 

 
</div>
  1. void *  
  2. ngx_alloc(size_t size, ngx_log_t *log)  
  3. {  
  4.     void  *p;  
  5.       
  6.     p = malloc(size);    
  7.     //從這里可以看到,ngx_alloc實際上就是調用malloc函數分配內存的。   
  8.       
  9.     if (p == NULL) {  
  10.         ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,  
  11.             "malloc() %uz bytes failed", size);  
  12.     }  
  13.       
  14.     ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);  
  15.       
  16.     return p;  
  17. }  
</div>

2.4.2、ngx_pcalloc與ngx_pmemalign函數

    ngx_pcalloc是直接調用palloc分配好內存,然后進行一次0初始化操作。ngx_pcalloc的源碼如下:

 
</div>
  1. void *  
  2. ngx_pcalloc(ngx_pool_t *pool, size_t size)  
  3. {  
  4.     void *p;  
  5.   
  6.     p = ngx_palloc(pool, size);  
  7.     if (p) {  
  8.         ngx_memzero(p, size);  
  9.     }  
  10.   
  11.     return p;  
  12. }  
</div>

    ngx_pmemalign將在分配size大小的內存并按alignment對齊,然后掛到large字段下,當做大塊內存處理。ngx_pmemalign的源碼如下:
 

 
</div>
  1. void *  
  2. ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)  
  3. {  
  4.     void              *p;  
  5.     ngx_pool_large_t  *large;  
  6.   
  7.     p = ngx_memalign(alignment, size, pool->log);  
  8.     if (p == NULL) {  
  9.         return NULL;  
  10.     }  
  11.   
  12.     large = ngx_palloc(pool, sizeof(ngx_pool_large_t));  
  13.     if (large == NULL) {  
  14.         ngx_free(p);  
  15.         return NULL;  
  16.     }  
  17.   
  18.     large->alloc = p;  
  19.     large->next = pool->large;  
  20.     pool->large = large;  
  21.   
  22.     return p;  
  23. }  
</div>

    其余的不再詳述。nginx提供給我們使用的內存分配接口,即上述本2.4節中這4種函數,至此,都已分析完畢。

2.5、釋放內存

 
</div>
  1.     if (p == l->alloc) {  
  2.         ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,  
  3. "free: %p", l->alloc);  
  4.         ngx_free(l->alloc);  
  5.         l->alloc = NULL;  
  6.   
  7.         return NGX_OK;  
  8.     }  
  9. }  
  10.   
  11. return NGX_DECLINED;  
</div>

    需要注意的是該函數只釋放large鏈表中注冊的內存,普通內存在ngx_destroy_pool中統一釋放。
2.6、注冊cleanup
 

 
</div>
  1. ngx_pool_cleanup_t *  
  2. ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)  
  3. {  
  4.     ngx_pool_cleanup_t  *c;  
  5.       
  6.     c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));  
  7.     if (c == NULL) {  
  8.         return NULL;  
  9.     }  
  10.       
  11.     if (size) {  
  12.         c->data = ngx_palloc(p, size);  
  13.         if (c->data == NULL) {  
  14.             return NULL;  
  15.         }  
  16.           
  17.     } else {  
  18.         c->data = NULL;  
  19.     }  
  20.       
  21.     c->handler = NULL;  
  22.     c->next = p->cleanup;  
  23.       
  24.     p->cleanup = c;  
  25.       
  26.     ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);  
  27.       
  28.     return c;  
  29. }  
</div>

2.7、文件相關
    一些文件相關的操作函數如下,此處就不在詳述了。
 

 
</div>
  1. void  
  2. ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)  
  3. {  
  4.     //....   
  5. }  
  6.   
  7. void  
  8. ngx_pool_cleanup_file(void *data)  
  9. {  
  10.     //....   
  11. }  
  12.   
  13. void  
  14. ngx_pool_delete_file(void *data)  
  15. {  
  16.     //...   
  17. }  
</div>


2.8、內存池的物理結構
    針對本文前幾節的例子,畫出的內存池的物理結構如下圖。
 

Nginx源碼剖析之內存池,與內存管理

    從該圖也能看出2.4節的結論,即內存池第一塊內存前40字節為ngx_pool_t結構,后續加入的內存塊前16個字節為ngx_pool_data_t結構,這兩個結構之后便是真正可以分配內存區域。 

全文總結

    來自淘寶數據共享平臺blog內的一篇文章對上述Nginx源碼剖析之內存池,與內存管理總結得很好,特此引用之,作為對上文全文的一個總結:

    Nginx的內存池實現得很精巧,代碼也很簡潔。總的來說,所有的內存池基本都一個宗旨:申請大塊內存,避免“細水長流”。
3.1、創建一個內存池
    nginx內存池主要有下面兩個結構來維護,他們分別維護了內存池的頭部和數據部。此處數據部就是供用戶分配小塊內存的地方。
 

 
</div>
  1. //該結構用來維護內存池的數據塊,供用戶分配之用。    
  2. typedef struct {   
  3.     u_char *last; //當前內存分配結束位置,即下一段可分配內存的起始位置    
  4.     u_char *end; //內存池結束位置    
  5.     ngx_pool_t *next; //鏈接到下一個內存池    
  6.     ngx_uint_t failed; //統計該內存池不能滿足分配請求的次數    
  7. } ngx_pool_data_t;   
  8. //該結構維護整個內存池的頭部信息。   
</div>

 
</div>
  1. struct ngx_pool_s {  
  2.     ngx_pool_data_t d; //數據塊    
  3.     size_t max;  //數據塊的大小,即小塊內存的最大值    
  4.     ngx_pool_t *current;    //保存當前內存池    
  5.     ngx_chain_t *chain; //可以掛一個chain結構    
  6.     ngx_pool_large_t *large;    //分配大塊內存用,即超過max的內存請求    
  7.     ngx_pool_cleanup_t *cleanup;    //掛載一些內存池釋放的時候,同時釋放的資源。   
  8.     ngx_log_t *log;   
  9. };  
</div>

有了上面的兩個結構,就可以創建一個內存池了,nginx用來創建一個內存池的接口是:
 

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);

調用這個函數就可以創建一個大小為size的內存池了。
Nginx源碼剖析之內存池,與內存管理
    ngx_create_pool接口函數就是分配上圖這樣的一大塊內存,然后初始化好各個頭部字段(上圖中的彩色部分)。紅色表示的四個字段就是來自于上述的第一個結構,維護數據部分,
    由圖可知:last是用戶從內存池分配新內存的開始位置,end是這塊內存池的結束位置,所有分配的內存都不能超過end。藍色表示的max字段的值等于整個數據部分的長度。用戶請求的內存大于max時,就認為用戶請求的是一個大內存,此時需要在紫色表示的large字段下面單獨分配;用戶請求的內存不大于max的話,就是小內存申請,直接在數據部分分配,此時將會移動last指針(具體見上文2.4.1節)。

3.2、分配小塊內存(size <= max)
    上面創建好了一個可用的內存池了,也提到了小塊內存的分配問題。nginx提供給用戶使用的內存分配接口有:

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和ngx_pnalloc都是從內存池里分配size大小內存,至于分得的是小塊內存還是大塊內存,將取決于size的大小;
他們的不同之處在于,palloc取得的內存是對齊的,pnalloc則否。
ngx_pcalloc是直接調用palloc分配好內存,然后進行一次0初始化操作。
ngx_pmemalign將在分配size大小的內存并按alignment對齊,然后掛到large字段下,當做大塊內存處理。下面用圖形展示一下分配小塊內存的模型:
Nginx源碼剖析之內存池,與內存管理

    上圖這個內存池模型是由上3個小內存池構成的,由于第一個內存池上剩余的內存不夠分配了,于是就創建了第二個新的內存池,第三個內存池是由于前面兩個內存池的剩余部分都不夠分配,所以創建了第三個內存池來滿足用戶的需求。
    由圖可見:所有的小內存池是由一個單向鏈表維護在一起的。這里還有兩個字段需要關注,failed和current字段。failed表示的是當前這個內存池的剩余可用內存不能滿足用戶分配請求的次數,即是說:一個分配請求到來后,在這個內存池上分配不到想要的內存,那么就failed就會增加1;這個分配請求將會遞交給下一個內存池去處理,如果下一個內存池也不能滿足,那么它的failed也會加1,然后將請求繼續往下傳遞,直到滿足請求為止(如果沒有現成的內存池來滿足,會再創建一個新的內存池)。
    current字段會隨著failed的增加而發生改變,如果current指向的內存池的failed達到了4的話,current就指向下一個內存池了。猜測:4這個值應該是Nginx作者的經驗值,或者是一個統計值(詳見上文2.4.1節a部分)。

3.3、大塊內存的分配(size > max)
    大塊內存的分配請求不會直接在內存池上分配內存來滿足,而是直接向操作系統申請這么一塊內存(就像直接使用malloc分配內存一樣),
然后將這塊內存掛到內存池頭部的large字段下。內存池的作用在于解決小塊內存池的頻繁申請問題,對于這種大塊內存,是可以忍受直接申請的。
    同樣,用圖形展示大塊內存申請模型:
Nginx源碼剖析之內存池,與內存管理
    注意每塊大內存都對應有一個頭部結構(next&alloc),這個頭部結構是用來將所有大內存串成一個鏈表用的。
這個頭部結構不是直接向操作系統申請的,而是當做小塊內存(頭部結構沒幾個字節)直接在內存池里申請的。
這樣的大塊內存在使用完后,可能需要第一時間釋放,節省內存空間,因此nginx提供了接口函數:
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
此函數專門用來釋放某個內存池上的某個大塊內存,p就是大內存的地址。
ngx_pfree只會釋放大內存,不會釋放其對應的頭部結構,畢竟頭部結構是當做小內存在內存池里申請的;遺留下來的頭部結構會作下一次申請大內存之用。

3.4、cleanup資源
Nginx源碼剖析之內存池,與內存管理
 

    可以看到所有掛載在內存池上的資源將形成一個循環鏈表,一路走來,發現鏈表這種看似簡單的數據結構卻被頻繁使用。
    由圖可知,每個需要清理的資源都對應有一個頭部結構,這個結構中有一個關鍵的字段handler,handler是一個函數指針,在掛載一個資源到內存池上的時候,同時也會注冊一個清理資源的函數到這個handler上。即是說,內存池在清理cleanup的時候,就是調用這個handler來清理對應的資源。
    比如:我們可以將一個開打的文件描述符作為資源掛載到內存池上,同時提供一個關閉文件描述的函數注冊到handler上,那么內存池在釋放的時候,就會調用我們提供的關閉文件函數來處理文件描述符資源了。

3.5、內存的釋放
    nginx只提供給了用戶申請內存的接口,卻沒有釋放內存的接口,那么nginx是如何完成內存釋放的呢?總不能一直申請,用不釋放啊。針對這個問題,nginx利用了web server應用的特殊場景來完成;
    一個web server總是不停的接受connection和request,所以nginx就將內存池分了不同的等級,有進程級的內存池、connection級的內存池、request級的內存池。
也就是說,創建好一個worker進程的時候,同時為這個worker進程創建一個內存池,待有新的連接到來后,就在worker進程的內存池上為該連接創建起一個內存池;連接上到來一個request后,又在連接的內存池上為request創建起一個內存池。
    這樣,在request被處理完后,就會釋放request的整個內存池,連接斷開后,就會釋放連接的內存池。因而,就保證了內存有分配也有釋放。

小結:通過內存的分配和釋放可以看出,nginx只是將小塊內存的申請聚集到一起申請,然后一起釋放。避免了頻繁申請小內存,降低內存碎片的產生等問題。
 

參考文獻

  1. dreamice:http://bbs.chinaunix.net/thread-3626006-1-1.html;
  2. http://blog.csdn.net/livelylittlefish/article/details/6586946;
  3. dreamice’blog:http://blog.chinaunix.net/space.php?uid=7201775;
  4. http://www.tbdata.org/archives/1390。
     

后記    

    今閑來無事,拿著個nginx源碼在編譯器上做源碼剖析,鼓搗了一下午,至晚上不料中途停電,諸多部分未能保存。然不想白忙活,又花費了一個晚上,終至補全,方成上文,并修訂至五日凌晨三點。同時,也參考和借鑒了 dreamice、阿波等朋友們及yixiao等大牛的作品,異常感謝。讀者若有興趣,還可以看看sgi stl 的內存池及其管理(或者,日后自個也寫下)。
    還是常有朋友問諸如下算法學習心得之類的,其實,我也是草包一個,不過,到底看來看來上次寫的文章( 由快速排序引申而來--如何學習算法)令讀者不甚滿意。 學算法,先修心。當你養成了算法無懼的心理后,學習任何新的東西時,都將所向披靡。任何東西,扎扎實實學就夠了。 至于工作,12.07日上班,在微博上給自己添加非常榮幸的給自己新添加了三條標簽:推薦系統,機器學習,算法工程師。在學習決策樹的過程中,發現網上并無好的資料可看,打算擇日寫一下。
    另外,對此前寫的KMP算法做了總結,寫了續集,讀者可以參考之。  六之再續:KMP算法之總結篇/algorithm/20111205/315670.html。相信,看過此文后,無論是誰,都一定可以把KMP算法搞懂了。
本文來自: http://blog.csdn.net/v_july_v/article/details/7040425  

 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!