nginx内存池源码分析

技术nginx内存池源码分析本篇内容主要讲解“nginx内存池源码分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nginx内存池源码分析”吧!内存池概述 内存池是在

本文主要讲解“nginx内存池源代码分析”,感兴趣的朋友不妨看一下。本文介绍的方法简单、快速、实用。让边肖带你学习“nginx内存池源代码分析”!

内存池概述

内存池是在实际使用内存之前,申请分配一定数量的大小相同的内存块(一般情况下)备用。当有新的内存需求时,一部分内存块会从内存池中分离出来。如果内存块不够,继续申请新内存。

内存池的优势包括降低系统申请和释放内存的时间成本,解决频繁内存分配带来的碎片化,提示程序性能,降低程序员写代码时对内存的关注。

目前一些常见的内存池实现方案包括STL中的内存分配区、object_pool、nginx boost、nginx中的NGX _ pool _ T、Google的开源项目TCMalloc等。

00-1010//SGISTL小块和大块内存的分界点:128B

//nginx(分配内存给HTTP服务器的所有模块)内存大小的分界点:4096B

# defineNGX _ MAX _ ALLOC _ FROM _ POOL(ngx _ page size-1)

//默认内存池大小

# Definengx _ DEFAULT _ POLL _ SIZE(16 * 1024)

//内存池字节对齐,SGISTL为8B

# define engx _ POLL _ ALIGNMENT 16

# defineNGX _ MIN _ POLL _ Sizengx _ align((sizeof(ngx _ POLL _ t)2 * sizeof(ngx _ POLL _ large _ t)),\

NGX _ POLL _ ALLIANCE)

//将打开的内存调整为16的整数倍

#definengx_align(d,a)(((d)(a-1))~(a-1))typedefstructngx _ pool _ sngx _ pool _ t;

typedefstruct{

u _ char * last//指向可用内存的起始地址。

u _ char * end//指向可用内存的最后一个地址

ngx _ pool _ t * next//指向下一个内存块

ngx _ uint _失败;//当前内存块分配空间失败的次数

} ngx _ pool _ data _ t;

//内存池块的类型

structngx_pool_s{

ngx _ pool _ data _ td铌

sp;      // 内存池块头信息
    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的时候先清理文件后清理堆内存。

到此,相信大家对“nginx内存池源码分析”有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/104192.html

(0)

相关推荐

  • Idea下仓库初始化与文件提交涉及到的基本命令有哪些

    技术Idea下仓库初始化与文件提交涉及到的基本命令有哪些小编给大家分享一下Idea下仓库初始化与文件提交涉及到的基本命令有哪些,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、Idea下仓库初始化与文件

    攻略 2021年12月9日
  • 如何使用Java的MD5工具类和客户端测试类

    技术Java的MD5工具类和客户端测试类怎么使用这篇文章主要讲解了“Java的MD5工具类和客户端测试类怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java的M

    攻略 2021年12月16日
  • 更新webservice引用会遇到什么问题

    技术更新webservice引用会遇到什么问题这篇文章给大家分享的是有关更新webservice引用会遇到什么问题的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。当写完webservice,并且

    攻略 2021年11月23日
  • 野心勃勃是什么意思,为什么二战时日本那么野心勃勃

    技术野心勃勃是什么意思,为什么二战时日本那么野心勃勃日本的野心,主要就是我们所熟悉的“大陆政策”野心勃勃是什么意思。而日本在二战时的疯狂,主要源于三方面。第一, 是军国主义盛行。
    日本军国主义虽然在近代才开始践行,却

    生活 2021年10月24日
  • 鼠标指针怎么换,Cursosr怎么换鼠标指

    技术鼠标指针怎么换,Cursosr怎么换鼠标指打开控制面板,双击“鼠标”,选择“指针”选项卡鼠标指针怎么换。可以更改方案,或者选中下面的某一状态,点击“浏览”,选择一个鼠标图片即可。
    但所有的鼠标的图片格式都是.cur的

    生活 2021年10月30日
  • 1317:【例5.2】组合的输出 深搜题解

    技术1317:【例5.2】组合的输出 深搜题解 1317:【例5.2】组合的输出 深搜题解1317:【例5.2】组合的输出
    时间限制: 1000 ms 内存限制: 65536 KB提交数: 2425

    礼包 2021年11月1日