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 相关文章推荐
Filebeat 采集 Nginx 日志的方法
Mar 31 Servers
Nginx实现高可用集群构建(Keepalived+Haproxy+Nginx)
May 27 Servers
Nginx反向代理配置的全过程记录
Jun 22 Servers
nginx的zabbix 5.0安装部署的方法步骤
Jul 16 Servers
Nginx禁止ip访问或非法域名访问
Apr 07 Servers
Mac电脑OS系统下安装Nginx的详细教程
Apr 14 Servers
nginx搭建NFS网络文件系统
Apr 14 Servers
IIS服务器中设置HTTP重定向访问HTTPS
Apr 29 Servers
winserver2019安装软件一直卡在应用程序正在为首次使用做准备
Jun 10 Servers
V Rising 服务器搭建图文教程
Jun 16 Servers
使用 DataAnt 监控 Apache APISIX的原理解析
Jul 07 Servers
Docker安装MySql8并远程访问的实现
Jul 07 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
不用数据库的多用户文件自由上传投票系统(3)
2006/10/09 PHP
php购物网站支付paypal使用方法
2010/11/28 PHP
php实现过滤UBB代码的类
2015/03/12 PHP
JavaScript 对话框和状态栏使用说明
2009/10/25 Javascript
onkeypress字符按键兼容所有浏览器使用介绍
2013/04/24 Javascript
javascript随机显示背景图片的方法
2015/06/18 Javascript
jquery-mobile基础属性与用法详解
2016/11/23 Javascript
jQuery弹出div层过2秒自动消失
2016/11/29 Javascript
纯js三维数组实现三级联动效果
2017/02/07 Javascript
node.js中debug模块的简单介绍与使用
2017/04/25 Javascript
深入理解ES7的async/await的用法
2017/09/09 Javascript
AngularJS实现表单验证功能详解
2017/10/12 Javascript
React全家桶环境搭建过程详解
2018/05/18 Javascript
Electron 调用命令行(cmd)
2019/09/23 Javascript
JS操作json对象key、value的常用方法分析
2019/10/29 Javascript
解决elementUI 切换tab后 el_table 固定列下方多了一条线问题
2020/07/19 Javascript
[58:35]OG vs EG 2019国际邀请赛淘汰赛 胜者组 BO3 第二场 8.22
2019/09/05 DOTA
python多维数组切片方法
2018/04/13 Python
Python3 关于pycharm自动导入包快捷设置的方法
2019/01/16 Python
Python符号计算之实现函数极限的方法
2019/07/15 Python
在win64上使用bypy进行百度网盘文件上传功能
2020/01/02 Python
Python面向对象封装操作案例详解 II
2020/01/02 Python
Pytorch 搭建分类回归神经网络并用GPU进行加速的例子
2020/01/09 Python
关于pytorch中全连接神经网络搭建两种模式详解
2020/01/14 Python
详解python变量与数据类型
2020/08/25 Python
Currentbody美国/加拿大:美容仪专家
2020/03/09 全球购物
电气工程及其自动化学生实习自我鉴定
2013/09/19 职场文书
护士自我鉴定
2013/10/23 职场文书
实习教师自我鉴定
2013/12/12 职场文书
优秀的茶餐厅创业计划书
2014/01/03 职场文书
晚会主持词开场白
2014/03/17 职场文书
2014年民政局关于保密工作整改措施
2014/09/19 职场文书
2014国庆节演讲稿:祖国在我心中(400字)
2014/09/25 职场文书
结婚通知短信怎么写
2015/04/17 职场文书
解析目标检测之IoU
2021/06/26 Python
python图像处理 PIL Image操作实例
2022/04/09 Python