nginx内存池源码刨析

文章目录

  • Nignx内存池
  • 源码刨析
    • 1. 重要的类型定义
      • 1.1 宏定义
      • 1.2 类型定义
    • 2. 主要的函数接口
      • 2.1 内存池的创建
        • 2.1.1 ngx_memalign函数
        • 2.1.2 ngx_create_pool函数
      • 2.2 内存申请函数
      • 2.3 小块内存申请
      • 2.4 ngx_palloc_block内存块分配函数
      • 2.5 大块内存申请
      • 2.6 内存释放函数(大块内存)
      • 2.7 内存池重置函数
      • 2.8 添加外部清理函数
      • 2.9 内存池销毁函数

Nignx内存池

nginx内存池是一个设计很巧妙,效率也特别高的内存池,较SGI STL的内存池也有许多的不同,本篇文章将会简单刨析nginx内存池的源码,通过分析nginx内存池的源码,我们也可以明显的看到该内存池与SGI STL内存池的一些区别。

源码刨析

1. 重要的类型定义

1.1 宏定义

首先我们需要先认识一些宏定义,下面是一些主要的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 可以从内存池中申请的最大的内存大小(一页,4k)
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

// 内存池的默认大小(16k)
#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_align函数,这个函数的作用和SGI STL二级空间配置器的_S_round_up函数如出一辙,即将d的大小调整到最邻近的a的倍数,其完整定义如下:

1
2
// 把d调整为a的倍数
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

1.2 类型定义

接着我们需要了解内存池得基本结构,首先先看一下其中最基础得结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 小块内存数据头信息
typedef struct {
    u_char          *last;      // 可分配内存开始位置
    u_char          *end;       // 可分配内存末尾位置
    ngx_pool_t      *next;      // 保存下一个内存池的地址
    ngx_uint_t      failed;     // 记录当前内存池分配失败的次数
} ngx_pool_data_t;

// 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;       // 日志
};

内存池中最主要的结构是ngx_pool_s,其中的ngx_pool_data_t也为结构体,用来存储小块内存数据的头信息,各个成员的含义已经在代码中给出,通过下图,我们可以简单的认识一下上述的结构的关系
在这里插入图片描述
接着我们来看内存池中的其他结构,下面给出了内存池中大块内存的类型定义ngx_pool_large_s以及外部清理操作的类型定义ngx_pool_cleanup_s ,关于下面两个结构体的功能将会在下文介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 大块内存类型定义
struct ngx_pool_large_s {
    ngx_pool_large_t    *next;      // 下一个大块内存
    void                *alloc;     // 记录分配的大块内存的起始地址
};


typedef void (*ngx_pool_cleanup_pt)(void *data);    // 清理回调函数的类型定义

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

因为C语言的语法,每次在使用结构体类型的时候,前面的struct 是不可以省略的,所以为了提高代码的可读性,这里对上述的结构体的名字进行了重命名。

1
2
3
typedef struct ngx_pool_s ngx_pool_t;
typedef struct ngx_pool_large_s ngx_pool_large_t;
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;

2. 主要的函数接口

nginx内存池主要包括下列8个函数,下面将会对这8个函数进行一一解读

1
2
3
4
5
6
7
8
9
10
11
12
13
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

2.1 内存池的创建

2.1.1 ngx_memalign函数

在看内存池创建函数之前,我们先要了解ngx_memalign函数,其定义如下:

1
2
3
4
5
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);  // #1
#else
#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)    // #2
#endif

当包含NGX_HAVE_POSIX_MEMALIGN或者NGX_HAVE_MEMALIGN时,调用 #1函数

当没有上述两个宏,则调用 #2函数,即ngx_alloc(size, log),这个函数就是对malloc的一个封装

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

2.1.2 ngx_create_pool函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;
    // 申请size大小的空间
    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中的内存池是在创建的时候就设定好了大小,在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。当要分配大块内存是,则是在内存池外面再分配空间进行管理的,称为大块内存池。

创建内存池函数先申请size字节大小的内存,然后将last指针指向可分配内存的起始位置,end指针指向该内存池的最后位置。其中max指向的是该内存池一次可分配的最大的大小,若当前内存池可分配大小大于NGX_MAX_ALLOC_FROM_POOL(4k),则max为NGX_MAX_ALLOC_FROM_POOL;否则max的值为当前内存池可分配大小。

2.2 内存申请函数

nginx对内存的管理分为大内存与小内存,当某一个申请的内存大于某一个值时,就需要从大内存中分配空间,否则从小内存中分配空间。

内存申请函数有三个,实现也特别的简单,其中调用了小块内存申请和大块内存申请两个函数,后面我将会对那两个函数进行简单的讲解,首先我们先看一下内存申请的三个函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 内存分配函数,支持内存对齐
void *ngx_palloc(ngx_pool_t *pool, size_t size)
{
    if (size <= pool->max) {    // 申请小块内存
        return ngx_palloc_small(pool, size, 1);
    }
   
    // 申请大块内存
    return ngx_palloc_large(pool, size);
}

// 内存分配函数,不支持内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
    if (size <= pool->max) {    // 申请小块内存
        return ngx_palloc_small(pool, size, 0);
    }

    // 申请大块内存
    return ngx_palloc_large(pool, size);
}

// 内存分配函数,支持内存初始化0
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;
}

上面三个函数就是三种不同的申请内存函数,其中关于是否支持内存对齐是由ngx_palloc_small的第三个参数决定的,为0则不支持对齐,为1则支持对齐;第三个可初始化为0的函数首先调用了ngx_palloc申请空间,申请成功后则调用ngx_memzero来进行初始化,ngx_memzero是对memset函数的封装,其定义如下:

1
2
// 将buf初始化为0
#define ngx_memzero(buf, n)       (void) memset(buf, 0, n)

2.3 小块内存申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static ngx_inline 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);
}

ngx_palloc_small函数首先先将指针指向内存池的入口,紧接着判断是否需要内存对齐,内存对齐是一个定义的宏,其定义如下:

1
2
3
4
5
6
// 小块内存分配考虑字节对齐时的单位
#define NGX_ALIGNMENT   sizeof(unsigned long)

// 将p指针按a对齐(将p调整为a的临近的倍数)
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

这是一个用来内存地址取整的宏,非常精巧,作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以需要自己实现该代码。
在本函数中,将会依据不同的应用场景(32位4字节,64位 8字节)把m指针的地址 调整为与平台相关的NGX_ALIGNMENT(4 或者 8)的整数倍的地址上去。

指针对齐操作完成后,会依此判断当前内存池是否可以分配空间,若可以顺利分配,就返回分配内存的首地址;若均分配失败,就需要申请新的内存块。
在这里插入图片描述
上图就是对申请小块内存的一个简单描述。

2.4 ngx_palloc_block内存块分配函数

当申请小块内存失败时,就需要重新申请一块内存块用来给用户分配内存,ngx_palloc_block函数就是解决这样一个问题,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static 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指向可分配空间的起始位置
    m = ngx_align_ptr(m, NGX_ALIGNMENT);// 使m指针对齐
    new->d.last = m + size;             // 偏移last指针,给用户分配内存

    // 因为小块内存分配失败,对每个failed++,若failed>4,则更新内存池的入口指针
    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;
}

该函数的实现较为简单,主要功能就是申请一块和内存池中内存块大小相同的一个空间,再将数据块信息进行初始化,紧接着给用户分配空间。分配完成后,需要遍历整个内存池,将所有内存块的failed+1,当一个内存块四次内存分配都失败,说明该内存块的可用空间已经不多,就需要调整内存池的入口指针。
在这里插入图片描述
通过上图我们就可以更好的理解该函数所进行的操作:申请好空间new后,先将m指向可分配内存的起始地址,再偏移last指针,为用户分配size大小的空间,将end指向该内存块的最后,然后将这个新的内存块连接到内存池中去。

2.5 大块内存申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static 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);     // 通过malloc申请大块内存
    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);    // ngx_free就是free函数
        return NULL;
    }

    large->alloc = p;           // 将alloc指向大块内存
    large->next = pool->large;  // 将该大块内存头插到内存池large处
    pool->large = large;

    return p;
}

在这里插入图片描述
如上图所示,函数首先先通过ngx_alloc申请一块内存,然后通过循环查看大块内存链表的前三个内存头下有没有数据:如果前三个大块内存头信息下都有大块内存,则继续执行程序,在小块内存中申请一个大块内存头节点,并将该头节点以头插的方式加入链表中;若里面有一个为空,则将该内存头的alloc指向大块内存。

2.6 内存释放函数(大块内存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) {    // 如果alloc指向的是需要释放的内存,将该块内存释放掉,将指针置空
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            ngx_free(l->alloc);  // free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

我们可以看到,内存池只提供了大块内存的释放而没有提供小块内存的释放。实际上从内存池小块内存的分配方式来看,他也无法提供小块内存的释放,因为小块内存是通过last指针偏移来实现的,所以内存池小块内存的分配效率特别的高。正所谓鱼和熊掌不可兼得,小块内存仅靠两个指针就实现了内存的分配,那么对于回收就无法进行控制。

就nginx而言,小块内存也是不需要进行回收的,这与nginx的应用场景有关:nginx的本质是http服务器,http服务器是一个短链接的服务器,客户端(浏览器)发起一个request请求,到达nginx服务器以后,处理完成后nginx给客户端返回一个response响应,http服务器就主动断开tcp连接,这时就可以调用重置函数重置内存池。
http1.1以后支持keep-alive(60s)即http服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间,否则60s之内没有客户端发来的响应, nginx就主动断开连接,此时 nginx可以调用ngx reset_poo1重置内存池,等待下一次该客户端的请求。

2.7 内存池重置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
        }
    }

    // 重置所有的last指针,将其指向可分配的起始位置
    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;
}

对于内存重置函数,首先先将内存池中的所有大块内存都释放掉,再将所有的内存块的last指针重置即可。因为内存池中只有第一个内存块的结构为ngx_pool_t,而剩余的都为ngx_pool_data_t,所以若按源码来看,将会造成部分的空间浪费,对于第二个for循环,我认为可做如下改进:

1
2
3
4
5
6
7
8
9
10
    // 处理第一块内存池
    p = pool;
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.failed = 0;

    // 第二块内存池到最后一块
    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;
    }

2.8 添加外部清理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
    }

    // 如果size指定大小,就为data开辟size大小的内存
    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;
}

在说明该函数前,我们先看这样一个问题:
在这里插入图片描述
在该内存池的一块大块内存上,有一个指针指向一个外部资源,当我们释放内存池的时候就需要先将外部的资源释放掉,进而在释放内存池中的内存,这就需要提供一个外部清理函数,对于该函数我们可做如下运用:
在这里插入图片描述
将清理外部资源函数的赋予handler,并将指向资源的指针赋予data作为handler函数的参数。

2.9 内存池销毁函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);    // 调用handler函数
        }
    }

    // 清理大块内存
    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;
        }
    }
}

销毁内存池主要分为三步:先释放外部资源,在释放大块内存,最后释放内存池。这三步的顺序不可乱,因为外部资源和大块内存的地址信息均存储在内存池中,所以内存池必须最后释放,最先释放的肯定是外部资源,接着就是大块内存,这样才能保证不会造成内存泄漏。