首页 > 系统相关 >Nginx内存池源码剖析

Nginx内存池源码剖析

时间:2022-12-20 01:22:06浏览次数:71  
标签:log large Nginx 源码 内存 ngx pool size

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 申请内存的流程如下:

标签:log,large,Nginx,源码,内存,ngx,pool,size
From: https://www.cnblogs.com/tuilk/p/16993453.html

相关文章

  • jsp企业财务管理系统设计与实现(论文+PPT+源码)
    毕业设计(论文)任务书第1页毕业设计(论文)题目:企业财务管理系统设计与实现毕业设计(论文)要求及原始数据(资料):了解企业费用管理流程的关键点;2.深入研究企业费用流程中的各个环节;3.熟......
  • Babel源码解析(一):源码调试(上)
    Babel源码解析(一):源码调试(上)Versions开发环境、npm包版本信息:名称版本babelv7.20.6nodev16.16.0npmv8.11.0OSmacOS13.1(22C65)ForkBabel......
  • [C++]LeetCode 2502 设计内存分配器
    [C++]LeetCode2502.设计内存分配器题目描述Difficulty:中等RelatedTopics:设计,数组,哈希表,模拟给你一个整数n,表示下标从0开始的内存数组的大小。所有内存......
  • Linux中源码安装软件
    源码安装以安装nginx为例:(1)安装准备:yuminstallgccgcc-c++gcc-g77;(2)下载源码包直接官网下载然后通过tar-zxvf命令解压到相应路径。 (3)安装源码包。编译安装软......
  • Linux基础-查看cpu、内存和环境等信息
    使用Linux系统的过程中,我们经常需要查看系统、资源、网络、进程、用户等方面的信息,查看这些信息的常用命令值得了解和熟悉。1,系统信息查看常用命令如下:lsb_release-a......
  • Windows共享内存以及相关函数的使用
        程序与程序之间、进程与进程之间、线程与线程之间进行数据交互与共享的方法是决定一个程序运行效率与速度的关键。方法1:共享内存函数 CreateFileMappingHANDLE......
  • MobaXterm部署环境(Nginx)
    1.MobaXterm工具(1).连接远程服务器点击OK输入密码(密码不会显示)然后回车进入控制台(2).MobaXterm界面现在就可以通过Linux命令来操作控制台了2.前端部署(1).修改服......
  • 深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)
    摘要: 深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)上篇文章《深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)》介......
  • HashSet源码解读
    HashSet源码解读publicclassDemo5{finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node......
  • 【深入浅出Spring原理及实战】「源码原理实战」从底层角度去分析研究PropertySourcesP
    Spring提供配置解析功能主要有一下xml文件占位符解析和Java的属性@Value的占位符解析配置这两种场景进行分析和实现解析,如下面两种案例。xml文件的占位符解析配置<beanid="......