• 一文看懂Ngnix内存池源码 附带详细讲解,清晰结构图



    Nginx的内存池模块定义在ngx_palloc.h和ngx_palloc.c文件中,其中ngx_palloc.h文件中主要定义了相关的结构体,ngx_palloc.c文件中则是主要函数的实现。
    下面附上源码,不看的可以跳过直接看讲解。🛬

    总体介绍🛬

    可以先看一下大概过程:
    在这里插入图片描述

    首先Ngnix内存池分为主内存池和其他内存池,主内存用来管理其他内存池,和自身初始具有的能够分配的内存。
    但主内存池和其他内存池在结构定义上是一样的,都是下面代码块中的 ngx_pool_s且具有别名 ngx_pool_t,只不过其他内存池再被创建的时候,会被分割,将除去成员 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;
    };
    typedef struct ngx_pool_s ngx_pool_t;
    //其中ngx_pool_data_t结构如下
    typedef struct {
        u_char               *last;		//当前内存池分配到的末尾地址,即下一次分配开始地址
        u_char               *end;		//内存池结束位置
        ngx_pool_t           *next;		//下一块内存
        ngx_uint_t            failed;	//当前块内存分配失败次数
    } ngx_pool_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image.png
    这里是各结构体中的关系:
    image.png

    各点分析🛬

    内存分配🛬

    是一个分流函数,按一个标准分割大块内存分配和小块内存分配
    宏定义可忽略

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

    小块内存分配🛬

    nginx对于小块内存分配效率极为高效,通过对主内存池的current指向的当前内存池的数据头中的起始指针进行操作,进而进行简单的指针偏移就可以实现

    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) {
                //将last指针移向新位置
                p->d.last = m + size;
    
                return m;
            }
    		
            //若当前内存池的剩余内存小于所需内存,则到下一个内存块中寻找
            p = p->d.next;
    
        } while (p);
    
        //若所有内存池都没有合适的内存空间,则开辟新的内存池。
        return ngx_palloc_block(pool, size);
    }
    
    • 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

    大块内存分配🛬

    其对于大块内存的分配,先分配一个小块内存对大块内存进行封装,封装结构如下,再通过C语言APImalloc分配用户想要的内存,将得到的返回地址用来初始化该封装结构体,最后采用头插法,插到主内存池的large链表上,方便清理。

    typedef struct ngx_pool_large_s ngx_pool_large_t;
    // 大块内存类型定义
    struct ngx_pool_large_s {
        ngx_pool_large_t *next; // 下一个大块内存
        void *alloc; 			// 记录分配的大块内存的起始地址
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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);
        if (p == NULL) {
            return NULL;
        }
     
        n = 0;
        // 查找到一个空的large区,如果有,则将刚才分配的空间交由它管理  
        for (large = pool->large; large; large = large->next) {//找到一个可以利用的,便只需要改变alloc指针的指向,然后返回
            if (large->alloc == NULL) {
                large->alloc = p;
                return p;
            }
     		
            //查找3次都未找到空的large结构体则直接跳出循环直接创建
            if (n++ > 3) {
                break;
            }
        }
        //若重新创建新的large结构体,便要进行一些插入操作。
        large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
        if (large == NULL) {
            ngx_free(p);
            return NULL;
        }
     
        large->alloc = p;
        large->next = pool->large;
        pool->large = large;
     
        return p;
    }
    
    • 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

    内存回收🛬

    小块内存回收🛬

    通过指针偏移来分配内存的机制决定了其不能够对小块内存进行回收,因为用last和end来记录未被分配的内存,采用2个指针不可能实现内存的随机回收。但其实这正是Ngnix的内存池针对场景的特性:
    Ngnix 的本质是一个HTTP服务器,是一个短链接的服务器,间歇性的完成请求。客户端(浏览器)发起一个request请求,到达nginx服务器以后,处理完成,nginx给客户端返回一个response响应,http服务器就主动断开tcp连接(http 1.1 keep-avlie 60s),http服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间,否则60s之内没有客户端发来的响应,nginx就主动断开连接。
    此时,服务完成,内存池分配出去的内存便可以进行重置。将主内存池中的数据头中的指针复位,进而重置内存池,达到回收小块内存的效果。至此可以看出Ngnix是没有对小块内存进行单独回收的操作,只能进行重置。

    大块内存回收🛬

    遍历原有的大块内存头链表,找到对应大块内存结构体,释放结构体中alloc指向相应的内存(就是申请时malloc得到的),但是,该结构体保留在链表上。
    且对于之前的大块内存分配,并没有当判断其为大块内存申请时就为其创建内存头保存信息,而是遍历原有的大块内存头链表,但是只遍历前三个,如果有空闲的直接利用即可,遍历前三个的原因是链表遍历效率不高,防止额外花销。(找不到就创建一个呗,没必要一直找)

    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);
                //实际上就是free
                ngx_free(l->alloc);
                l->alloc = NULL;
    
                return NGX_OK;
            }
        }
    
        return NGX_DECLINED;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    新内存池的开辟🛬

    若内存池中无可用的内存,便要进行新的内存池开辟。
    且主内存池和其他内存池的开辟函数不一样,但大同小异,对此讲其他内存池的开辟:
    在开辟新内存池方面,新内存池的大小和原来第一次创建的内存大小一样,但是只包含ngx_pool_data_t小块内存数据头信息,而不再包含 max、current、large等信息,因为控制信息只在主内存池上记录即可。
    新开辟的内存池将插入主内存池的next链表的尾部。
    每开辟一个新的内存池,说明在前面的内存池中都申请失败,于是在插入遍历过程中,把他们的failed字段+1,当某个内存池failed次数大于4时,便认为其上内存几乎分配完毕或内存碎片过小无法利用,令主内存池的current指向下一块内存池的地址,即 主内存池next字段
    主内存池开辟源码:

    ngx_pool_t *
    ngx_create_pool(size_t size, ngx_log_t *log)
    {
        //为当前内存池分配第一块内存
        ngx_pool_t  *p;		
    	
        //调用nginx的字节对齐内存分配函数为p分配size大小的内存块
        p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
        if (p == NULL) {
            return NULL;
        }
    	
        //last跨过内存块中数据头ngx_pool_t结构体,指向紧接着的可分配内存的起始位置
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        //end指向当前size大小内存块的末尾
        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;
    }
    
    • 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

    其他内存池开辟源码:(一般在分配失败的时候会调用)

    static void *
    ngx_palloc_block(ngx_pool_t *pool, size_t size)
    {
        u_char      *m;
        size_t       psize;
        ngx_pool_t  *p, *new;
        //获得主内存池的大小,end字段指向的时内存池末端,pool是指向主内存池的指针,即起始地址
        psize = (size_t) (pool->d.end - (u_char *) pool);
        //分配一块对齐过的内存,细节不用管,就是调用系统API
        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;
        //插入到主内存池的next链表
        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;
    }
    
    
    • 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

    内存池的重置🛬

    1. 重置内存池时,先遍历主内存池的 max链表把大块内存释放掉,因为大块内存的内存头信息还在小块内存池中保留着,如果先释放小块内存,那么大块内存的地址信息丢失就会造成内存泄露。
    2. 再重置每个内存池数据头中的可分配内存起始地址(last指针)。
    3. 重置主内存池中的一些控制信息
    void
    ngx_reset_pool(ngx_pool_t *pool)
    {
        ngx_pool_t        *p;
        ngx_pool_large_t  *l;
    	//原函数是都按主内存池进行处理,存在内存浪费,故将原函数的一个循环按条件分为2个循环
        
        //释放大块内存
        for (l = pool->large; l; l = l->next) {
            if (l->alloc) {
                ngx_free(l->alloc);
            }
        }
    	//重置小块内存,并不调用free将内存交还给系统,只是指针的复位操作。
        for (p = pool->next; 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
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    释放内存回调函数🛬

    Nginx内存池支持用户自定义回调函数来在释放内存时对外部资源进行自定义的释放操作。
    因为假设你将分配出去的内存给一个结构体,若结构体中具有指针成员变量指向另一块其他地方申请的内存,便需要用户自己手动释放,用户可通过Ngnix注册一个回调函数,在内存池销毁的时候,依次执行。
    ngx_pool_cleanup_t是回调函数结构体,它在内存池中一链表形式报错,在整个内存池销毁时,将循环调用该结构体中的回调函数来对资源进行释放操作。

    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; // 指向下一个清理操作
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    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;
        }
        //有参数的话,初始化结构体中的data,后续回调时用到
        if (size) {
            c->data = ngx_palloc(p, size);
            if (c->data == NULL) {
                return NULL;
            }
    
        } else {
            c->data = NULL;
        }
        //handler置空,需要用户自己通过返回值添加,因为返回值便是这个结构体的指针。
        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;
    }
    
    • 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

    内存池的销毁🛬

    内存池的销毁操作在ngx_destroy_pool函数中,该函数循环调用ngx_pool_cleanup_s中的handle函数释放资源,并释放大块内存和小块内存链表的内存块

    ngx_destroy_pool(ngx_pool_t *pool)
    {
        ngx_pool_t          *p, *n;
        ngx_pool_large_t    *l;
        ngx_pool_cleanup_t  *c;
    
        //循环调用handle数据清理函数
        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);
            }
        }
    
        //释放大块内存
        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;
            }
        }
    }
    
    • 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
  • 相关阅读:
    虚拟机安装CentOS 7
    RFSoC应用笔记 - RF数据转换器 -22- API使用指南之配置DAC相关工作状态和中断相关函数使用
    如何实现点云体素化及由密集特征得到二维伪装
    C++ 多线程使用
    (24)语义分割--BiSeNetV1 和 BiSeNetV2
    swoole process 消息通信
    TiKV 源码阅读三部曲(三)写流程
    linux之线程
    【ubuntu】修改系统及硬件时间
    代码随想录笔记_动态规划_474一和零
  • 原文地址:https://blog.csdn.net/q2453303961/article/details/125503987