nginx内存池源码解析


Posted in Servers onNovember 20, 2021

内存池概述

    内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。

   内存池的好处有减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能,减少程序员在编写代码中对内存的关注等

   目前一些常见的内存池实现方案有STL中的内存分配区,boost中的object_pool,nginx中的ngx_pool_t,google的开源项目TCMalloc等。

为了自身使用的方便,Nginx封装了很多有用的数据结构,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,对于内存池,nginx设计的十分精炼,值得我们学习,本文重点给大家介绍nginx内存池源码,并用一个实际的代码例子作了进一步的讲解。

一、nginx数据结构

// SGI STL小块和大块内存的分界点:128B
// nginx(给HTTP服务器所有的模块分配内存)小块和大块内存的分界点:4096B
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1) 

// 内存池默认大小
#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)

// 内存池字节对齐,SGI STL对其是8B
#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)

// 将开辟的内存调整到16的整数倍
#define ngx_align(d, a)          (((d) + (a - 1)) & ~(a - 1))
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;

// 内存池块的类型
struct ngx_pool_s {
    ngx_pool_data_t       d;          // 内存池块头信息
    size_t                max;	
    ngx_pool_t           *current;    // 指向可用于分配空间的内存块(failed < 4)的起始地址
    ngx_chain_t          *chain;      // 连接所有的内存池块
    ngx_pool_large_t     *large;	  // 大块内存的入口指针
    ngx_pool_cleanup_t   *cleanup;    // 内存池块的清理操作,用户可设置回调函数,在内存池块释放之前执行清理操作
    ngx_log_t            *log;        // 日志
};

nginx内存池源码解析

二、nginx向OS申请空间ngx_create_pool

// 根据size进行内存开辟
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
    ngx_pool_t  *p;
	// 根据系统平台定义的宏以及用户执行的size,调用不同平台的API开辟内存池
    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);              // 能使用的空间 = 总空间 - 头信息
    // 指定的大小若大于一个页面就用一个页面,否则用指定的大小
    // max = min(size, 4096),max指的是除开头信息以外的内存块的大小
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;         // 指向可用于分配空间的内存块的起始地址
    p->chain = NULL;
    p->large = NULL;        // 小块内存直接在内存块开辟,大块内存在large指向的内存开辟
    p->cleanup = NULL;
    p->log = log;

    return p;
}

nginx内存池源码解析

三、nginx向内存池申请空间

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
    	// 当前分配的空间小于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);   // 可以初始化内存为0
    }

    return p;
}

ngx_palloc_small 分配效率高,只做了指针的偏移

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;
	// 从第一个内存块的current指针指向的内存池进行分配
    p = pool->current;

    do {
        m = p->d.last;  // m指向可分配内存的起始地址

        if (align) {
        	// 把m调整为NGX_ALIGNMENT整数倍
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
		// 内存池分配内存的核心代码
        if ((size_t) (p->d.end - m) >= size) {
        	// 若可分配空间 >= 申请的空间
        	// 偏移d.last指针,记录空闲空间的首地址
            p->d.last = m + size;
            return m;
        }
        // 当前内存块的空闲空间不够分配,若有下一个内存块则转向下一个内存块
        // 若没有,p会被置空,退出while
        p = p->d.next;
    } while (p);
	
    return ngx_palloc_block(pool, size);
}

当前内存池的块足够分配:

nginx内存池源码解析

当前内存池的块不够分配:

  1. 开辟新的内存块,修改新内存块头信息的last、end、next、failed
  2. 前面所有内存块的failed++
  3. 连接新的内存块以及前面的内存块
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);
	
	// 将psize对齐为NGX_POOL_ALIGNMENT的整数倍后,向OS申请空间
    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;		   // 下一块内存的地址为NULL 
    new->d.failed = 0;		   // 当前内存块分配空间失败的次数
    
	// 指向头信息的尾部,而max,current、chain等只在第一个内存块有
    m += sizeof(ngx_pool_data_t);  
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;                // last指向当前块空闲空间的起始地址
	
	// 由于每次都是从pool->current开始分配空间
	// 若执行到这里,除了new这个内存块分配成功,其他的内存块全部分配失败
    for (p = pool->current; p->d.next != NULL; p = p->d.next) {
    	// 对所有的内存块的failed都++,直到该内存块分配失败的次数大于4了
    	// 就表示该内存块的剩余空间很小了,不能再分配空间了
    	// 就修改current指针,下次从current开始分配空间,再次分配的时候可以不用遍历前面的内存块
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }
	
    p->d.next = new;   // 连接可分配空间的首个内存块 和 新开辟的内存块

    return m;
}

nginx内存池源码解析

四、大块内存的分配与释放

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;   // 下一个大块内存的起始地址
    void                 *alloc;  // 大块内存的起始地址
};

static void * ngx_palloc_large(ngx_pool_t *pool, size_t size){
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;
	
	// 调用的就是malloc
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;
	// for循环遍历存储大块内存信息的链表
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
        	// 当大块内存被ngx_pfree时,alloc为NULL
        	// 遍历链表,若大块内存的首地址为空,则把当前malloc的内存地址写入alloc
            large->alloc = p;
            return p;
        }
		// 遍历4次后,若还没有找到被释放过的大块内存对应的信息
		// 为了提高效率,直接在小块内存中申请空间保存大块内存的信息
        if (n++ > 3) {
            break;
        }
    }
	// 通过指针偏移在小块内存池上分配存放大块内存*next和*alloc的空间
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
    	// 如果在小块内存上分配存储*next和*alloc空间失败,则无法记录大块内存
    	// 释放大块内存p
        ngx_free(p);
        return NULL;
    }
	
    large->alloc = p;			   // alloc指向大块内存的首地址
    large->next = pool->large;	   // 这两句采用头插法,将新内存块的记录信息存放于以large为头结点的链表中
    pool->large = large;

    return p;
}

nginx内存池源码解析

大块内存的释放

// 释放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) {
    	// 遍历存储大块内存信息的链表,找到p对应的大块内存
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            // 释放大块内存,但不释放存储信息的内存空间
            ngx_free(l->alloc);  // free
            l->alloc = NULL;     // alloc置空

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

五、关于小块内存不释放

就用了last和end两个指着标识空闲的空间,是无法将已经使用的空间合理归还到内存池的,只是会重置内存池。同时还存储了指向大内存块large和清理函数cleanup的头信息

考虑到nginx的效率,小块内存分配高效,同时也不回收内存

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、failed、current、chain、large等管理信息
    for (p = pool; p; p = p->d.next) {
    	// 由于只有第一个内存块有除了ngx_pool_data_t以外的管理信息,别的内存块只有ngx_pool_data_t的信息
    	// 不会出错,但是会浪费空间
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }
	
	// current指向可用于分配内存的内存块
    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

nginx本质是http服务器,通常处理的是短链接,间接性提供服务,需要的内存不大,所以不回收内存,重置即可。

客户端发起一个requests请求后,nginx服务器收到请求会返回response响应,若在keep-alive时间内没有收到客户的再次请求,nginx服务器会主动断开连接,此时会reset内存池。下一次客户端请求再到来时,可以复用内存池。

如果是处理长链接,只要客户端还在线,服务器的资源就无法释放,直到系统资源耗尽。长链接一般使用SGI STL内存池的方式进行内存的开辟和释放,而这种方式分配和回收空间的效率就比nginx低

六、销毁和清空内存池

假设如下情况:

// 假设内存对齐为4B
typedef struct{
	char* p;
	char data[508];
}stData;

ngx_pool_t *pool = ngx_create_pool(512, log);  // 创建一个总空间为512B的nginx内存块
stData* data_ptr = ngx_alloc(512);            // 因为可用的实际内存大小为:512-sizeof(ngx_pool_t),所以属于大内存开辟
data_ptr->p = malloc(10);                   // p指向外界堆内存,类似于C++对象中对用占用了外部资源

当回收大块内存的时候,调用ngx_free,就会导致内存泄漏

nginx内存池源码解析

以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)

typedef void (*ngx_pool_cleanup_pt)(void *data);

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

// 以下结构体由ngx_pool_s.cleanup指向,也是存放在内存池的小块内存
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;     // 指向需要释放的资源
    ngx_pool_cleanup_t   *next;     // 释放资源的函数都放在一个链表,用next指向这个链表
};

nginx提供的函数接口:

// p表示内存池的入口地址,size表示p->cleanup->data指针的大小
// p->cleanup指向含有清理函数信息的结构体
// 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申请size的空间
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }
    } else {
        c->data = NULL;
    }

    c->handler = NULL;
    // 采用头插法,将当前结构体串在pool->cleanup后
    c->next = p->cleanup;
    p->cleanup = c;

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

    return c;
}

使用方式:

void release(void* p){
	free(p);
}

ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(pool, sizeof(char*));
clean_ptr->handler = &release;   // 用户设置销毁内存池前需要调用的函数
clean_ptr->data = data_ptr->p;   // 用户设置销毁内存池前需要释放的内存的地址

ngx_destroy_pool(pool);          // 用户销毁内存池

七、编译测试内存池接口功能

void ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;
	
	// 遍历cleanup链表(存放的时释放前需要调用的函数),可释放外部占用的资源
    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;
        }
    }
}

nginx内存池源码解析

执行configure生成Makefile文件(若报错则表示需要apt安装软件)

nginx内存池源码解析

Makefile如下:

nginx内存池源码解析

执行make命令使用Makefile编译源码,在相应目录下生成 .o文件

nginx内存池源码解析

#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{
    char *ptr;
    FILE *pfile;
};

void func1(char *p){
    printf("free ptr mem!\n");
    free(p);
}

void func2(FILE *pf){
    printf("close file!\n");
    fclose(pf);
}

void main(){
	// max = 512 - sizeof(ngx_pool_t)
	// 创建总空间为512字节的nginx内存块
    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;
	
	// 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存
    ngx_destroy_pool(pool); 

    return;
}

nginx内存池源码解析

由于ngx_pool_cleanup_add中用头插法将创建的清理块链入pool->cleanup,所以ngx_destroy_pool的时候先清理文件后清理堆内存。

相关测试代码推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool

到此这篇关于nginx内存池源码解析的文章就介绍到这了,更多相关nginx内存池内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Servers 相关文章推荐
解决Nginx 配置 proxy_pass 后 返回404问题
Mar 31 Servers
nginx处理http请求实现过程解析
Mar 31 Servers
nginx location优先级的深入讲解
Mar 31 Servers
图文详解nginx日志切割的实现
Jan 18 Servers
详解nginx location指令
Jan 18 Servers
使用 Apache Dubbo 实现远程通信(微服务架构)
Feb 12 Servers
从零开始在Centos7上部署SpringBoot项目
Apr 07 Servers
Windows Server 2012 R2 磁盘分区教程
Apr 29 Servers
Tomcat配置访问日志和线程数
May 06 Servers
详解如何使用Nginx解决跨域问题
May 06 Servers
Nginx文件已经存在全局反向代理问题排查记录
Jul 15 Servers
本地搭建minio文件服务器(使用bat脚本启动)的方法
Jul 15 Servers
苹果M1芯片安装nginx 并且部署vue项目步骤详解
Nginx stream 配置代理(Nginx TCP/UDP 负载均衡)
Nov 17 #Servers
Nginx源码编译安装过程记录
Nov 17 #Servers
Nginx 路由转发和反向代理location配置实现
Nov 11 #Servers
nginx中proxy_pass各种用法详解
Apache POI的基本使用详解
nginx实现动静分离的方法示例
You might like
一个简单的PHP入门源程序
2006/10/09 PHP
PHP+Ajax异步通讯实现用户名邮箱验证是否已注册( 2种方法实现)
2011/12/28 PHP
PHP判断IP并转跳到相应城市分站的方法
2015/03/25 PHP
谈谈PHP中substr和substring的正确用法及相关参数的介绍
2015/12/16 PHP
php使用Jpgraph创建折线图效果示例
2017/02/15 PHP
一个收集图片的bookmarlet(js 刷新页面中的图片)
2010/05/27 Javascript
js change,propertychange,input事件小议
2011/12/20 Javascript
JS简单模拟触发按钮点击功能的方法
2015/11/30 Javascript
BootStrap初学者对弹出框和进度条的使用感觉
2016/06/27 Javascript
基于javascript实现最简单选项卡切换
2017/02/01 Javascript
Move.js入门
2017/02/08 Javascript
Vue.js学习笔记之修饰符详解
2017/07/25 Javascript
JavaScript数组的5种迭代方法
2017/09/29 Javascript
JS实现的文字间歇循环滚动效果完整示例
2018/02/13 Javascript
vue 国际化 vue-i18n 双语言 语言包
2018/06/07 Javascript
Node.js中的cluster模块深入解读
2018/06/11 Javascript
详解VS Code使用之Vue工程配置format代码格式化
2019/03/20 Javascript
js 获取本周、上周、本月、上月、本季度、上季度的开始结束日期
2020/02/01 Javascript
Python编程实现从字典中提取子集的方法分析
2018/02/09 Python
pandas数据清洗,排序,索引设置,数据选取方法
2018/05/18 Python
python画柱状图--不同颜色并显示数值的方法
2018/12/13 Python
对python mayavi三维绘图的实现详解
2019/01/08 Python
python绘制地震散点图
2019/06/18 Python
解决在pycharm运行代码,调用CMD窗口的命令运行显示乱码问题
2019/08/23 Python
python实现从ftp服务器下载文件
2020/03/03 Python
解决python父线程关闭后子线程不关闭问题
2020/04/25 Python
python如何处理程序无法打开
2020/06/16 Python
捷克浴室和厨房设备购物网站:SIKO
2018/08/11 全球购物
美国手工艺品市场的领导者:Annie’s
2019/04/04 全球购物
碧欧泉法国官网:Biotherm法国
2019/10/23 全球购物
银行毕业实习自我鉴定
2013/09/19 职场文书
机电一体化专业应届生求职信
2013/11/27 职场文书
煤矿开采专业求职信
2014/07/08 职场文书
旅游活动总结
2014/08/27 职场文书
宾馆前台接待岗位职责
2015/04/02 职场文书
python编程实现清理微信重复缓存文件
2021/11/01 Python