Nginx内存池源码剖析

Nginx 源码版本: 1.13.1

Nginx 内存池的定义主要位于如下两个文件中:

  • ngx_palloc.h
  • ngx_palloc.c

首先是几个重要的宏定义:

#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)
#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)
#define NGX_POOL_ALIGNMENT       16
#define NGX_MIN_POOL_SIZE                                                     \
    ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)),            \
              NGX_POOL_ALIGNMENT)

它们的含义分别如下:

  • NGX_MAX_ALLOC_FROM_POOL: 最多可以从内存池中取得的大小,在 x86 机器上为 4095
  • NGX_DEFAULT_POOL_SIZE: 默认的内存池大小,16K
  • NGX_POOL_ALIGNMENT: 内存池字节对齐相关
  • NGX_MIN_POOL_SIZE: 最小的内存池大小

其中的 ngx_align 的定义如下:

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

上述两个宏函数的作用分别是:(1) 将数值 d 调整到 a 的临近倍数;(2) 将指针 p 调整到 a 的临近倍数。类似于 SGI STL 中的位运算设计。

然后介绍几个重要的数据类型,它们被用来表示内存池的头部信息:

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;

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;
    ngx_log_t            *log;
};

typedef struct ngx_pool_s ngx_pool_t;

1. 调整内存边界

函数 ngx_memalign 是一个调整内存对齐的函数,分为 Windows 平台和 Unix 两种平台实现,其中 Unix 平台下的实现如下,通过两个宏 NGX_HAVE_POSIX_MEMALIGNNGX_HAVE_MEMALIGN 进行控制:

/*
 * Linux has memalign() or posix_memalign()
 * Solaris has memalign()
 * FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
 * aligns allocations bigger than page size at the page boundary
 */
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
	void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);
#else
	#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)
#endif

其中的 ngx_alloc 函数实现如下,可以看到,其内部实现实际上调用的就是 malloc 函数来分配动态内存:

void* ngx_alloc(size_t size, ngx_log_t *log) {
    void  *p;
    p = malloc(size);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "malloc(%uz) failed", size);
    }
    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
    return p;
}

如果定义了 NGX_HAVE_POSIX_MEMALIGN 宏,则会调用如下函数:

void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
    void  *p;
    int    err;
    err = posix_memalign(&p, alignment, size);
    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;
}

如果定义了 NGX_HAVE_MEMALIGN 宏,则会调用如下函数:

void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
    void  *p;
    p = memalign(alignment, size);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "memalign(%uz, %uz) failed", alignment, size);
    }
    ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0, "memalign: %p:%uz @%uz", p, size, alignment);
    return p;
}

如上两个函数会根据传入的 alignment 函数参数进行字节对齐。

2. 创建内存池

首先来看一下函数 ngx_create_pool,其作用是创建一个内存池,其源码如下:

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

该函数根据用户传入的 size 大小来开辟内存池,首先调用了 ngx_memalign 函数来进行字节对齐和动态内存分配,字节对齐使用的是上方的 NGX_POOL_ALIGNMENT 宏。同时可以根据不同的平台所定义的宏来调用不同的内存分配函数,如果没有相关的宏,则实质调用的是 malloc 函数来进行动态内存分配。

然后,分别初始化 d.lastd.endd.nextd.failed,可以看出来,d.last 指向了内存池头部信息的末尾位置,d.end 则指向了内存池的最末尾位置,如下图所示:

然后通过用 size 减去内存池头部数据的长度,得到内存池的可用空间大小。而 max 则调整为 sizeNGX_MAX_ALLOC_FROM_POOL 的最小值,保证内存池的最大容量不超过一页。然后 current 指针则指向了当前内存池的起始地址,示意图如下:

创建成功后,返回内存池头部地址即可。其它头部信息后面再说。

3. 向内存池申请内存

如果需要向内存池申请内存,则可用调用如下几个函数:

函数 ngx_palloc 的作用是向内存池申请 size 大小的内存,同时使用字节对齐:

void* ngx_palloc(ngx_pool_t *pool, size_t size) {
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif
    return ngx_palloc_large(pool, size);
}

函数 ngx_pnalloc 的作用是向内存池申请 size 大小的内存,但不使用字节对齐:

void* ngx_pnalloc(ngx_pool_t *pool, size_t size) {
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 0);
    }
#endif
    return ngx_palloc_large(pool, size);
}

函数 ngx_pcalloc 的作用是先申请内存,然后对内存块清零:

void* ngx_pcalloc(ngx_pool_t *pool, size_t size) {
    void *p;
    p = ngx_palloc(pool, size);
    if (p) {
        ngx_memzero(p, size);
    }
    return p;
}

综上可以看到,向内存池申请内存时,Nginx 会根据用户传入的 size 参数来选择调用 ngx_palloc_small 函数和 ngx_palloc_large 函数,前者用来申请小块内存,后者用来申请大块内存,可以看到,小块内存和大块内存的分界线便是头部信息中的 max 参数。

4. 申请小块内存

如果申请的是小块内存,则调用 ngx_palloc_small 函数,其代码如下:

void *ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align) {
    u_char      *m;
    ngx_pool_t  *p;
    p = pool->current;
    do {
        m = p->d.last;
        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
        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);
}

可以看到,在循环中,先获取了内存池头部信息的末尾位置,然后根据用户传入的 align 参数来确定是否调用 ngx_align_ptrd.last 进行字节调整,即调整内存池头部信息的末尾位置。

此后,如果内存池末尾位置减去头部信息末尾位置的大小大于等于 size 参数,即内存池可用空间大小要大于用户需要的大小,则简单的调整 d.last 指针即可,这也是 Nginx 内存池分配内存快的原因。

而如果可用空间小于用户的需求量,那么会通过 d.next 指针进入下一个内存块,由于初始化时该指针为空,则会跳出循环,转而调用 ngx_palloc_block 函数创建一个新的内存池。

我们也可以根据 next 字段的存在大概猜到,Nginx 的小块内存采用的是链表结构。

5. 创建次级内存池

这里我暂且称 ngx_palloc_block 函数所创建的内存池为次级内存池,其代码如下:

void* ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;
    
    psize = (size_t) (pool->d.end - (u_char *) pool);
    
    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;
    
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;
    
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }
    
    p->d.next = new;
    
    return m;
}

该函数会先创建一个新的内存池,该内存池的大小与之前创建的内存池大小相同,不同的只是该次级内存池只保留有 ngx_pool_data_t 的相关信息。然后在这个次级内存池中取出用户需要的部分,并调整相关指针、调整边界对齐等。

在最后的循环中,从内存池链表的 current 指针开始,遍历内存池链表,如果某个内存池的 failed 字段比 4 大,则表明该内存池已经分配失败至少 4 次了,说明该内存池的可用空间大小已经不足以分配新的内存空间了,于是就让 current 指向下一个内存池节点。

最后将新创建的次级内存池插入到内存池链表的末尾,返回用户所需的内存空间。

下图为小块内存池链表的相关信息,可见,由于第一个小块内存池的 failed 字段为 5,则其 current 字段则指向了下一个 failed 字段不为 4 的小块内存池,各个小块内存池之间通过 next 指针形成链表形式的数据结构。同时,可以看到,除了第一个内存池之外,后面的所有次级内存池都只有 last、end、next、failed 这四个头部信息:

6. 创建大块内存

首先来看一个关于大块内存信息的数据结构:

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

其中的 next 指针用于指向下一个大块内存池,和小块内存池一样,其也是一个链表形式的数据结构。另外的 alloc 参数则用于指向在堆中申请的大块内存空间。

如果用户需要的内存空间大于 max 字段,则会调用 ngx_palloc_large 函数来创建大块内存池,其源码如下:

void* ngx_palloc_large(ngx_pool_t *pool, size_t size) {
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;
    
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }
    
    n = 0;
    
    for (large = pool->large; large; large = large->next) {
        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);
        return NULL;
    }
    
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;
    
    return p;
}

该函数先调用了 ngx_alloc 函数来申请堆内存,前面阅读源码我们知道,ngx_alloc 函数的底层就是调用的 malloc 函数。然后遍历大块内存池链表,如果有某个大块内存的的 alloc 字段为空,则让该字段指向新申请的堆内存。

为了效率考虑,只寻找 3 次,如果没有找到,则在小块内存池中申请一部分空间用于存放 ngx_pool_large_t 类型,且该结构的 alloc 字段指向新创建的大块堆内存,然后使用头插法放入大块内存的链表中。

大块内存的相关示意图如下所示:

7. 释放大块内存

函数 ngx_pfree 是用来释放大块内存的,其源码如下:

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

可见,该函数会遍历大块内存链表,找寻要释放的大块内存,通过调用 ngx_free,即底层的 free 来释放大块内存空间。释放完成后,将 alloc 字段置为空,用于下次存放重新申请的大块内存。

注意:Nginx 内存池不存在对于小块内存的释放函数,因为从小块内存池中取出区块是通过偏移 d.last 指针来完成的,如果现在在小块内存池中有 3 块连续的内存:1、2、3,现在需要释放内存块 2,很显然,内存块 2 释放后还需要将内存块 1 和内存块 3 拼接在一起,并不高效。

如此设计的原因是因为 Nginx 的应用场景,由于 Nginx 是一个 短链接 的服务器,浏览器(即客户端)发送一个 Request 到达 Nginx 服务器并处理完成,Nginx 会给客户端返回一个 Response 响应,此时 HTTP 服务器就会主动断开 TCP 连接。即使在 HTTP 1.1 中有了 60s 的 Keep Alive 心跳时间(即返回响应后,等待 60s,如果这 60s 内客户端又发来请求,就重置这个时间,否则就主动断开连接),在超过心跳时间之后,Nginx 就可以调用 ngx_reset_pool 来重置内存池,等待下一个连接的到来。

而如果将该内存池的分配方案应用于一个长连接的服务器,那么内存池模块会持续申请小块内存,而得不到释放,则会一直申请直到服务器资源耗尽。如果需要在长连接的服务器中使用内存池模块,那么可以使用 SGI 的二级空间配置器方案。

8. 内存池重置

内存池重置操作是通过 ngx_reset_pool 函数来完成的,其源码如下:

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

该函数先遍历大块内存池链表,释放大块内存池。然后遍历小块内存池链表,调整 d.last 指针的偏移,并将 failed 字段重置为 0。

注意,释放小块内存池的循环代码中,存在些许问题,由于只有 pool 指针所指的第一个小块内存池具有全局的数据信息,而后面的次级小块内存池则仅仅包含 lastendnextfailed 这四个信息,但是在该循环中,是按照 ngx_pool_t 的长度来调整 last 指针的,这会使得后面的次级小块内存池在重置后浪费掉部分空间。

9. 清理回调函数

现在来考虑如下场景,如果需要申请一个大块内存,该大块内存用于存放一个如下的结构体类型:

struct Data {
	char* str;
	... // 其它成员
};

其中的 str 字段则指向了堆上的一块内存区域,如果现在调用 ngx_pfree 对该大块内存池进行释放,观察 ngx_pfree 的相关源代码可知,其并未处理 str 字段所指向的堆内存,这就会造成内存泄漏。同时由于 C 语言并不存在析构函数来进行内存的清理工作,因此 Nginx 设计了一个回调函数,用于进行内存的清理工作。

位于 ngx_pool_s 结构体中的 cleanup 字段便是做的如此工作:

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;
    ngx_log_t            *log;
};

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

其中的三个字段的作用如下:

  • handler: 存放清理数据的回调函数
  • data: 用于存放回调函数的函数参数
  • next: 表示回调函数也是一个链表形式的数据结构,指向下一个回调函数结构

10. 绑定回调函数

函数 ngx_pool_cleanup_add 的作用便是用来绑定回调函数,其源代码如下:

ngx_pool_cleanup_t* ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
    ngx_pool_cleanup_t  *c;
    
    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }
    
    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }
    } else {
        c->data = NULL;
    }
    
    c->handler = NULL;
    c->next = p->cleanup;
    
    p->cleanup = c;
    
    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
    
    return c;
}

可见,ngx_pool_cleanup_t 也是存放于小块内存池中的,函数最终返回一个 ngx_pool_cleanup_t 的结构,用于用户绑定回调函数。

其示意图如下:

11. 清理内存池

函数 ngx_destory_pool 的作用是清理内存池,其相关代码如下:

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

可见,它首先遍历清理用户数据的回调函数链表,调用相应的回调函数来清理内存。然后遍历大块内存池链表以释放大块内存,最后遍历小块内存池链表清理小块内存。

12. 总结

Nginx 申请内存的流程如下:

posted @ 2022-12-20 01:14  Leaos  阅读(219)  评论(0编辑  收藏  举报