C++学习笔记5

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))
#define ngx_align_ptr(p, a)     (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

00000000 00000000 00000000 00001000  a
00000000 00000000 00000000 00000111 a-1    
11111111 11111111 11111111 11111000 ~a-1
d的区间:
1-16   ==> 16
17-32  ==> 32
...
// 小块内存数据头信息
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;        // 日志
};

typedef struct ngx_pool_s ngx_pool_t;
// 大块内存类型定义
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存
void *alloc; // 记录分配的大块内存的起始地址
};

在这里插入图片描述

二nginx内存池重要函数接口

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log); // 创建内存池
void ngx_destroy_pool(ngx_pool_t *pool); // 销毁内存池
void ngx_reset_pool(ngx_pool_t *pool); // 重置内存池
void *ngx_palloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,不支持内存对齐
void *ngx_pcalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存初始化0
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p // 内存释放(大块内存)
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); // 添加清理handler

三、申请内存分配

#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)

void* ngx_alloc(size_t size, ngx_log_t *log){
    void  *p = malloc(size);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "malloc(%uz) failed", size);
    }
    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
    return p;
}
// 根据size进行内存开辟
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
    ngx_pool_t  *p;
	// 根据系统平台定义的宏以及用户执行的size,调用不同平台的API开辟内存池,malloc
    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, 4095),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;
}

在这里插入图片描述

申请的内存不大于4095则调用ngx_palloc_small,分配小块内存。

否则size>4095调用ngx_palloc_large=>ngx_alloc=>malloc,申请大块内存。

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当前内存池的块不够分配

  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开始分配空间,再次分配的时候可以不用遍历current前面的内存块
    if (p->d.failed++ > 4) {
        pool->current = p->d.next;
    }
}

p->d.next = new;   // 连接可分配空间的首个内存块 和 新开辟的内存块

return m;
}

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

在这里插入图片描述

由上面可以看出ngx_palloc_small :带有内存对齐的(把分配出去的起始地址对齐)小块内存池的分配,分配效率高,只做了指针的偏移

#define ngx_align_ptr(p, a)   (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

四、大块内存的分配与释放ngx_palloc_large
在这里插入图片描述

通过ngx_alloc,即malloc分配大块内存,通过ngx_palloc_small在小块内存上存放大块内存头信息,大块内存头信息包含* next和* alloc,分别表示下一个大块内存头信息和大块内存的起始地址
在这里插入图片描述

在小块内存上开辟大块内存头信息时,也不是直接就在小块内存上重新申请新的空间,而是查看前4个内存块,是否有已经被释放过的大块内存的头信息,进行复用。前4个无法复用,则直接使用ngx_palloc_small申请新的空间

并不是每次分配大块内存都重新在小块内存上分配头信息,会遍历存储大块内存头信息的链表,查找已经释放的大块内存

遍历4次后,若还没有找到被释放过的大块内存对应的信息(只查看前4个内存块,由于是头插法,前4个内存块是不断改变的 ,为了提高效率,直接在小块内存中申请空间保存大块内存的信息

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次后,若还没有找到被释放过的大块内存对应的信息(只查看前4个内存块,由于是头插法,前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;
}

五、资源释放

在这里插入图片描述

小块内存的释放:

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释放内存池的大块内存
            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;
    }
	/*
	正确写法如下:
	pool->d.last = (u_char *) pool + sizeof(ngx_pool_t);  // 第一个内存块的last指针偏移ngx_pool_t
	pool->d.failed = 0;
	for (p = pool->d.next; p; p = p->d.next) {
    	// 由于只有第一个内存块有除了ngx_pool_data_t以外的管理信息,别的内存块只有ngx_pool_data_t的信息
    	// 不会出错,但是会浪费空间
        p->d.last = (u_char *) p + sizeof(ngx_pool_data_t);  // 后续所有内存块的last指针偏移ngx_pool_data_t
        p->d.failed = 0;
    }
    */
	
	// current指向可用于分配内存的内存块
    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

大块内存的释放

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

}

释放顺序

1.预制的回调函数,把data传给handler

2.遍历large,释放大块内存

3遍历小块内存池,并且清空

六、设置清理外部资源的回调函数:

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

七、测试内存池释放外部资源
#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){
    free(p);
    printf("free ptr mem!\n");
}

void func2(FILE *pf){
    fclose(pf);
    printf("close file!\n");
}

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 = (char*)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;
}

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值