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 比 Apache 更牛逼
Mar 31 Servers
Nginx 根据URL带的参数转发的实现
Apr 01 Servers
Linux中Nginx的防盗链和优化的实现代码
Jun 20 Servers
制作能在nginx和IIS中使用的ssl证书
Jun 21 Servers
nginx配置虚拟主机的详细步骤
Jul 21 Servers
Nginx location 和 proxy_pass路径配置问题小结
Sep 04 Servers
Windows下用Nginx配置https服务器及反向代理的问题
Sep 25 Servers
nginx从安装到配置详细说明(安装,安全配置,防盗链,动静分离,配置 HTTPS,性能优化)
Feb 12 Servers
tomcat正常启动但网页却无法访问的几种解决方法
May 06 Servers
Windows server 2012 NTP时间同步的实现
Jun 25 Servers
云服务器部署 Web 项目的实现步骤
Jun 28 Servers
教你使用Ubuntu搭建DNS服务器
Sep 23 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
phpMyAdmin 链接表的附加功能尚未激活的问题
2010/08/01 PHP
一个简单的php加密解密函数(动态加密)
2013/06/19 PHP
免费手机号码归属地API查询接口和PHP使用实例分享
2014/04/10 PHP
codeigniter发送邮件并打印调试信息的方法
2015/03/21 PHP
php模板引擎技术简单实现
2016/03/15 PHP
JQuery Tips(4) 一些关于提高JQuery性能的Tips
2009/12/19 Javascript
Jquery getJSON方法详细分析
2013/12/26 Javascript
JavaScript实现的链表数据结构实例
2015/04/02 Javascript
javascript实现表单提交后,提交按钮不可用的方法
2015/04/18 Javascript
理解javascript中try...catch...finally
2015/12/25 Javascript
基于javascript实现全国省市二级联动下拉选择菜单
2016/01/28 Javascript
深入理解Ajax的get和post请求
2016/06/02 Javascript
分享jQuery封装好的一些常用操作
2016/07/28 Javascript
浅谈js函数的多种定义方法与区别
2016/11/29 Javascript
vue2滚动条加载更多数据实现代码
2017/01/10 Javascript
js中document.referrer实现移动端返回上一页
2017/02/22 Javascript
javascript连接mysql与php通过odbc连接任意数据库的实例
2017/12/27 Javascript
详解ES6中的代理模式——Proxy
2018/01/08 Javascript
在Docker上部署Python的Flask框架的教程
2015/04/08 Python
一个基于flask的web应用诞生 记录用户账户登录状态(6)
2017/04/11 Python
Python中元组,列表,字典的区别
2017/05/21 Python
python简单图片操作:打开\显示\保存图像方法介绍
2017/11/23 Python
Python 日期的转换及计算的具体使用详解
2020/01/16 Python
职称自我鉴定
2013/10/15 职场文书
文明教师事迹材料
2014/01/16 职场文书
JAVA程序员自荐书
2014/01/30 职场文书
技校毕业生个人学习的自我评价
2014/02/21 职场文书
5s推行计划书
2014/05/06 职场文书
街道党风廉政建设调研报告
2015/01/01 职场文书
业务员岗位职责
2015/02/03 职场文书
国富论读书笔记
2015/06/26 职场文书
手把手教你制定暑期学习计划,让你度过充实的暑假
2019/08/22 职场文书
导游词之任弼时故居
2020/01/07 职场文书
用python自动生成日历
2021/04/24 Python
python爬取某网站原图作为壁纸
2021/06/02 Python
win11怎么用快捷键锁屏? windows11锁屏的几种方法
2021/11/21 数码科技