Nginx源码:内存池的实现
创始人
2024-01-30 11:37:33
0

文章目录

    • 1、数据结构
    • 2、接口函数
      • 2.1、创建内存池
      • 2.2、内存分配
        • 2.2.1、小块内存分配
        • 2.2.2、大块内存分配
      • 2.3、内存释放
        • 2.3.1、大块内存释放
        • 2.3.2、内存池释放
    • 4、参考

为什么需要对内存管理?

  • 避免频繁的系统调用带来的开销。
  • 减少了频繁分配和释放小块内存产生的内存碎片。

解决上述问题,最好的方法就是内存池。内存池就是对堆上的内存进行管理。

内存池的具体做法是固定大小、提前申请、重复利用。

  • 固定大小。在调用内存分配函数的时候,小块内存每次都分配固定大小的内存块,这样避免了内存碎片产生的可能性。
  • 提前申请一块大的内存,内存不够用时再二次分配,减少了malloc的次数,提高了效率。

Nginx 使用内存池管理进程内存,当接收到请求时,创建一个内存池。处理请求过程中需要的内存都从这个内存池中申请,请求处理完成后释放内存池。Nginx 将内存池中的内存分为两类:小块内存和大块内存。对于小块内存,用户申请后并不需要释放,而是等待释放内存池时再释放。对于大块内存,用户可以调用相关接口进行释放,也可以等内存池释放时再释放。同时 Nginx 内存池支持增加回调函数,当内存池释放时,自动调用回调函数释放用户申请的资源。回调函数允许增加多个,通过链表进行链接,在内存池释放时被逐一调用。

源码位置:src/core/ngx_palloc.h, src/core/ngx_palloc.c

1、数据结构

在这里插入图片描述

内存池由内存块链表(内存池节点)组成,每个内存块分为两个两部分,一部分存储该内存块相关信息,另一部分用于小块内存的分配。

ngx_pool_data_t

typedef struct {u_char               *last;   // 指向该内存块已分配内存的末尾地址,下一个待分配内存的起始地址u_char               *end;    // 指向该内存块的末尾地址ngx_pool_t           *next;   // 指向下一个内存块ngx_uint_t            failed; // 当前内存块分配空间失败的次数
} ngx_pool_data_t;

ngx_pool_t:内存块管理信息

struct ngx_pool_s {ngx_pool_data_t       d;        // 内存块管理信息size_t                max;      // 小块内存能分配的最大空间,超过该值使用大块内存分配ngx_pool_t           *current;  // 指向可分配的内存块ngx_chain_t          *chain;    ngx_pool_large_t     *large;    // 指向大块内存链表ngx_pool_cleanup_t   *cleanup;  // 内存块清理函数ngx_log_t            *log;      // 日志信息
};

ngx_pool_large_s:大块内存链表节点

struct ngx_pool_large_s {ngx_pool_large_t     *next;     // 指向下一个大块内存节点void                 *alloc;    // 指向实际分配的大块内存
};

2、接口函数

2.1、创建内存池

申请的内存由两部分组成,一部分用来容纳ngx_pool_t结构体,另一部分内存则是用于满足用户申请。ngx_pool_data_t结构体中的last指针和end指针之间的内存是空闲的,当用户申请小块内存时,如果空闲的内存大小满足用户的需求,则可以分配给用户。

// 内存对齐,默认16字节对齐
#define NGX_POOL_ALIGNMENT       16/*** @brief 创建内存池* @param size 内存块的大小* @param log  log 打印日志* @return ngx_pool_t* 返回创建的内存池地址*/
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log) {ngx_pool_t  *p;// 申请内存,默认16字节对齐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);p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;p->current = p;p->chain = NULL;p->large = NULL;p->cleanup = NULL;p->log = log;return p;
}

2.2、内存分配

用户可以调用ngx_palloc向内存池申请未初始化的内存。分配内存的时候,根据本次申请空间的大小 size 和内存池设定的pool->max,判断当前要分配的内存是小块内存还是大块内存。

void *ngx_palloc(ngx_pool_t *pool, size_t size) {// 判断申请的内存块是大块还是小块// 1、申请小块内存if (size <= pool->max) {return ngx_palloc_small(pool, size, 1);}// 2、申请大块内存return ngx_palloc_large(pool, size);
}

2.2.1、小块内存分配

若用户申请的是小块内存,则调用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;// 获取当前可分配的内存块p = pool->current;do {// 指向下一个内存块m = p->d.last;// 内存对齐if (align) {m = ngx_align_ptr(m, NGX_ALIGNMENT);}// 当前内存块的剩余空间是否足够分配// 1、够分配if ((size_t) (p->d.end - m) >= size) {// 更新 last 指针p->d.last = m + size;return m;}// 2、不够分配,继续查找下一个内存池节点p = p->d.next;} while (p);// 重新申请一个内存块,用于小块内存分配return ngx_palloc_block(pool, size);
}

ngx_palloc_block申请新的内存块,尾插到内存块链表中

每次调用ngx_palloc_block函数,代表现有内存块的小块内存分配失败,此时,所有内存块的 failed + 1,表示不满足用户的需求增加 1 次。 由于采取的是尾插法,所以内存块链表中内存块的 failed 计数值依次递减。若某个内存块连续 5 次不满足用户需求,则不再使用它,下次遍历时跳过。

使用ngx_pool_t->current来记录可分配的内存块,下次遍历时,先尝试从可分配内存块的剩余空间分配。

  • 若空间足够,则返回last的地址作为内存分配的起始地址,并更新 last = last + size
  • 若空间不足,则创建一个新的结点,并更新 p->next,返回 last 的地址作为内存分配的起始地址,并更新 last = last + size
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);// 申请新的内存块(和第一个内存块大小相同)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;new->d.failed = 0;// 将指针m移动到可分配内存的开始位置m += sizeof(ngx_pool_data_t);// 对指针做内存对齐m = ngx_align_ptr(m, NGX_ALIGNMENT);// 设置新内存块的lastnew->d.last = m + size;// 将所有内存块的 failed + 1,表示不满足用户的需求 +1 次for (p = pool->current; p->d.next; p = p->d.next) {// 若某个内存块若连续 5 次都不满足用户需求,则跳过这个内存块,以后不再遍历它if (p->d.failed++ > 4) {// 调整 current 指向下一个内存块(该内存块以前的内存块无法分配内存)pool->current = p->d.next;}}// 将新创建的内存块,尾插到内存块链表p->d.next = new;return m;
}

2.2.2、大块内存分配

对于大块内存,直接申请相应大小的内存,并通过链表将已经申请的大快内存进行链接。值得注意的是,对于大块内存的管理链表节点 ngx_pool_large_t ,从内存池进行申请

在这里插入图片描述

static void *ngx_palloc_large(ngx_pool_t *pool, size_t size) {void              *p;ngx_uint_t         n;ngx_pool_large_t  *large;// 申请大块内存 p = ngx_alloc(size, pool->log);if (p == NULL) {return NULL;}n = 0;// 遍历大块内存链表,找到可以挂载大内存块的位置for (large = pool->large; large; large = large->next) {// 找到该内存块可以挂载的地方if (large->alloc == NULL) {large->alloc = p;return p;}// 若连续 4 次都没找到,不找了if (n++ > 3) {break;}}// 在内存池中申请大块内存管理的链表节点large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);if (large == NULL) {ngx_free(p);return NULL;}// 将大块内头插到内存池中large->alloc = p;large->next = pool->large;pool->large = large;return p;
}

2.3、内存释放

Nginx 内存池内部只提供大块内存的释放接口,小块内存不需要释放,内存池销毁的时候随之释放。

2.3.1、大块内存释放

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p) {ngx_pool_large_t  *l;// 遍历大块内存链表for (l = pool->large; l; l = l->next) {if (p == l->alloc) {ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);ngx_free(l->alloc);l->alloc = NULL;return NGX_OK;}}return NGX_DECLINED;
}

2.3.2、内存池释放

  • 查看内存池是否挂载清理函数,若有,则调用链表中的所有回调函数
  • 释放大块内存
  • 释放内存池中的内存块
void ngx_destroy_pool(ngx_pool_t *pool) {ngx_pool_t          *p, *n;ngx_pool_large_t    *l;ngx_pool_cleanup_t  *c;// 遍历清理函数,逐一调用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); // free}}// 释放内存池内存块for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {ngx_free(p); // freeif (n == NULL) {break;}}
}

4、参考

  • 聂松松等. Nginx底层设计与源码分析[M]. 北京:机械工业出版社,2021.

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...