评论

收藏

[Nginx] nginx内存池源码解析

web服务器 web服务器 发布于:2021-12-01 10:51 | 阅读数:671 | 评论:0

内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用,接下来通过本文给大家介绍nginx内存池源码,本文通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
目录

  • 内存池概述
  • 一、nginx数据结构
  • 二、nginx向OS申请空间ngx_create_pool
  • 三、nginx向内存池申请空间
  • 四、大块内存的分配与释放
  • 五、关于小块内存不释放
  • 六、销毁和清空内存池
  • 七、编译测试内存池接口功能

内存池概述
内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。
内存池的好处有减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能,减少程序员在编写代码中对内存的关注等
目前一些常见的内存池实现方案有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;    // 日志
};
DSC0000.jpg


二、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;
}
DSC0001.jpg


三、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);
}
当前内存池的块足够分配:
DSC0002.jpg

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

  • 开辟新的内存块,修改新内存块头信息的last、end、next、failed
  • 前面所有内存块的failed++
  • 连接新的内存块以及前面的内存块
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;
}
DSC0003.jpg


四、大块内存的分配与释放
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;
}
DSC0004.jpg

大块内存的释放
// 释放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,就会导致内存泄漏
DSC0005.jpg

以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)
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;
    }
  }
}
DSC0006.png

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

Makefile如下:
DSC0008.png

执行make命令使用Makefile编译源码,在相应目录下生成 .o文件
DSC0009.png
#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;
}
DSC00010.jpg

由于ngx_pool_cleanup_add中用头插法将创建的清理块链入pool->cleanup,所以ngx_destroy_pool的时候先清理文件后清理堆内存。
相关测试代码推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool
到此这篇关于nginx内存池源码解析的文章就介绍到这了,更多相关nginx内存池内容请搜索CodeAE代码之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持CodeAE代码之家!
原文链接:https://blog.csdn.net/qq_42500831/article/details/121328558

关注下面的标签,发现更多相似文章