重写nginx内存池--使用C++ OOP的方式

nginx内存池是nginx中的一个重要功能模块,用于管理内存的分配和释放,以提高nginx服务器的性能和稳定性。

nginx内存池特点

  • nginx内存池是一种灵活、线程安全的内存分配器。与其他内存池不同的是,每个进程或线程都可以创建自己的内存池,因此不需要考虑多线程竞争的问题,无需加锁,从而节省了开销。
  • nginx内存池可以高效地分配小块内存。它通过指针的偏移来管理和分配内存,减少了malloc调用的次数,避免了用户态和内核态频繁切换的开销。每个小块内存池都是一块连续内存,大小控制在一页大小以内,既避免了内存碎片问题造成的内存浪费,页内寻址还保证了申请小内存时的速度效率。
  • 小块内存不提供单个回收接口,只在重置内存池时回收小内存,销毁内存池时内存归还给系统。这是因为nginx多用于http服务器,处理完客户端请求后,等待60秒的keepalive时间后,如果没有收到该客户端链接的后续请求,服务器会主动断开链接,此时nginx可以调用ngx_reset_pool重置内存池,所以小内存无需单个回收。
  • 对于大块内存,nginx内存池使用malloc申请,并将所有申请的大内存地址记录在一条链表中。此外,nginx允许用户挂载自己的数据区域及对应的数据清理操作,用户可以使用ngx_pool_cleanup_add接口注册清理函数,在内存池销毁时释放自己申请的动态内存,避免内存泄漏。

nginx内存池重要的数据结构

在nginx内存池中,最重要的数据结构是ngx_pool_s。它作为整个内存池的头部,管理着内存池节点链表、大内存链表、cleanup链表等信息,并存放在第一个小内存池里。后续其他的小内存池中只有ngx_pool_data_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; // 清理函数handler的入口指针
ngx_log_t *log;
};

ngx_pool_data_t是小内存池的数据头,通过next指针将小内存池串成链表(尾插)。它记录了可分配内存的开始位置和末尾位置,保存了下一个内存池的地址,还记录了当前内存池分配失败的次数。

// 小块内存数据头信息
typedef struct {
u_char *last; // 可分配内存开始位置
u_char *end; // 可分配内存末尾位置
ngx_pool_t *next; // 保存下一个内存池的地址
ngx_uint_t failed; // 记录当前内存池分配失败的次数
} ngx_pool_data_t;

ngx_pool_large_s则是用来记录大内存地址的,通过next指针将大内存串成链表(头插),它记录了分配的大块内存的起始地址。

struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存
void *alloc; // 记录分配的大块内存的起始地址
};

ngx_pool_cleanup_s是用于用户自定义清理操作的,当内存池销毁时,可以用它来清理用户自己申请的动态内存。它包括了一个清理回调函数、传给回调函数的数据和下一个清理操作的地址。

// 清理操作的类型定义,包括一个清理回调函数,传给回调函数的数据和下一个清理操作的地址
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 清理回调函数
void *data; // 传递给回调函数的指针
ngx_pool_cleanup_t *next; // 指向下一个清理操作
};

nginx内存池结构图

在这里插入图片描述

nginx内存池的分配机制和重要接口

nginx内存池提供了高效的内存管理机制,其设计旨在避免内存碎片和频繁的系统调用。以下是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
  • 用户通过调用ngx_create_pool函数来创建指定大小的内存池。这个函数会在内存中分配一块大小为size的内存池,并返回一个指向ngx_pool_t类型的结构体的指针。
  • 用户可以使用ngx_palloc函数从内存池中申请内存。如果所申请的内存大小小于内存池的最大大小,ngx_palloc会遍历小块内存池链表,并从第一个满足要求的内存池中分配内存。分配完成后,当前内存池的last指针会向后偏移size个字节,更新可用内存的起始地址。
  • 如果所申请的内存大小大于内存池的最大大小,ngx_palloc会调用ngx_pool_large使用malloc的方式从系统中再申请一块大小为size的内存,并将其记录到大内存链表中。
  • 如果链表上所有内存池的可用空间都不足以提供用户申请的size空间,ngx_palloc会调用ngx_pool_block再申请一个内存池,从其中分配内存并将其返回给用户,并将新的内存池尾插到内存池链表上。
  • 用户可以通过调用ngx_reset_pool函数来重置内存池。这个函数会释放大内存链表中记录的所有大块内存,并将所有小块内存池的last指针重置回收小块内存。
  • 用户可以通过调用ngx_destroy_pool函数来销毁内存池。这个函数会遍历cleanup列表,调用用户注册的清理函数回收外部资源。然后,它会释放大内存链表中的所有大内存,并释放所有小块内存池。

用C++ OOP的方式封装重写nginx内存池

减少内存浪费

在第一个小内存池中,除了记录头部信息之外,还记录整个内存池的管理成员信息。其他小内存池只记录头部信息。因此,第一个小内存池的可用内存要比其他小内存池小。在重置内存池时,需要将这两种内存池分别处理,以避免内存浪费。
所以在ngx_reset_pool重置函数中,做了优化

ngx_mem_pool.h

#pragma once
#include <stdlib.h>
#include <memory.h>

// 重定义类型
using u_char = unsigned char;
using ngx_uint_t = unsigned int;

// 向上对齐到a的倍数
#define ngx_align(d,a) (((d) + (a -1)) & ~(a - 1)
// 将地址值向上对齐到a(指针大小)的倍数
#define ngx_align_ptr(p, a) \
	(u_char*) (((uintptr_t)(p) + ((uintptr_t)a - 1)) & ~((uintptr_t)a - 1))

// 将给定内存初始化为0
#define ngx_memzero(buf, n)  (void) memset(buf, 0, n)

// 前置声明
struct ngx_pool_s;

// 一个页面大小为4K
const int ngx_pagesize = 4096;
// 大块内存和小块内存的分解
const int NGX_MAX_ALLOC_FROM_POOL = (ngx_pagesize - 1);
// 如果用户不提供内存池size参数,则默认16K
const int NGX_DEFAULT_POOL_SIZE = (16 * 1024);
// 内存池大小字节对齐16byte
const int NGX_POOL_ALIGNMENT = 16;


const int NGX_ALIGNMENT = sizeof(unsigned long);

// 清理外部资源回调函数指针的类型定义
typedef void (*ngx_pool_cleanup_pt)(void* data);

// 小块内存池的头部信息
struct ngx_pool_data_t {
    u_char* last;   // 小块内存池中可分配内存起始位置
    u_char* end;    // 小块内存池中可分配内存末尾位置
    ngx_pool_s* next; // 所有小块内存池都被串在一个链表上
    ngx_uint_t failed; // 当前小块内存池内存分配失败次数
};

// 管理大块内存池,用于将所有大块内存池串成链表
struct ngx_pool_large_s {
    ngx_pool_large_s* next; // 将所有大块内存池串成链表
    void* alloc; // 从堆上malloc申请的大块内存
};

// 管理清理外部资源回调函数指针的类
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler; // 清理外部资源回调函数指针, 由用户提供
    void* data;
    ngx_pool_cleanup_s* next; // 串成一个链表
};


// 内存池的管理成员
struct ngx_pool_s {
    ngx_pool_data_t       d; // 小块内存池的头部信息
    size_t                max;  // 申请的内存大于max,则分配大块内存
    ngx_pool_s* current; // 指向当前可分配的小块内存池
    ngx_pool_large_s* large; // 指向大块内存池的管理头
    ngx_pool_cleanup_s* cleanup; // 指向清理外部资源的回调函数管理结构
};

// 最小内存池size
const int NGX_MIN_POOL_SIZE = ngx_align((sizeof(ngx_pool_s) + 2 * sizeof(ngx_pool_large_s)), NGX_POOL_ALIGNMENT));

class ngx_mem_pool
{
public:
    // 申请一块内存池
    ngx_pool_s* ngx_create_pool(size_t size);
    // 从内存池中分配内存,并且考虑字节对齐
    void* ngx_palloc(size_t size);
    // 从内存池中分配内存,不考虑字节对齐
    void* ngx_pnalloc(size_t size);
    // 从内存池中分配内存,并且考虑字节对齐, 并且初始化为0
    void* ngx_pcalloc(size_t size);
    // 当前小块内存池耗尽时,用于申请新的小块内存池
    void* ngx_palloc_block(size_t size);
    // 释放大块内存池中的节点
    void  ngx_pfree(void* p);
    // 重置所有内存池
    void ngx_reset_pool();
    // 销毁内存,并释放外部资源
    void ngx_destroy_pool();
    // 给用户注册回调函数,用于在内存池销毁时清理用户自己申请的外部内存资源
    ngx_pool_cleanup_s* ngx_pool_cleanup_add(size_t size);

private:
    ngx_pool_s* pool;

    void* ngx_palloc_small(size_t size, ngx_uint_t align);
    void* ngx_palloc_large(size_t size);
};

ngx_mem_pool.cpp

#include "ngx_mem_pool.h"
#include "stdio.h"

// 创建第一个内存池,并初始化内存池管理成员
ngx_pool_s* ngx_mem_pool::ngx_create_pool(size_t size)
{
	// malloc申请内存池空间
	pool = (ngx_pool_s*)malloc(size);
	if (nullptr == pool)
	{
		printf("ngx create pool failed!\n");
		return nullptr;
	}

	// 初始化小块内存池头部信息
	pool->d.last = (u_char*)pool + sizeof(ngx_pool_s);
	pool->d.end = (u_char*)pool + size;
	pool->d.next = nullptr;
	pool->d.failed = 0;

	// 小块内存可分配大小(第一块小块内存池减去内存池管理成员大小)
	size = size - sizeof(ngx_pool_s);
	pool->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

	// 初始化内存池管理成员
	pool->current = pool;
	pool->large = nullptr;
	pool->cleanup = nullptr;

	printf("ngx create pool, first small pool created!\n");
	return pool;
}

// 从内存池中分配内存,字节对齐
void* ngx_mem_pool::ngx_palloc(size_t size)
{
	if (size <= pool->max)
	{
		return ngx_palloc_small(size, 1);
	}
	return ngx_palloc_large(size);
}

// 从小块内存池中分配内存
void* ngx_mem_pool::ngx_palloc_small(size_t size, ngx_uint_t align)
{
	u_char* m;
	ngx_pool_s* p;
	p = pool->current;
	do {
		m = p->d.last;
		
		if (align) // 地址按照指针大小,字节对齐
		{
			m = ngx_align_ptr(m, NGX_ALIGNMENT);
		}

		if ((size_t)(p->d.end - m) >= size) // 当前小块内存池可分配空间大于申请内存
		{
			p->d.last = m + size;
			return m;
		}

		p = p->d.next; // 遍历链表上的小块内存池
	} while (p);

	// 如果当前所有小块内存池都没有足够的空间,新开辟一个小块内存池
	return ngx_palloc_block(size);
}

void* ngx_mem_pool::ngx_palloc_large(size_t size)
{
	void* p; // 保存申请大块内存的地址
	ngx_uint_t n;
	ngx_pool_large_s* large; // 用于管理大内存的节点

	p = malloc(size);
	if (nullptr == p)
	{
		return nullptr;
	}

	n = 0;

	// 遍历当前大块内存管理链表的前3个节点,如果有空闲的管理节点,,则将申请的大内存地址给它
	// 否则就用申请小块内存的方式,新申请一个大块内存管理节点,头插法插入到大内存管理链表中
	// 因为申请小块内存时非常快捷,所以只遍历前3个现有节点,没有空闲节点就去申请小内存,避免浪费时间
	for (large = pool->large; large; large = large->next)
	{
		if (large->alloc == nullptr) 
		{
			large->alloc = p;
			return p;
		}

		if (n++ > 3)
		{
			break;
		}
	}

	// 从小块内存池中为大内存管理节点申请空间
	large = (ngx_pool_large_s*)ngx_palloc_small(sizeof(ngx_pool_large_s), 1);
	if (nullptr == large)
	{ // 如果管理节点的空间申请失败,则释放申请的大内存
		free(p);
		return nullptr;
	}

	// 头插法,将大内存管理节点large插入大内存管理链表
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;
	printf("alloc large mem %p, size %d\n", p, size);
	return p;
}

// 用于新申请一个小块内存池
void* ngx_mem_pool::ngx_palloc_block(size_t size)
{
	u_char* m;
	size_t psize;
	ngx_pool_s* p, * newpool;

	// size和第一个小块内存池相同
	psize = (size_t)(pool->d.end - (u_char*)pool);

	m = (u_char*)malloc(psize);
	if (nullptr == m)
	{
		return nullptr;
	}

	// 初始化小块内存池头部信息
	newpool = (ngx_pool_s*)m;
	newpool->d.end = m + psize;
	newpool->d.next = nullptr;
	newpool->d.failed = 0;

	m += sizeof(ngx_pool_data_t);
	m = ngx_align_ptr(m, NGX_ALIGNMENT);
	newpool->d.last = m + size;

	// 遍历链表上现有的小块内存池,更新current指针(记录链表第一个可以分配内存的小块内存池)
	for (p = pool->current; p->d.next; p = p->d.next)
	{
		if (p->d.failed++ > 4)
		{
			pool->current = p->d.next;
		}
	}
	p->d.next = newpool;

	return m;
}

// 用于释放大内存
void ngx_mem_pool::ngx_pfree(void* p)
{
	ngx_pool_large_s* l;
	for (l = pool->large; l; l = l->next)
	{
		// 释放大内存,并将其对应的管理节点指针置为nullptr
		if (p == l->alloc)
		{
			printf("free: %p\n", l->alloc);
			free(l->alloc);
			l->alloc = nullptr;
			return;
		}
	}
}

// 重置所有小块和大块内存池
void ngx_mem_pool::ngx_reset_pool()
{
	ngx_pool_s* p;
	ngx_pool_large_s* l;

	// 释放所有大内存
	for (l = pool->large; l; l = l->next)
	{
		if (l->alloc)
		{
			free(l->alloc);
		}
	}

	/****************************************
	* 这里逻辑与nginx源码不同,因为第一块小内存池不仅有头部信息,还记录整个内存池的管理成员信息,
	* 但其他小内存池只有头部信息,所以可用内存大于第一块小内存池,重置时需区分开,避免内存浪费
	****************************************/
	// 重置所有小内存池
	// 重置第一块小内存池
	pool->d.last = (u_char*)pool + sizeof(ngx_pool_s);
	pool->d.failed = 0;
	// 重置后面的小内存池
	for (p = pool->d.next; p; p = p->d.next)
	{
		p->d.last = (u_char*)p + sizeof(ngx_pool_s);
		p->d.failed = 0;
	}

	pool->current = pool;
	pool->large = nullptr;
}

// 销毁内存池
void ngx_mem_pool::ngx_destroy_pool()
{
	ngx_pool_s* p, * n;
	ngx_pool_large_s* l;
	ngx_pool_cleanup_s* c;

	// 调用用户注册的回调函数,清理外部内存资源,避免内存泄露
	for (c = pool->cleanup; c; c = c->next)
	{
		if (c->handler) {
			printf("run cleanup: %p\n", c);
			c->handler(c->data);
		}
	}

	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	for (p = pool, n = pool->d.next;/*void*/; p = n, n = n->d.next)
	{
		free(p);
		if (nullptr == n)
		{
			break;
		}
	}
}

// 给用户注册回调函数,用于在内存池销毁时清理用户自己申请的外部内存资源
ngx_pool_cleanup_s* ngx_mem_pool::ngx_pool_cleanup_add(size_t size)
{
	ngx_pool_cleanup_s* c;
	c = (ngx_pool_cleanup_s*)ngx_palloc(sizeof(ngx_pool_cleanup_s));
	if (nullptr == c)
	{
		return nullptr;
	}

	if (size)
	{
		c->data = ngx_palloc(size);
		if (nullptr == c->data)
		{
			return nullptr;
		}
	}
	else
	{
		c->data = nullptr;
	}

	c->handler = nullptr;

	c->next = pool->cleanup;
	pool->cleanup = c;

	printf("add cleanup\n");

	return c;
}

void* ngx_mem_pool::ngx_pnalloc(size_t size)
{
	if (size <= pool->max) {
		return ngx_palloc_small(size, 0);
	}

	return ngx_palloc_large(size);
}

void* ngx_mem_pool::ngx_pcalloc(size_t size)
{
	void* p;

	p = ngx_palloc(size);
	if (p) {
		ngx_memzero(p, size);
	}

	return p;
}

test.cpp

#include "ngx_mem_pool.h"
#include <cstring>
#include <cstdio>

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);
}
int main()
{
    ngx_mem_pool mempool;
    // 512 - sizeof(ngx_pool_s) - 4095   =>   max
    if (nullptr == mempool.ngx_create_pool(512))
    {
        printf("ngx_create_pool fail...");
        return -1;
    }

    void* p1 = mempool.ngx_palloc(128); // 从小块内存池分配的
    if (p1 == nullptr)
    {
        printf("ngx_palloc 128 bytes fail...");
        return -2;
    }

    Data* p2 = (Data*)mempool.ngx_palloc(512); // 从大块内存池分配的
    if (p2 == nullptr)
    {
        printf("ngx_palloc 512 bytes fail...");
        return -3;
    }
    p2->ptr = (char*)malloc(12);
    if (nullptr == p2->ptr)
    {
        return -4;
    }
    strcpy_s(p2->ptr, 12, "hello world");
    fopen_s(&p2->pfile, "data.txt", "w");

    ngx_pool_cleanup_s* c1 = mempool.ngx_pool_cleanup_add(sizeof(char*));
    c1->handler = (ngx_pool_cleanup_pt)func1;
    c1->data = p2->ptr;

    ngx_pool_cleanup_s* c2 = mempool.ngx_pool_cleanup_add(sizeof(FILE*));
    c2->handler = (ngx_pool_cleanup_pt)func2;
    c2->data = p2->pfile;

    // 也可以把ngx_destroy_pool放在ngx_mem_pool的析构函数里执行.
    mempool.ngx_destroy_pool(); // 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值