2

Nginx源码分析-内存池

 3 years ago
source link: https://atticuslab.com/2020/10/06/nginx-annotated-4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。

内存池概念一定程度上符合 “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决“ 的设计理念,内存池避免出现内存碎片,减少向操作系统申请内存的次数,降低各个模块的开发复杂度。

nginx内存池综述

nginx设计了一种简单的内存池,即不负责回收内存池中已分配的内存,只在该内存池对应使用区域的生命周期结束时才销毁内存池,并把内存一次性归还给操作系统。

这样的优点在于大大减少了频繁的mallocfree,降低了CPU资源消耗,也减少了了内存碎片。但是也引入了一个新问题:即如果使用内存池的区域生命周期很长,但分配的每一块内存使用的生命周期很短,那么会导致内存不能及时回收,造成内存浪费。所有一般应用中不会这么设计内存池,而nginx严格明确生命周期,同时在设计上考虑了大小块内存不同分配策略,所有该问题在nginx中没有影响。

ngx_pool_t结构体

nginx内存池结构定义在src/core/ngx_palloc.h文件,与内存池设计相关的文件还有src/core/ngx_palloc.c,os/unix/alloc.c,os/unix/alloc.h文件。

nginx内存池在设计上区分大小块内存,通常小于等于NGX_MAX_ALLOC_FROM_POOL大小意味着小块内存,但不是绝对,当调用ngx_create_pool创建内存,size参数小于NGX_MAX_ALLOC_FROM_POOL+sizeof(ngx_pool_t)时,则size-sizeof(ngx_pool_t)字节就是小块内存标准。

/* ngx_pagesize在main函数调用ngx_os_init函数时初始化 */
2
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

小块内存,通过从进程的堆中预分配更多的内存,而后直接使用这块内存的一部分作为小块内存返回给申请者,以此实现减少碎片和调用malloc的次数。当内存池预分配的size不足使用时,就会再接着分配一个小块内存池,用ngx_pool_data_tnext成员相连。新增的ngx_pool_t结构体中与小块内存无关的其他成员此时是无意义的,例如max不会赋值,lagre链表为空等

typedef struct ngx_pool_large_s  ngx_pool_large_t;
2
3
struct ngx_pool_large_s {
4
    ngx_pool_large_t     *next;     /* 所有大块内存通过next指针联起来 */
5
    void                 *alloc;    /* 数据域 */
6
};
7
8
typedef struct {
9
    u_char               *last;     /* 指向未分配的空闲内存的首地址 */
    u_char               *end;      /* 指向当前小块内存池的尾部 */
    ngx_pool_t           *next;     /* 同属一个pool的多个小块内存间,通过next相连 */
    /* 当剩余空间不足以分配小块内存时,failed成员就会+1,当failed成员大于4
     * 后,ngx_pool_t的current将移向下一个小块内存池 */
    ngx_uint_t            failed;
} ngx_pool_data_t;
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
20
    /* 小块内存相关结构,当分配小块内存时,剩余的预分配空间不足时,会在分配一个
21
     * ngx_pool_t,会在d中的next成员构成单链表 */
22
    ngx_pool_data_t       d;
23
    size_t                max;      /* 评估内存属于小块内存还是大块内存的标准 */
24
    /* 多个小块内存池构成链表时,current指向分配内存时遍历的第一个小块内存池 */
25
    ngx_pool_t           *current;
26
    ngx_chain_t          *chain;    /* 与内存池关系不大,略过 */
27
    /* 大块内存堆分配,这里组成单向链表,便于销毁时,同时释放 */
28
    ngx_pool_large_t     *large;
29
    /* 所有待清理资源(例如需要关闭或删除的文件等)组成单链表 */
30
    ngx_pool_cleanup_t   *cleanup;
31
    ngx_log_t            *log;      /* 日志 */
32
};

ngx_pool_cleanup_t结构体

ngx_pool_t不止希望程序员不用释放内存,而且还能不需要释放如文件等资源。使用ngx_pool_cleanup_add方式添加清理方法。

typedef void (*ngx_pool_cleanup_pt)(void *data);
2
3
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
4
5
struct ngx_pool_cleanup_s {
6
    ngx_pool_cleanup_pt   handler;  /* 初始化为NULL,需要设置清理方法 */
7
    void                 *data;     /* 清理方法对应的参数 */
8
    ngx_pool_cleanup_t   *next;     /* 多个方法组成链表 */
9
};

内存池相关方法

内存池操作

创建内存池,size大小包含了sizeof(ngx_pool_t)的管理结构大小,通常可以设置sizeNGX_DEFAULT_POOL_SIZE

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)
2
{
3
    ngx_pool_t  *p;
4
5
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
6
    if (p == NULL) {
7
        return NULL;
8
    }
9
    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;
20
    p->large = NULL;
21
    p->cleanup = NULL;
22
    p->log = log;
23
24
    return p;
25
}

销毁内存池,同时会把通过该pool分配出的内存释放,还会执行通过ngx_pool_cleanup_add方法添加的各类资源清理方法。

void ngx_destroy_pool(ngx_pool_t *pool)
2
{
3
    ngx_pool_t          *p, *n;
4
    ngx_pool_large_t    *l;
5
    ngx_pool_cleanup_t  *c;
6
7
    for (c = pool->cleanup; c; c = c->next) {
8
        if (c->handler) {
9
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }
    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) {
20
            ngx_free(l->alloc);
21
        }
22
    }
23
24
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
25
        ngx_free(p);
26
27
        if (n == NULL) {
28
            break;
29
        }
30
    }
31
}

重置内存池,即将内存池中的原有内存释放后继续使用。该实现会把大块内存释放给操作系统,小块内存不释放。

void ngx_reset_pool(ngx_pool_t *pool)
2
{
3
    ngx_pool_t        *p;
4
    ngx_pool_large_t  *l;
5
6
    for (l = pool->large; l; l = l->next) {
7
        if (l->alloc) {
8
            ngx_free(l->alloc);
9
        }
    }
    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;
20
}

内存池分配,释放操作

分配地址对齐的内存。按总线长度(例如sizeof(unsigned long))对齐地址后,可以减少CPU读取内存的次数,当然代价是一些内存浪费。

void *ngx_palloc(ngx_pool_t *pool, size_t size)
2
{
3
    u_char      *m;
4
    ngx_pool_t  *p;
5
6
    if (size <= pool->max) {
7
8
        p = pool->current;
9
        do {
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
            if ((size_t) (p->d.end - m) >= size) {
                p->d.last = m + size;
                return m;
            }
            p = p->d.next;
20
        } while (p);
21
22
        return ngx_palloc_block(pool, size);
23
    }
24
25
    return ngx_palloc_large(pool, size);
26
}

分配内存时,不进行地址对齐操作。

void *ngx_pnalloc(ngx_pool_t *pool, size_t size)
2
{
3
    u_char      *m;
4
    ngx_pool_t  *p;
5
6
    if (size <= pool->max) {
7
8
        p = pool->current;
9
        do {
            m = p->d.last;
            if ((size_t) (p->d.end - m) >= size) {
                p->d.last = m + size;
                return m;
            }
            p = p->d.next;
20
21
        } while (p);
22
23
        return ngx_palloc_block(pool, size);
24
    }
25
26
    return ngx_palloc_large(pool, size);
27
}

分配出地址对齐的内存后,在调用memset将这些内存全部清0。

void* ngx_pcalloc(ngx_pool_t *pool, size_t size)
2
{
3
    void *p;
4
5
    p = ngx_palloc(pool, size);
6
    if (p) {
7
        ngx_memzero(p, size);
8
    }
9
    return p;
}

如果pool空间已经用完,则从新开辟空间ngx_pool_t。

static void * ngx_palloc_block(ngx_pool_t *pool, size_t size)
2
{
3
    u_char      *m;
4
    size_t       psize;
5
    ngx_pool_t  *p, *new;
6
7
    psize = (size_t) (pool->d.end - (u_char *) pool);
8
9
    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;
20
    m += sizeof(ngx_pool_data_t);
21
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
22
    new->d.last = m + size;
23
24
    for (p = pool->current; p->d.next; p = p->d.next) {
25
        if (p->d.failed++ > 4) {
26
            pool->current = p->d.next;
27
        }
28
    }
29
30
    p->d.next = new;
31
32
    return m;
33
}

大块内存分配。

static void * ngx_palloc_large(ngx_pool_t *pool, size_t size)
2
{
3
    void              *p;
4
    ngx_uint_t         n;
5
    ngx_pool_large_t  *large;
6
7
    p = ngx_alloc(size, pool->log);
8
    if (p == NULL) {
9
        return NULL;
    }
    n = 0;
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
20
        if (n++ > 3) {
21
            break; 
22
        }
23
    }
24
25
    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
26
    if (large == NULL) {
27
        ngx_free(p);
28
        return NULL;
29
    }
30
31
    large->alloc = p;
32
    large->next = pool->large;
33
    pool->large = large;
34
35
    return p;
36
}

按参数alignment进行地址对齐来分配内存,这样分配出的内存不管申请的size有多小,都是不会使用小块内存池,会从进程的堆中分配内存,并挂在大块内存组成的large链表中。

void * ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
2
{
3
    void              *p;
4
    ngx_pool_large_t  *large;
5
6
    p = ngx_memalign(alignment, size, pool->log);
7
    if (p == NULL) {
8
        return NULL;
9
    }
    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;
20
21
    return p;
22
}

提前释放大块内存,它的效率不高。

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)
2
{
3
    ngx_pool_large_t  *l;
4
5
    for (l = pool->large; l; l = l->next) {
6
        if (p == l->alloc) {
7
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
8
                           "free: %p", l->alloc);
9
            ngx_free(l->alloc);
            l->alloc = NULL;
            return NGX_OK;
        }
    }
    return NGX_DECLINED;
}

内存池同步操作

添加一个需要在内存池释放时的同步释放的资源。

ngx_pool_cleanup_t * ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
2
{
3
    ngx_pool_cleanup_t  *c;
4
5
    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
6
    if (c == NULL) {
7
        return NULL;
8
    }
9
    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }
    } else {
        c->data = NULL;
    }
20
    c->handler = NULL;
21
    c->next = p->cleanup;
22
23
    p->cleanup = c;
24
25
    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
26
27
    return c;
28
}

在内存池释放前,如果需要提前关闭文件,则调用该方法。

void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
2
{
3
    ngx_pool_cleanup_t       *c;
4
    ngx_pool_cleanup_file_t  *cf;
5
6
    for (c = p->cleanup; c; c = c->next) {
7
        if (c->handler == ngx_pool_cleanup_file) {
8
9
            cf = c->data;
            if (cf->fd == fd) {
                c->handler(cf);
                c->handler = NULL;
                return;
            }
        }
    }
}

以关闭文件来释放资源的方法。

void ngx_pool_cleanup_file(void *data)
2
{
3
    ngx_pool_cleanup_file_t  *c = data;
4
5
    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
6
                   c->fd);
7
8
    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
9
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

以删除文件来释放资源的方法。

void ngx_pool_delete_file(void *data)
2
{
3
    ngx_pool_cleanup_file_t  *c = data;
4
5
    ngx_err_t  err;
6
7
    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
8
                   c->fd, c->name);
9
    if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
        err = ngx_errno;
        if (err != NGX_ENOENT) {
            ngx_log_error(NGX_LOG_CRIT, c->log, err,
                          ngx_delete_file_n " \"%s\" failed", c->name);
        }
    }
    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
20
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
21
                      ngx_close_file_n " \"%s\" failed", c->name);
22
    }
23
}

操作系统上内存操作封装

void * ngx_alloc(size_t size, ngx_log_t *log)
2
{
3
    void  *p;
4
5
    p = malloc(size);
6
    if (p == NULL) {
7
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
8
                      "malloc(%uz) failed", size);
9
    }
    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
    return p;
}
void * ngx_calloc(size_t size, ngx_log_t *log)
2
{
3
    void  *p;
4
5
    p = ngx_alloc(size, log);
6
7
    if (p) {
8
        ngx_memzero(p, size);
9
    }
    return p;
}
#define ngx_free          free

内存分配流程示例

  1. 将申请的内存大小sizengx_pool_tmax成员比较,以决定申请的是小块内存还是大块内存。如果size<=max,则继续执行第2步开始分配小块内存,否则跳到第10步分配大块内存。
  2. 取到ngx_pool_tcurrent指针,它表示应当首先尝试从这个小块内存池里分配,因为current之前的pool已经屡次分配失败(大于4次),其剩余空间多半无法满足size。这当然是一种浪费的预估,但性能不坏。
  3. 从当前小块内存池的ngx_pool_data_tlast指针入手,先调用ngx_align_ptr找到last后最近的对齐地址。
    #define ngx_align_ptr(p, a)                               \
    2
        (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & \
    3
        ~((uintptr_t) a - 1))
    4
    5
    #define NGX_ALIGNMENT   sizeof(unsigned long)
    6
    7
    ngx_pool_t *p = ...;
    8
    u_char* m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
  4. 比较对齐地址与ngx_pool_data_tend指针间是否可以容纳size字节。如果end - m >= size,那么继续执行第5步准备返回地址m;否则,在检查ngx_pool_datanext指针是否为NULL,如果是空指针,那么跳到第6步准备再申请新的小块内存池,不为空则跳到第3步继续遍历小块内存池构成的链表。
  5. 先将ngx_pool_data_tlast指针置为下次空闲内存的首地址,例如:p->d.last = m + size;再返回地址m,分配内存流程结束。
  6. 分配一个大小与上一个ngx_pool_t一致的内存池专用于小块内存的分配。内存池大小获取简单:(size_t)(pool->d.end - (u_char *)pool)
  7. 将新内存池的空闲地址的首地址对齐,作为返回给申请的内存,在设last到空闲内存的首地址。
  8. current指向的小块内存池开始遍历到当前的新内存池,依次将各failed成员加1,并把current指向首个failed<=4的小块内存池,用于下一次的小块内存分配。
  9. 返回第7步对齐的地址,分配流程结束。
  10. 调用ngx_alloc方法从进场的堆内存中分配size大小的内存。
  11. 遍历ngx_pool_tlarge链表,看看有没有ngx_pool_large_talloc成员值为NULL(这个alloc指向的大块内存执行过ngx_pfree方法)。如果找到了这个ngx_pool_large_t,继续执行第12步;否则,跳到第13步。为了防止large链表过大,遍历次数做了限制,例如最多4次还未找到alloc==NULL的元素,也会跳出这个遍历循环进行第13步。
  12. ngx_pool_large_talloc成员设置为第10步分配的内存地址,返回地址,分配流程结束。
  13. 从内存池中分配出ngx_pool_large_t结构体,alloc成员置为第10步分配的内存地址,将ngx_pool_large_t添加到ngx_pool_tlarge链表首部,返回地址,分配流程结束。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK