千家信息网

nginx内存池源码分析

发表于:2024-11-22 作者:千家信息网编辑
千家信息网最后更新 2024年11月22日,本篇内容主要讲解"nginx内存池源码分析",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"nginx内存池源码分析"吧!内存池概述内存池是在真正使用内存之前
千家信息网最后更新 2024年11月22日nginx内存池源码分析

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

内存池概述

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

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

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

一、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向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向内存池申请空间

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

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

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

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

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

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

大块内存的释放

// 释放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低

六、销毁和清空内存池

假设如下情况:

// 假设内存对齐为4Btypedef 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,就会导致内存泄漏

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

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

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

Makefile如下:

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

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

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

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

0