zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

nginx内存池的源码剖析

2023-09-14 09:15:35 时间

2019年8月18日18:47:51
2019年8月18日23:07:15

百度百科:

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,第一个公开版本0.1.0发布于2004年10月4日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。2011年6月1日,nginx 1.0.4发布。Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

学习方向:

此时仅仅是学习ngix的内存池模块,相较于SGI STL的二级空间配置器 的内存池的设计与实现的区别和相同点。以及两种内存池 的应用场景 和 设计实现的区别。

SGI STL的二级空间配置器 的内存池主要是给 C++的容器底层进行内存管理的使用。
类似于SGI STL的二级空间配置器 的内存池 在内存管理的时候,小于等于128字节 和 大于128字节的内存开辟相分开了(因为小块内存的频繁开辟malloc 释放free:影响性能、产生内存碎片、大块连续内存的缺少),ngix内存池也把 大块和小块内存的开辟释放也分开了。

ngix相关的类型定义如下:

//从内存池里面取得的最大的内存   4096字节。范围就是0 -- 4095字节
/*
 * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.
 * On Windows NT it decreases a number of locked pages in a kernel.
 */
#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)

对于ngix(给整个HTTP服务器所有的模块提供这么一个内存池的)而言:小块内存 和 大块内存的界限就是一个页面(4K:32位系统的内存管理的一个物理页面大小)。

最小的大小 那个函数:ngx_align函数 如下:

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

上面的这个是不是 和SGI STL的二级空间配置器的 s_round_up函数(将给定字节 上调到最邻近的8的倍数)?当然这里的意思就是:所需要的内存的开辟 调整到 最邻近的 a 的倍数。也即:参数1 d:(sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)) ;参数2 :16 。将参数1 上调到16的倍数。

1. ngix 内存池涉及到的内存管理 函数的声明:

// 创建ngix内存池,可以指定内存池的大小
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);

//内存分配函数,支持内存初始化0  清0
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

//
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

//内存释放(大块内存)
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);
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);

2. ngix内存池的重要类型定义

// 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_data_t 就是内存池的头信息。当创建一个指定size大小的内存池之后,在这个内存的头部:放的是一个ngx_pool_s 类型的变量。这个ngx_pool_s 类型里面的第一个:ngx_pool_data_t 就是内存池的头信息,里面就包含了如下的类型变量:*last *end *next failed 。

此外的ngx_pool_t 就是struct ngx_pool_s的一个类型typedef 。

typedef struct ngx_pool_s            ngx_pool_t;

因为在C语言当中:结构体类型在使用的时候,前面的struct 是不可以省略的,否则的话 这里只能够:struct ngx_pool_s *current这样,就很麻烦。

ngx_pool_cleanup_t *cleanup;是内存池数据的清理操作:用户可以设置回调接口。在内存池释放之前,对内存池上数据 进行释放动作。(这个和C++ 的析构函数,在对象内存被释放掉之前,执行对象的析构函数把对象占有的外部资源 释放掉。)相当于用户自己提供的一个Handler,在内存池内存释放之前,对内存池上数据 进行释放动作。
在这里插入图片描述

typedef struct ngx_pool_s       ngx_pool_t;
// 小块内存数据头信息
typedef struct {
  u_char        *last; // 可分配内存开始位置
  u_char        *end; // 可分配内存末尾位置
  ngx_pool_t      *next; // 保存下一个内存池的地址
  ngx_uint_t       failed; // 记录当前内存池分配失败的次数
} ngx_pool_data_t;//内存池的头信息
typedef struct ngx_pool_large_s  ngx_pool_large_t;
// 大块内存类型定义
struct ngx_pool_large_s {
  ngx_pool_large_t   *next; // 下一个大块内存
  void         *alloc; // 记录分配的大块内存的起始地址
};
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; // 指向下一个清理操作
};

3. ngix 内存池涉及到的内存管理 函数:

3.1 创建内存池的函数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_pool_t * 也就是ngx_pool_s 结构体类型的指针。

接下来:

//NGX_POOL_ALIGNMENT   16   内存池 内存分配字节对齐数
 p = ngx_memalign(16, size, log);//根据用户指定的大小来开辟内存,可以根据不同系统平台
 //定义的宏,调用不同系统平台的API

ngx_memalign函数的定义如下:

#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(size, log),与那个对齐数字alignment 没有关系,也即:不做内存对齐。其底层就是调用malloc
ngx_alloc(size, log)函数如下:

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;//失败 就返回NULL
}

而上面的两个有 宏对应的两个函数(定义了相应的宏,调用的就是相应的系统API)如下:

#if (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;
}

#elif (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;
}

接着ngx_create_pool函数,若是分配失败 则返回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 就是1024 字节(内存池的大小,但是并不是全部都可以放数据)。其最上面就是一个内存池的头信息。即:ngx_pool_s 类型。指针p指向的是这个内存池的最开始地址。
如上:就是初始化 数据头变量的一些信息(头信息的第一块:内存池的数据信息 4个)。内存头信息就是为了 记录内存池的一些 状态信息的。
last 指向的是 开辟的内存池(除过ngx_pool_s 类型 内存头信息)以外的可以使用的内存 起始地址。
end 指向的是 这个内存池的末尾地址(也可以说:可以使用的内存 末尾地址)。

	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;//日志模块 根据ngx_create_pool函数 传入的不同模块的log日志对象

size 计算出来就是内存池可以使用的实际大小。
max 可以的size小于 4095,则取size。若是不小于 ,想开辟的内存池大于一个页面,则取一个页面大小(4095)。这是为了维护 Nginx 小块内存的特点(总之 不会超过一个页面大小)。max存的就是当前小块内存的内存分配的最大值,也即:当前内存池可以分配的字节数,小块内存的上限。
current 指针 指向内存的起始地址。小块内存 可以分配多块内存池的,current 指向的是当前块内存池,以后将来在分配小块内存的时候:直接从当前块开始分配内存 即可。

最后返回这个新创建的内存池的起始地址。
在这里插入图片描述
小块内存就是在 上面的空白区分配的,而大块内存分配 的入口指针就是large 。

3.2 小块内存分配

每个内存池都有一个 内存头,(注:在第一个内存池上的内存块头信息是比较详细的)。
申请内存在Nginx里面主要有3个函数:

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


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


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函数第三个参数为1的:考虑内存对齐,而第三个参数为0的:不考虑内存对齐。第三个函数 调用的还是ngx_palloc,开辟内存成功之后,会给这个内存全部进行清0操作的。

所以这里统一 详谈ngx_pnalloc函数:如何从Nginx内存池里面 申请内存?

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);//进入 大块内存分配
}

size是传递的大小(申请内存的字节大小),pool 是内存池的起始的地址。pool指针指向 内存块入口块的起始地址。内存对齐的好处:减少CPU I/O次数,通过CPU访问内存的效率。

我们先讨论 进入 小块内存分配函数 ngx_palloc_small(pool, size, 1)如下:

static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)//1 :内存对齐
{
    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);

	//所有的小块内存都检测了一遍,都不足够 size大小需求
    return ngx_palloc_block(pool, size);
}

p = pool->current;表示:current指向哪个内存池,就从这个内存池开始内存分配。此时p就指向了 这块内存的起始地址了。

接下来的 do while循环m指针先指向可分配内存的起始地址考虑内存对齐,依据具体的平台(32位4字节,64位 8字节)把m指针的地址 调整为与平台相关的NGX_ALIGNMENT(4 或者 8)的整数倍的地址上去。

接下来:如果内存池空闲的内存空间、可以分配的内存是足够的,那么就可以进行分配的。具体分配内存的做法就是:p->d.last = m + size; 将last指针 偏移上size字节。return m ;把刚分配出去的内存的起始地址 返还。(这种情况就是:在当前的内存块 直接就可以分配成功的

如果内存池空闲的内存空间、可以分配的内存是不够分配的。或者说 此时随着分配,last指针一直在进行偏移,且可以分配的内存空间 不断减少。此刻:分配内存 不够分size个字节了。也只能 去 p = p->d.next;。但是现在仅仅只有这一个内存块,(最开始的 next 初始化空),所以p是空的(只有这一个内存块,没有下一个内存块)。退出循环,内存没有分配成功。所以就进入了下面的函数:ngx_palloc_block(pool, size)。表示:这个内存块不够用,再继续分配。

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)//pool是内存块的起始地址
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    psize = (size_t) (pool->d.end - (u_char *) pool);
	
	//第一件事情:
	//又开辟一个上面psize那么大的  内存块,然后由m指向 起始地址
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }
	//初始化一下 内存块的一些信息
    new = (ngx_pool_t *) m;//new 也指向了这块 内存的起始地址

    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);//这两句 m指针指向了 的对齐地址上
    new->d.last = m + size;//last 偏移,把内存分配出去了
	
	//第二件事情:遍历之前的内存块 这一次的内存分配全失败了,所有内存块failed +1
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;//current记录一下 下一个内存块的起始地址
        }
    }

    p->d.next = new;//新生成的 内存块接到之前的内存块链表当中

    return m;//完成 小块内存的分配
}

从上面代码 可以知道 从第二个内存块开始,内存头里面仅仅只需要存储一下ngx_pool_data_t类型变量即可(管理当前内存块 内存分配这4个必要的变量即可)。接下来 通过last指针的偏离:new->d.last = m + size;(小块内存的分配 就是通过last指针进行的偏移)。此时m就指向的是 给外面分配的size大小内存的起始地址。如下:在这里插入图片描述
每个内存块都有 last 和 end,指向着空闲内存的 起始和终止地址。在这里插入图片描述
下面的for循环:pool指向的这里的 第一个内存块,current还指向的是 第一个内存块。但是 此时进不去for循环:d.next 是空的。 进不去for循环,在p->d.next = new;上 把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;

在这里插入图片描述
接着:假如 后面继续分配内存,大于第一个内存块的剩余字节大小。分配的时候,依旧是从 current指针指向的内存块开始分配的,发现不够。然后p = p->d.next; 跳到下一个内存块上。p指向了第二个 内存块。接着在 第二个内存块上分配,够分配 成功(last指针进行偏移),直接return。如下:在这里插入图片描述
如果连续进行内存 申请,而申请的内存又比较大。每次去遍历现有的内存块(小块内存池)的时候,发现剩余的空闲内存块都不足够 size大小。只能去 调用ngx_palloc_block(pool, size);方法,重新去开辟一段 小块内存的内存池。在开辟成功之后,先初始化一些 内存池信息。然后在下面的for循环里面,把之前的这几个 内存池都遍历一下,如果p->d.failed++(分配很多次都不成功),然后大于4 就把pool指针指向的current指针进行移动:移动到下一个内存块。在这里插入图片描述
这块内存块 分配多次都不成功,说明内存块的剩余空闲空间比较小了。把current指针进行移动:因为每次内存开辟 都是在current指针指向的内存块上进行开辟的。(上面被移走的那个内存块就再也不会去使用 开辟了)。

于是总结:在之后的开辟中:下面的内存块 分配也经常失败,几个内存块都分配不到内存了。则又需要 新增加一个内存块,在这个新的内存块把size内存分配出去之后,for循环里面 把从current指向的内存块,(所有的内存块)开始逐一进行遍历 d.failed++,这个failed此时超过4(这个内存块无法分配出有效的内存了), 则current指针是要一直向后移动的,最后指向第一个failed小于4的 内存块。下次内存分配就从这个 内存块开始分配了。

(这个failed就是:记录内存分配失败的次数)

至此为止 小块内存分配函数ngx_palloc_small 到此为止。

3.3 大块内存分配

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

如果这里 size大于pool->max(不超过一个页面大小4095),则 进入ngx_palloc_large(pool, size)函数。毕竟:max=(size < 4095) ? size : 4095;
先看一下:

typedef struct ngx_pool_large_s  ngx_pool_large_t;
//这个结构体是记录大块内存的信息的  内存头,也是直接在 小块内存块上分配了
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;//下一个大块内存的地址
    void                 *alloc;//保存大块内存的内存起始地址
};

ngx_palloc_large(pool, size)函数如下:

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函数来开辟size的大块内存
    if (p == NULL) {
        return NULL;
    }

    n = 0;
	//在上面malloc大块内存之后  进入for循环
    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;
}

在这里插入图片描述
如上图所示:这个图是学习Nginx内存池 大块内存分配很重要的一张图。最右边的就是内存池最开始开辟的内存块,里面有这个内存块的数据信息(4个重要的变量)、和下面内存池管理的一些信息。从第二个内存块开始 里面就只放内存块的数据信息(4个重要的变量),。

小块内存分配过程:current指向的是 第一个可以分配内存的小块内存块,如果在小块内存池分配失败 会重新创建一个 小块内存:然后在这个小块内存块上 先分配size内存,然后for循环给前面的内存块的failed ++,谁的值超过了 4,则current就会指向下一个内存块(以后分配就不会从前面这几个内存块上开始分配了)。

但是这张图的几个缺陷如下:

  1. 中间的两个小块内存,图示上应该和 最右边的第一个小块内存是一致大小的。因为每一次都是计算小块内存psize=end - pool在这里插入图片描述
  2. 这个图不准确的第二点:大块内存的内存头信息(ngx_pool_large_t)画在了外面。而是应该直接在这个pool指向的这些小块内存块上 分配这么一个ngx_pool_large_t类型变量的大小。

pool指向当前内存块的里面的large指针:large指向的是
在上面malloc大块内存之后,进入for循环里面,最开始的时候 large也是一个空指针(这个large也是pool指向当前内存块的里面的large指针),进不去for循环,则执行如下:

large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);

进入 小块内存池分配的入口函数(在这个小块内存分配函数中:分配大块内存 内存头这个变量、结构体类型信息),直接在这个pool指向的小块内存块上 分配这么一个ngx_pool_large_t类型变量(记录大块内存的内存头)的大小。因为小块内存池的分配效率十分之高:借助于last指针的偏移就完成了。 做这个操作的目的就是:让开辟的大块内存 给记录下来。
接下来:

	if (large == NULL) {//记录大块内存的内存头  这个变量分配失败
        ngx_free(p);//就没有办法记录 这个大块内存,所以开辟的大块内存只能释放
        return NULL;
    }

    large->alloc = p;//在这个大块内存的内存头里面alloc 记录大块内存地址 
    large->next = pool->large;//next域  指向了小块内存块里面的large为入口的指
    pool->large = large;//向的大块链,把这个大块内存 头插法给插入到链里面

    return p;

在这里插入图片描述

//在上面malloc大块内存之后  进入for循环
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) {
            break;
        }
    }

在这个代码里:刚才pool 指向的内存块的large是空,所以我们把这个for循环给忽略了。现在加入我们已经有了一个 大块内存链。进入for循环:(遍历这个large指向的链表)
large 指向的allo域为空,说明:找到了一个可以挂在这个大块内存的地方了,下一步把 大块内存的起始地址 p给挂上(把 大块内存的起始地址 p写到 内存头的alloc域里面),OK return。也即:并非每次开辟大块内存 都对应的在小块内存块里面 先给 开辟这么一个大块内存的内存头信息,而是 先从这个大块内存的链上遍历,找到一个内存头的 空的alloc域(有人用过之后,给释放的这种),直接挂上去就OK。 下面的if (n++ > 3)遍历超过3次 ,那么也就是说 从large开始遍历,找这个空的alloc域,找了3个都没有找到,就停止找了。在下面 重新在小块内存中给开辟这么一个新的大块内存的内存头吧(重新开辟效率又高:last指针一做偏移就行。继续寻找浪费时间:这个大块内存的链表有可能很长的)。

那这个小块内存块里面的大块内存的内存头信息的alloc域 什么时候是空的呢?

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)//释放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;//就在这里 先free 然后置为空

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

这个ngx_pfree函数是来释放Nginx的大块内存的,小块内存人家是不释放的。

至此,大块内存的分配源码解剖到此为止!!!
所有说:这个Nginx内存池 处处都在考虑效率的问题!!!!这老毛子太厉害,佩服!

3.4 Nginx的内存池 重置函数和小块内存回收方法

上面也说了 小块内存的开辟,并没有做释放。原因是什么?已经Nginx提供了什么样子的解决方案?

因为鉴于 小块内存的分配是通过last指针的偏移完成的(这样做:效率很高,但是回收起来 就不太可能了),根本就没有办法完成小块内存的释放。如下:假如如下 已使用的部分,被分配成了3块,当1 和 3还在使用的时候,怎么可能把2号小块内存给归还给 这个内存块的呢?
在这里插入图片描述
也就是说:仅仅依靠两个指针 last和end来标识 这个空闲空间,是无法完成 2号这块内存的回收操作。

所以说:小块内存无法回收,那随着分配 内存池空闲空间越来越少,怎么解决?以及Nginx内存池设置这么一个 小块内存申请的应用场景 或者 意义作用何在?

//下面是 Nginx的所有 大块 小块的内存重置函数
void
ngx_reset_pool(ngx_pool_t *pool)//内存池 重置函数
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;
	
	//遍历大块内存(从large这个入口的大块内存链表)
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);//alloc下面的大块内存全部释放掉
        }
    }
	
	//p从第一个内存块 pool开始。通过d.next把所有的内存块给连起来
    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;
}

但是这个第二个for循环 明显有错误。(我也不敢相信 源码竟然出现这种问题。确实当我认真分析完这个代码 之后,确实感觉有点欠缺考虑)
问题如下:在之前的小块内存分配上,我们已经说过了。只有第一个内存块上:除了 各个内存块必有的ngx_pool_data_t类型变量(内存头信息)之外,它还有max 、current、chain、large、cleanup、log等(内存池管理的这些数据域:全局的小块 大块开辟的统一内存管理数据域)。但是其他的内存块是没有后面的了,只有一个内存头ngx_pool_data_t信息。
在这里插入图片描述
所以说这里for循环统一处理:

p->d.last = (u_char *) p + sizeof(ngx_pool_t);  导致其他内存块会平白浪费一些空间

这样处理 第一个内存块是没有问题的。

所以应该如下处理:在这里插入图片描述
在此之间,failed 应该重置为0 。
接下来的:处理内存池的其他管理信息

	pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;

current又重新指向了 第一个内存块,下次小块内存 内存开辟的时候 还是从这个内存块开始分配。大块内存随着全部free 此时在内存块上 开辟的大块内存的内存头(ngx_pool_large_t),也是全部都无效了,所以就可以直接把large(入口指针)置为空。

应用场景:
在这里插入图片描述

  1. 长连接的服务器:不管客户端有没有 又发过来请求,它和客户端之间的连接是不能断开的。资源就不可以去释放了,这样的应用场景 适合用之前的 SGI STL的二级空间配置器内存池。无论是大块 还是小块,从效率上而言:SGI STL的二级空间配置器内存池的小块申请 是绝对不如Nginx内存池的小块内存申请(last偏移即可)。但是SGI STL的二级空间配置器内存池的小块 和 大块是可以使用在任何的场景之下的,因为它提供了小块 和 大块 内存开辟和释放的。
  2. HTTP服务器(Nginx)是短连接服务器,在用户发过来请求的时候,为了处理这个请求涉及的模块。可以为这些需要内存的模块 创建相应的内存池,在内存池上进行内存分配。若是给这个client响应之后,服务器就可以断开和这个client的连接了。此时,处理刚才请求涉及的所有的资源就都可以回收了。于是此时,Nginx可以调用ngx_reset_pool函数 进行重置内存池了,等待下一次client的请求:继续使用这个Nginx内存池了。
  3. 基于短连接的服务场景,请求处理完了以后。(此次连接的内存上所有的数据都是无效的了,服务器和client没有任何关系了)这次请求,所有的资源就都可以进行回收了。此时就是调用ngx_reset_pool函数 进行重置内存池。
  4. Nginx内存池因此就仅仅非常适合于 http服务器了,基于短连接的给client提供服务的应用场景。

3.5 Nginx内存池外部资源释放和内存池销毁

再来看一下 nginx内存池的主结构体类。其中有一个cleanup变量:是内存池的清理函数handler(用户自己提供的)的入口指针。

	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_cleanup_t 类型如下:

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

整个过程:我们首先使用ngx_palloc(ngx_pool_t *pool, size_t size)这个函数 来在小块内存块上开辟size字节大小的小块内存,然后比较

if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);//小块内存分配
    }

    return ngx_palloc_large(pool, size);//大块内存分配

在ngx_palloc_large(pool, size);里:首先使用malloc开辟一块大块内存p指针指向(以后给用户返回的就是p),然后在 内存块的内存池里面开辟一个 大块内存的内存头信息:记录一下这个大块内存的相关信息(alloc 指向大块内存地址、然后利用next域 头插入这个pool->large指向的大块内存链上)。

假如:我们此刻使用这个Nginx内存池:在这里插入图片描述
此时 创建一个内存池 总的内存块大小512字节,但是可用空闲的内存池大小肯定小于512字节(因为要减去 内存头的字节大小,然后才给pool->max 指向了。)但是此刻 ngx_palloc(512),超过max 肯定是大块内存分配。假如此时,问题描述 如下:
在这里插入图片描述
如上:这个 sizeof(Data)或者sizeof(stData)=512字节,它里面有一个char *指针p 又指向了一块新开辟的堆内存外部资源。相当于新开辟的大块内存为512字节,由(stData类型指针)pData指向,但是大块内存里面的成员变量p又指向了堆上的12字节大小的外部资源(里面放的 hello world)。在这里插入图片描述
(这个和 OOP里面的对象里面的成员变量占据了 外部资源是很相似的,但是对象出了作用域 调用析构函数 是可以释放这个外部资源的)。但是对于Nginx内存池而言,其大块内存的释放是通过调用ngx_reset_pool函数,在ngx_reset_pool函数里面调用ngx_free(l->alloc);//alloc下面的大块内存全部释放掉。其底层做的就是调用free。但是这只能做到大块内存释放掉了(相当于只是把对象本身占用的资源释放掉了),其中成员变量占用的外部资源就 被泄露了。

所以说这样的问题怎么解决呢?(可以参考OOP的析构函数)在释放这个大块内存之前,必须要先执行一个(对象里面自动调用的析构函数)一样的函数去先把 这个外部资源给释放掉。

注:但是这个函数用户可以预先设置好,但是系统却不会主动调用。所以还得用户把这个函数设置成一个 回调函数(具体就是通过 函数指针实现的),把大块内存其中成员变量占用的外部资源释放了,然后再释放这个大块内存本身的内存。

这也就是上面内存池的主结构体类里面的*cleanup;// 内存池的清理函数handler的入口指针 的作用了: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;
};

ngx_pool_cleanup_s 类型就是:类似于大块内存的头信息结构一样,但是这个结构体就是把 释放外部资源的回调 给组合在了一起。

第一个成员变量Handler:函数指针类型 保存用户自己预先设置的资源释放函数(回调函数)。
next指针:链表的下一个域。在释放资源的时候,有很多释放资源的动作,并非只有一个。所以就需要把这些释放资源的操作(回调函数给连接起来)。

此时我们看一下:ngx_pool_cleanup_add函数(清理操作的添加函数)

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)//p是 内存池的入口地址
{
    ngx_pool_cleanup_t  *c;//size 是上面void * data,地址的字节大小
	
	//下面就是通过 小块内存开辟函数  来分配  包含清理外部资源的操作的头信息
    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }
	
	//这里的size  如果清理函数需要传参,则把 这个所需要占的内存大小给传进去
    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }

    } else {//预置函数  不需要参数传入size就填0
        c->data = NULL;//然后data也就置成空,在调用回调函数的时候 不需要传入任何参数
    }
	
	//接下来的操作就是:仅仅是开辟回掉函数的内存头信息,尚未对Handler进行赋值呢
    c->handler = NULL;

	//下面两句话:把这个提前分配的 预置回调函数的内存头给连接在cleanup链表上
    c->next = p->cleanup;
    p->cleanup = c;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

    return c;//返回的就是 创建的内存头信息的 起始地址
}

c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
如上:ngx_pool_cleanup_t 结构体的类型变量 和大块内存的内存头信息一样也是保存在 小块内存块上的内存池里面(通过ngx_palloc小块内存分配函数)

总结:ngx_pool_cleanup_add函数先是创建 ngx_pool_cleanup_t 结构体的类型变量(清理函数的内存头信息)也即:在大块内存释放之前,需要先进行 外部资源的清理工作。然后对ngx_pool_cleanup_t 结构体的类型变量做一下初始化操作,最后返回的就是 创建的内存头信息的 起始地址。

于是在调用完ngx_pool_cleanup_add函数之后(已经把内存外部资源清理函数的 内存头变量的内存开辟好了,且也挂上了cleanup链表上了),我提供这么一个函数(用户给定的回调函数)如下:
在这里插入图片描述
接下来做的事情就是:给handler 和 data赋值:(next是链表操作,已经有值了)
在这里插入图片描述
此刻进入:ngx_destroy_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);//调用执行 回调函数。并把当前头里面的 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;
        }
    }
}

在这里插入图片描述
外部资源的申请是用户操作的,所以 清理的回调函数也是需要用户提供。

ngx_destroy_pool函数 若是先清理 小块内存,那么ngx_pool_cleanup_t 结构体的类型变量 和大块内存的内存头信息一样也是保存在 小块内存块上的内存池里面(通过ngx_palloc小块内存分配函数)就全部失效了。

ngx_destroy_pool函数释放顺序:先释放大块内存上成员变量占用的外部资源,执行用户提供的回调资源清理函数。把data传给handler,通过调用回调函数先释放外部资源。接下来:遍历large指针,ngx_free(l->alloc);//释放大块内存。第三步:遍历pool 把这一个个的小块 内存池给free了。

至此为止 把Nginx内存池外部资源释放和内存池销毁函数 解剖结束。

3.6 内存池接口函数的 功能编译测试

此时我们看一下:ngx_pool_cleanup_add函数(清理操作的添加函数)

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)//p是 内存池的入口地址
{
    ngx_pool_cleanup_t  *c;//size 是上面void * data,地址的字节大小
	
	//下面就是通过 小块内存开辟函数  来分配  包含清理外部资源的操作的头信息
    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }
	
	//这里的size  如果清理函数需要传参,则把 这个所需要占的内存大小给传进去
    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }

    } else {//预置函数  不需要参数传入size就填0
        c->data = NULL;//然后data也就置成空,在调用回调函数的时候 不需要传入任何参数
    }
	
	//接下来的操作就是:仅仅是开辟回掉函数的内存头信息,尚未对Handler进行赋值呢
    c->handler = NULL;

	//下面两句话:把这个提前分配的 预置回调函数的内存头给连接在cleanup链表上
    c->next = p->cleanup;
    p->cleanup = c;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

    return c;//返回的就是 创建的内存头信息的 起始地址
}

在释放大块内存的之前,先去释放大块内存可能占有的外部资源,再去释放大块内存。本节重点就是实际上用代码 演练,来详细剖析 在内存销毁ngx_destroy_pool函数调用的时候,是否会调用?以及pool->cleanup的这个链表上 pool->cleanup->handler是 怎么执行的?
第一步:在这里插入图片描述
第二步:在这里插入图片描述
遇到生成error:忽略掉在这里插入图片描述
第三步:执行make 生成目标文件:
在这里插入图片描述
第四步:把我们自定义的测试源文件拷贝进去:进行编译
在这里插入图片描述
源文件代码为:

#include <ngx_config.h>
#include <nginx.h>
#include <ngx_core.h>
#include <ngx_palloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*
这里只是为了测试一下我们这里提供的外部资源释放函数是否会被调用
以及调用过程是怎样的
*/

void ngx_log_error_core(ngx_uint_t level, ngx_log_t* log, ngx_err_t err,
	const char* fmt, ...)//日志打印函数
{

}

typedef struct Data stData;
struct Data//这是 我们申请大块内存放的东西  假装它需要512字节
{
	char* ptr;//关键是这两个指针代表的资源类型也还是不一样的
	FILE* pfile;//指向外部资源不同 处理关闭函数也就不同
};

//下面两个是 用户自己提供的外部资源清理函数
void func1(char* p)
{
	printf("free ptr mem!");
	free(p);
}
void func2(FILE* pf)
{
	printf("close file!");
	fclose(pf);
}
void main()
{
	// 512 - sizeof(ngx_pool_t) - 4095   =>   较小值放入max域
	ngx_pool_t* pool = ngx_create_pool(512, NULL);//创建一个内存池
	if (pool == NULL)
	{
		printf("ngx_create_pool fail...");
		return;
	}

	void* p1 = ngx_palloc(pool, 128); // 从小块内存池分配的
	if (p1 == NULL)
	{
		printf("ngx_palloc 128 bytes fail...");
		return;
	}

	stData* p2 = ngx_palloc(pool, 512); // 从大块内存池分配的
	if (p2 == NULL)
	{
		printf("ngx_palloc 512 bytes fail...");
		return;
	}
	p2->ptr = malloc(12);//指向的是堆上的一块资源
	strcpy(p2->ptr, "hello world");
	p2->pfile = fopen("data.txt", "w");//指向的是一个打开的文件


	//给回调函数 传入相应的参数:释放内存的起始地址
	ngx_pool_cleanup_t* c1 = ngx_pool_cleanup_add(pool, sizeof(char*));
	c1->handler = func1;//保存回调函数
	c1->data = p2->ptr;

	ngx_pool_cleanup_t* c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*));
	c2->handler = func2;
	c2->data = p2->pfile;//作为参数 传给回调函数

	ngx_destroy_pool(pool); 
	// 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存

	return;
}

编译命令:

gcc -c -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules -o ngx_testpool.o  ngx_testpool.c

第五步:链接 :这里也直接生成的是带调试信息的可执行文件
链接命令:

gcc -o ngx_testpool ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o

第六步 执行可执行文件:在这里插入图片描述
正确!!!
我们对它进行单步调试:
在这里插入图片描述
在这里插入图片描述
2019年8月21日11:19:48

到此Nginx内存池的源码剖析结束