nginx内存池源码剖析

简介: 小块内存使用尾插法,大块内存使用头插法,大块内存申请挂载时如果前面三个节点都有对象,则不再向后遍历,直接创建一个节点插在头部如果一个小块内存超过四次都没有成功分配出内存,则认为他已经没有足够的内存了,则会抛弃(之后不在考虑在这个节点上分配内存)小块内存节点的内存不回收,但是大内存块的节点可以回收,提供回收方法内存对齐,多处内存对齐减少内存跨 cache 的数量。

在很多博客中都将nginx内存池的关系图的next指针指向的结构体画成ngx_pool_data_t类型(可能是为了方便读者理解而故意那样画的),但是通过源码可以看出,next实际指向实际上应该是ngx_pool_s类型,所以我特意找了一张符合源码结构的图

nginx关于内存池的源码路径是src/core/目录下ngx_palloc.h和ngx_palloc.cpp文件

在这里插入图片描述
在这里插入图片描述

相关结构体定义

// 大块内存
typedef struct ngx_pool_large_s  ngx_pool_large_t;
struct ngx_pool_large_s {
   
   
    ngx_pool_large_t     *next;         // 下一个大块内存池
    void                 *alloc;        // 实际分配内存
};

// 小块内存池
typedef struct {
   
   
    u_char               *last;         // 可分配内存起始地址
    u_char               *end;          // 可分配内存结束地址
    ngx_pool_t           *next;         // 指向内存管理结构
    ngx_uint_t            failed;       // 内存分配失败次数
} ngx_pool_data_t;

// 内存池管理结构
typedef struct ngx_pool_s            ngx_pool_t;
struct ngx_pool_s {
   
   
    ngx_pool_data_t       d;            // 小块内存池
    size_t                max;          // 小块内存最大的分配内存,评估大内存还是小块内存
    ngx_pool_t           *current;      // 当前开始分配的小块内存池
    ngx_chain_t          *chain;        // chain
    ngx_pool_large_t     *large;        // 大块内存
    ngx_pool_cleanup_t   *cleanup;      // 待清理资源
    ngx_log_t            *log;          // 日志对象
};

ngx_pool_t 是整个内存池的管理结构,这种结构对于个内存池对象来说可能存在多个,但是对于用户而言,第一下访问的始终是创建时返回的那个。多个 ngx_pool_t 通过 d.next 来进行连接,current 指向 当前开始分配的小块内存池,注意 ngx_pool_data_t 在内存池结构的起始处,可以进行类型转换访问到不同的成员。

nginx的每一个节点都是nginx-pool-t类型,,但是除了头结点外剩下的节点只使用了nginx-pool-data-t部分

内存对齐

#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))

创建内存池

在nginx中都以16字节为单位对齐

max 的最大值为 4095,当从内存池中申请的内存大小大于 max 时,不会从小块内存中进行分配。

#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_pool_t * ngx_create_pool(size_t size, ngx_log_t *log)
{
   
   
    ngx_pool_t  *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); //// 分配一块 size 大小的内存  内存空间16字节对齐
    if (p == NULL) {
   
   
        return NULL;
    }

    // 对pool中的数据项赋初始值
    p->d.last = (u_char *) p + sizeof(ngx_pool_t); //可用空间要减去这个头部 首sizeof(ngx_pool_t)便是pool的header信息,header信息中的各个字段用于管理整个pool
    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; //不能超过NGX_MAX_ALLOC_FROM_POOL// pool 中最大可用大小

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p; //指向空间最顶部头部
}

内存申请

在nginx源码中有三个可以申请内存的函数,他们有细微区别

  1. ngx_palloc 普通版本
  2. ngx_pnalloc 内存对齐版本
  3. ngx_pcalloc 会调用ngx_palloc然后初始化内存(清0)
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
   
   
    u_char      *m;
    ngx_pool_t  *p;

    // 判断 size 是否大于 pool 最大可使用内存大小
    if (size <= pool->max) {
   
   

        p = pool->current; //从current所在的pool数据节点开始往后遍历寻找那个节点可以分配size内存

        do {
   
   
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);// 将 m 对其到内存对齐地址
            if ((size_t) (p->d.end - m) >= size) {
   
   // 判断 pool 中剩余内存是否够用
                p->d.last = m + size;

                return m;
            }

            p = p->d.next;//如果当前内存不够,则在下一个内存快中分配空间

        } while (p);

        return ngx_palloc_block(pool, size);
    }

    return ngx_palloc_large(pool, size);
}




void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
   
   
    u_char      *m;
    ngx_pool_t  *p;

    if (size <= pool->max) {
   
   

        p = pool->current;

        do {
   
   
            m = p->d.last;

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

    return ngx_palloc_large(pool, size);
}




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

小块内存申请

当没有小块内存池满足申请时,会再申请一个小块内存池来满足分配,在设置完 last 和 end 两个内存指示器后,对从 current 开始的内存池成员 failed 进行自增操作,并且当这个内存池的 failed 分配次数大于 4 时,表面这个内存分配失败的次数太多,根据经验应该下一次分配可能还是失败,所以直接跳过这个内存池,移动 current。

//如果前面开辟的pool空间已经用完,则从新开辟空间ngx_pool_t
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
   
   
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    // 先前的整个 pool 的大小
    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;

    // 判断在当前 pool 分配内存的失败次数,即:不能复用当前 pool 的次数,
    // 如果大于 4 次,这放弃在此 pool 上再次尝试分配内存,以提高效率
    //如果失败次数大于4(不等于4),则更新current指针,放弃对老pool的内存进行再使用
    for (p = pool->current; p->d.next; p = p->d.next) {
   
   
        if (p->d.failed++ > 4) {
   
   
            pool->current = p->d.next;// 更新 current 指针, 每次从pool中分配内存的时候都是从curren开始遍历pool节点获取内存的
        }
    }

    // 让旧指针数据区的 next 指向新分配的 pool
    p->d.next = new;

    return m;
}

大块内存申请

大块内存是通过 large 连接的,并且都属于 ngx_create_pool 返回的 ngx_pool_t 结构。malloc 分配的内存由一个 ngx_pool_large_t 节点来挂载,而这个 ngx_pool_large_t 节点又是从小块内存池中分配的。

  • 为避免large链表长度过大导致在遍历寻找空闲挂载节点耗时过长,限制了遍历的节点为3,如果没有满足要求则直接分配
  • 头插法 插入至large链表中,新的节点后面也是最先被访问
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
   
   
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;

    /*
    // 重新申请一块大小为 size 的新内存
    // 注意:此处不使用 ngx_memalign 的原因是,新分配的内存较大,对其也没太大必要
    //  而且后面提供了 ngx_pmemalign 函数,专门用户分配对齐了的内存
    */
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
   
   
        return NULL;
    }

    n = 0;

    // 查找largt链表上空余的large 指针
    for (large = pool->large; large; large = large->next) {
   
   
        if (large->alloc == NULL) {
   
    //就用这个没用的large
            large->alloc = p;
            return p;
        }

        /*
         // 如果当前 large 后串的 large 内存块数目大于 3 (不等于3),
        // 则直接去下一步分配新内存,不再查找了
        */
        if (n++ > 3) {
   
   //也就是说如果pool->large头后面连续4个large的alloc指针都被用了,则重新申请一个新的pool_larg并放到pool->large头部
            break; //????? 感觉没啥用,因为后面每次alloc的large对应的alloc都是赋值了的
        }
    }

    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
    if (large == NULL) {
   
   
        ngx_free(p);
        return NULL;
    }

    // 将新分配的 large 串到链表后面
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

大块内存释放

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

销毁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) {
   
    //cleanup在ngx_pool_cleanup_add赋值
        if (c->handler) {
   
   
            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) {
   
   
            ngx_free(l->alloc);
        }
    }

#if (NGX_DEBUG)

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

    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 (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
   
   
        ngx_free(p);

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

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

总结

  1. 小块内存使用尾插法,大块内存使用头插法,
  2. 大块内存申请挂载时如果前面三个节点都有对象,则不再向后遍历,直接创建一个节点插在头部
  3. 如果一个小块内存超过四次都没有成功分配出内存,则认为他已经没有足够的内存了,则会抛弃(之后不在考虑在这个节点上分配内存)
  4. 小块内存节点的内存不回收,但是大内存块的节点可以回收,提供回收方法
  5. 内存对齐,多处内存对齐减少内存跨 cache 的数量
  6. nginx内存池注重时间(空间换时间),内存的利用度并不高,与c++ sgi stl的二级内存池有很大的差异,这是由于nginx的业务场景所决定的
目录
相关文章
|
6月前
|
缓存 监控 Java
ThreadLocal 源码解析get(),set(), remove()用不好容易内存泄漏
ThreadLocal 源码解析get(),set(), remove()用不好容易内存泄漏
75 1
|
6月前
|
应用服务中间件 Linux 网络安全
CentOS 7.4源码编译nginx1.12 并且隐藏nginx的版本
CentOS 7.4源码编译nginx1.12 并且隐藏nginx的版本
119 0
|
2月前
|
负载均衡 网络协议 应用服务中间件
web群集--rocky9.2源码部署nginx1.24的详细过程
Nginx 是一款由 Igor Sysoev 开发的开源高性能 HTTP 服务器和反向代理服务器,自 2004 年发布以来,以其高效、稳定和灵活的特点迅速成为许多网站和应用的首选。本文详细介绍了 Nginx 的核心概念、工作原理及常见使用场景,涵盖高并发处理、反向代理、负载均衡、低内存占用等特点,并提供了安装配置教程,适合开发者参考学习。
|
3月前
|
存储 缓存 编译器
Linux源码阅读笔记06-RCU机制和内存优化屏障
Linux源码阅读笔记06-RCU机制和内存优化屏障
|
3月前
|
应用服务中间件 Linux nginx
在CentOS上使用源码包安装Nginx、以及手动启动Nginx的步骤过程
这篇文章介绍了在CentOS系统上使用Nginx源码包进行安装和配置的详细步骤,包括源码包的获取、解压、配置、编译、安装、启动验证以及注意事项。
320 0
在CentOS上使用源码包安装Nginx、以及手动启动Nginx的步骤过程
|
6月前
|
存储 NoSQL 算法
Redis源码、面试指南(2)内存编码数据结构(下)
Redis源码、面试指南(2)内存编码数据结构
58 4
|
6月前
|
存储 NoSQL API
Redis源码、面试指南(2)内存编码数据结构(上)
Redis源码、面试指南(2)内存编码数据结构
64 0
|
6月前
|
消息中间件 Java 关系型数据库
JAVA云HIS医院管理系统源码、基于Angular+Nginx+ Java+Spring,SpringBoot+ MySQL + MyCat
JAVA云HIS医院管理系统 常规模版包括门诊管理、住院管理、药房管理、药库管理、院长查询、电子处方、物资管理、媒体管理等,为医院管理提供更有力的保障。 HIS系统以财务信息、病人信息和物资信息为主线,通过对信息的收集、存储、传递、统计、分析、综合查询、报表输出和信息共享,及时为医院领导及各部门管理人员提供全面、准确的各种数据。
122 1
|
6月前
|
前端开发 应用服务中间件 网络安全
nginx配置SSL证书配置https访问网站 超详细(附加配置源码+图文配置教程)
nginx配置SSL证书配置https访问网站 超详细(附加配置源码+图文配置教程)
552 0
|
6月前
|
应用服务中间件 nginx
Nginx源码阅读:共享内存ngx_shm_t和它的组织方式ngx_shm_zone_t、ngx_list_t
Nginx源码阅读:共享内存ngx_shm_t和它的组织方式ngx_shm_zone_t、ngx_list_t
72 0