Nginx内存池

93 阅读9分钟

Nginx内存池是Nginx为了优化内存管理而引入的一种机制。在Nginx中,每个层级(如模板、TCP连接、HTTP请求等)都会创建一个内存池进行内存管理。当这些层级的生命周期结束时,整个内存池会被销毁,将分配的内存一次性归还给操作系统。

Nginx内存池的主要优势体现在:

  1. 减少频繁的malloc和free操作:通过内存池,避免了频繁的动态内存申请和释放,从而降低了内存管理的开销。
  2. 防止内存泄漏:内存池可以有效地避免因为申请未释放、二次释放或异常流程未释放而导致的内存泄漏问题。
  3. 提高内存使用效率:内存池可以优化内存的分配和回收,避免内存碎片的产生,从而提高内存的利用率。
  4. 提高系统稳定性:通过内存池,系统能够更好地管理内存资源,减少因为内存问题引发的异常,从而增强系统的稳定性和健壮性。

重要类型定义

// nginx内存池的主结构体类型 -> 作为整个内存池的头信息,只有第一个内存块才有
struct ngx_pool_s {
    ngx_pool_data_t d; // 内存池的数据头
    size_t max; // 小块内存分配的最大值
    ngx_pool_t *current; // 小块内存入口指针
    ngx_chain_t *chain; // 链接所有内存池
    ngx_pool_large_t *large; // 大块内存分配入口指针
    ngx_pool_cleanup_t *cleanup; // 清理函数handler的入口指针
    ngx_log_t *log;
};

typedef struct ngx_pool_s ngx_pool_t;
// 小块内存数据头信息
typedef struct {
    u_char *last; // 可分配内存开始位置
    u_char *end; // 可分配内存末尾位置
    ngx_pool_t *next; // 保存下一个内存块的地址
    ngx_uint_t failed; // 记录当前内存池分配失败的次数
} ngx_pool_data_t; // 每个内存块都有,记录本小块内存的信息

typedef struct ngx_pool_large_s ngx_pool_large_t;
// 大块内存类型定义 -> 保存大块内存的头信息
struct ngx_pool_large_s {
    ngx_pool_large_t *next; // 下一个大块内存
    void *alloc; // 记录分配的大块内存的起始地址
};

typedef void (*ngx_pool_cleanup_pt)(void *data); // 清理回调函数的类型定义 -> 保存清理相关操作,在内存池销毁的时候才成链的去调用

typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
// 清理操作的类型定义,包括一个清理回调函数,传给回调函数的数据和下一个清理操作的地址
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt handler; // 清理回调函数
    void *data; // 传递给回调函数的指针
    ngx_pool_cleanup_t *next; // 指向下一个清理操作
};

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log); // 创建内存池
void ngx_destroy_pool(ngx_pool_t *pool); // 销毁内存池
void ngx_reset_pool(ngx_pool_t *pool); // 重置内存池
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); // 内存分配函数,支持内存初始化0
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p // 内存释放(大块内存)

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); // 添加清理
handler

内存池创建函数剖析

// 创建内存池
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    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; // 返回创建内存池的起始地址
}

小块内存分配剖析

在Nginx中,小块内存通常指的是那些大小相对较小、分配和释放频率较高的内存块。这些内存块由于数量众多、管理复杂,因此需要使用一种高效的内存管理机制来减少内存管理的开销和内存碎片的产生。

  • Nginx内存池通过一种预分配和复用的方式来管理小块内存。当需要分配小块内存时,内存池会首先检查是否已经有可用的内存块。如果有,则直接从中分配一个;如果没有,则根据预设的规则进行内存块的预分配。这种预分配的策略可以确保在需要时能够快速获取到内存块,避免了频繁的malloc和free操作带来的开销。
void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) { // 指定的size <= 内存池小块内存分配的最大size -> 小块内存分配即可
        return ngx_palloc_small(pool, size, 0);
    }
#endif

    return ngx_palloc_large(pool, size); 
}

// 小块内存分配函数
static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align) // align表示是否考虑内存对齐
{
    u_char      *m;
    ngx_pool_t  *p;

    p = pool->current; // 每次都从内存池的current块进行内存分配

    do {
        m = p->d.last; // 可分配内存的起始地址

        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT); // 将m调整到平台相关的unsigned long的整数倍
        }

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

大块内存分配剖析

Nginx需要分配一块大块内存时,它通常会直接调用操作系统的内存分配函数(如malloc、calloc或posix_memalign等)。这些函数会根据请求的大小在操作系统的内存空间中寻找一块合适的连续内存区域,并将其分配给Nginx使用。由于大块内存的大小通常较大,因此操作系统能够更容易地找到满足需求的连续内存空间。

  • 在分配大块内存时,Nginx通常不会过多进行额外的内存池检查或预分配操作。这是因为大块内存的分配和释放相对较为罕见,且频繁地进行内存池检查会增加不必要的开销。
// 大块内存分配函数
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large; // 结构体中有下块地址next和本块地址alloc

    p = ngx_alloc(size, pool->log); // malloc开辟大块内存
    if (p == NULL) {
        return NULL;
    }

    n = 0;

    for (large = pool->large; large; large = large->next) {
        // 复用大块内存头信息体链表,现有的空头信息体(alloc为空是被释放过的)
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) { // 防止栈的次数太多浪费时间
            break;
        }
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); // 在小块内存的内存池中,存放大块内存的头信息体
    if (large == NULL) { // 开辟失败
        ngx_free(p); // 把新创建的大块内存空间free掉
        return NULL;
    }

    large->alloc = p; // 头结点记录大块内存的地址信息
    large->next = pool->large; // 大块内存头信息表(头插法)
    pool->large = large;

    return p;
}

内存池重置函数剖析

内存池重置:将内存池恢复到初始状态,释放已经分配但尚未使用的内存块,并重置内存池的内部状态信息。这样,当再次需要分配内存时,内存池可以从一个干净、一致的状态开始,方便重新使用。(清理已分配的内存块 → 重置内部状态信息 → 保持必要的配置信息)

// 小块内存无free函数,只能重置
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); // 释放内存但指针未置空 → 随后重置
        }
    }

    /* 源码有缺陷 → 造成除首块(含头信息)的每个块都浪费了ngx_pool_s-ngx_pool_data_t大小的空间
    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t); // 每个块都指向除头信息和块信息之外的内存起始地址
        p->d.failed = 0;
    }
    */

    // 修正
    // 第一块(有头信息)指向除头信息ngx_pool_s和块信息ngx_pool_data_t 之外的内存的首地址
    p = pool;
    p->d.last = (u_char*) p +sizeof(ngx_pool_t);
    p->d.failed = 0;

    // 第一块之后的每个块(无头信息)都指向除块信息ngx_pool_data_t之外的内存的首地址
    for(p = p->d.next; p; p = p->d.next){
        p->d.last = (u_char *) p + sizeof(ngx_pool_data_t); 
        p->d.failed = 0;
    }
    // 修正

    pool->current = pool; // 重置头信息
    pool->chain = NULL;
    pool->large = NULL; // 完全舍弃已经重置的大块内存
}

内存池销毁函数剖析

为什么先要释放外部资源?

  • 假设Nginx在处理一个HTTP请求时需要打开一个文件,并读取其中的内容。这个文件描述符就是一个外部资源,因为它不是由Nginx内存池直接管理的。Nginx可能会在内存池中分配一个结构体来存储与这个请求相关的状态信息,这个结构体中可能包含一个指向文件描述符的指针。当请求处理完毕,Nginx准备释放与这个请求相关的内存池时,如果仅仅释放内存池中的内存块,那么文件描述符并不会被自动关闭,这就会导致资源泄漏。为了避免这种情况,Nginx可以在创建内存池时注册一个回调函数,这个回调函数的职责就是在内存池被销毁时关闭文件描述符。
// 释放外部资源,销毁内存池(先释放外部资源,再释放大块内存,最后释放小块内存)
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);
        }
    }

#if (NGX_DEBUG) // 忽略不看

    /*
     * we could allocate the pool->log from this pool
     * so we cannot use this log while free()ing the pool
     */

    for (l = pool->large; l; l = l->next) {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
    }

    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

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            // 释放每一个大块内存
            ngx_free(l->alloc);
        }
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        // 释放每一个小块内存
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}