手写Nginx内存池移植项目 |施磊C++ | 项目实战

手写Nginx内存池移植项目

可配合这篇博客一起学习
剖析Nginx的内存池源码 | 施磊 | C++项目实战-优快云博客

注意点

1.所有需要传入内存池入口指针(pool)的地方都不用传入了,因为私有变量pool_存储的就是内存池入口指针

2.在C++中,void* 指针是一个通用指针类型,它可以指向任何类型的数据。然而,由于C++是一种类型安全的语言,直接将 void* 转换为其他类型的指针(或反之)而不进行显式转换(即“强转”)通常是不被允许的,或者至少是不被推荐的。

0.预编译文件

//pch.h
#pragma once
#ifndef PCH_H
#define PCH_H

#endif
//pch.cpp
#include"pch.h"

1.ngx_mem_pool.h 变量和接口函数定义

#pragma once
#include<stdlib.h>
#include<memory.h>
/*
移植nginx内存池的代码,用oop来实现
*/

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

//类型前置声明
struct ngx_pool_s;

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

//大块内存的头部信息
struct ngx_pool_large_s {
	ngx_pool_large_s* next;//所有的大块内存分配被串在一条链表上
	void* alloc;//保存分配出去的大块内存的起始地址
};

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

// nginx内存池头部信息和管理成员信息
struct ngx_pool_s {
	ngx_pool_data_t d;  // 存储的是当前小块内存池的使用情况
	size_t max;  // 小块内存分配的最大值 小块和大块内存的分界线
	ngx_pool_s* current;  // 小块内存池入口指针 指向第一个可以分配的小块内存池,每次分配都是从这个指针所指的内存池开始分配的
	ngx_pool_large_s* large; // 大块内存的入口地址(大块也在一条链表上)
	ngx_pool_cleanup_s* cleanup;
	/*指向所有预置的清理函数handler回调函数的入口(串在一条链表上) 用户可以设置回调函数,
	在内存池释放之前,可以对内存池上的数据进行一个释放操作。相当于析构函数的角色。
	*/	
};
//把d调整到邻近的a的倍数上
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
//小块内存分配考虑字节对齐时的单位
#define NGX_ALIGNMENT   sizeof(unsigned long)
//把指针p调整到a邻近的倍数
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
//buf缓冲区清0(初始化为0)
#define ngx_memzero(buf, n)       (void) memset(buf, 0, n)

const int ngx_pagesize = 4096;
/*能从内存池分配到的最大内存 4096-1=4095(4k) 就是32位操作系统一个页面的大小
这是nginx内存池区分大小块内存的一个分界线,比SGI STL内存池的128字节要大得多,在SGI STL中,只要比128字节大就去调用malloc而不是内存池
*/
const int NGX_MAX_ALLOC_FROM_POOL = ngx_pagesize - 1;
//默认的内存池的大小最大为 16k
const int NGX_DEFAULT_POOL_SIZE = 16 * 1024;
//小块内存池大小按照16字节进行对齐
const int NGX_POOL_ALIGNMENT = 16;
//小块内存池最小的大小   gnx_align是把数字调整到邻近的16的倍数上
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:
	//创建指定size大小的内存池,但小块内存池不超过一个页面的大小
	void* ngx_create_pool(size_t size);
	//考虑内存字节对齐,从内存池申请size大小的内存
	void* ngx_palloc(size_t size);
	//和上面的函数一样,不考虑内存字节对齐
	void* ngx_pnalloc(size_t size);
	//调用的是ngx_palloc实现内存分配,但是会初始化0
	void* ngx_pcalloc(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内存池的入口指针
	ngx_pool_s* pool;
	//小块内存分配
	void* ngx_palloc_small(size_t size, ngx_uint_t align);
	//分配新的小块内存池
	void* ngx_palloc_block(size_t size);
	//分配大块内存池
	void* ngx_palloc_large(size_t size);
};

2.ngx_mem_pool.cpp 接口具体实现

#include"pch.h"
#include"ngx_mem_pool.h"


//创建指定size大小的内存池,但小块内存池不超过一个页面的大小
void* ngx_mem_pool::ngx_create_pool(size_t size)
{
    ngx_pool_s* p;
    //根据用户指定的大小开辟内存 根据不同的平台调用不同的API 如果没有指定平台,那调用的就是malloc函数
    p = (ngx_pool_s*)malloc(size);
    //申请失败 退出
    if (p == nullptr) {
        return nullptr;
    }
    //last指向的是用户可以使用的内存空间的首地址(除了内存池的头结构体部分的其他内容) end指向的是这块的末尾地址 size是传入的要申请的大小 在图中表示为log末尾到内存池末尾这一块
    p->d.last = (u_char*)p + sizeof(ngx_pool_s);
    p->d.end = (u_char*)p + size;
    //初始化
    p->d.next = nullptr;
    p->d.failed = 0;
    //这个size是我们用户实际上可以使用的大小 申请的大小减去内存池记录信息所用的数据头的大小
    size = size - sizeof(ngx_pool_s);
    //如果比一个页面小,那就用size,否则就用一个页面的大小
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
    //初始化
    p->current = p;//指向本内存池的起始地址
    p->large = nullptr;
    p->cleanup = nullptr;

    pool = p;//封装后对私有变量要赋值的

    return p;
}


//考虑内存字节对齐,从内存池申请size大小的内存
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_pnalloc(size_t size)
{
    if (size <= pool->max) {
        return ngx_palloc_small(size, 0);
    }
    return ngx_palloc_large(size);
}
//调用的是ngx_palloc实现内存分配,但是会初始化0
void* ngx_mem_pool::ngx_pcalloc(size_t size)
{
    void* p;

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

    return p;
}


//小块内存分配
void* ngx_mem_pool::ngx_palloc_small(size_t size, ngx_uint_t align)
{
    u_char* m;
    ngx_pool_s* p;

    //p指向传入内存池的起始地址
    p = pool->current;

    do {
        //指向可分配内存的起始地址
        m = p->d.last;
        //考虑内存对齐
        if (align) {
            //根据平台调整字节对齐,调整到4或8字节的倍数上去
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
        //目前可分配内存(空闲内存)比要申请的内存要大 就可以分配 然后更新last即空闲内存起始地址
        if ((size_t)(p->d.end - m) >= size) {
            p->d.last = m + size;

            return m;//m是已经分配好的内存空间的首地址 返回这个指针给用户
        }

        p = p->d.next;//这个内存池大小不够就去下一个,没有下一个内存池的话就会退出循环

    } while (p);
    //没有分配成功就会进入这个函数
    return ngx_palloc_block(size);
}
//分配新的小块内存池
void* ngx_mem_pool::ngx_palloc_block(size_t size)
{
    u_char* m;
    size_t       psize;
    ngx_pool_s* p, * newpool;
    //计算当前整个内存池的大小
    psize = (size_t)(pool->d.end - (u_char*)pool);
    //再开辟另一块大小一模一样的内存池
    m =(u_char*)malloc(psize);
    if (m == nullptr) {
        return nullptr;
    }
    //记录内存池起始地址
    newpool = (ngx_pool_s*)m;
    /*初始化内存池结构体
    从第二块内存池开始 只需要ngx_pool_data_t里面的四个指针来管理,从max到log全都不需要了,存在第一块内存池里面就行了,剩下的只要这四个必要的指针就行
    */
    newpool->d.end = m + psize;
    newpool->d.next = nullptr;//这是新创建的,就相当于链表最后一个结点,next域就是nullptr
    newpool->d.failed = 0;

    m += sizeof(ngx_pool_data_t);
    //字节对齐 调整到4或8的倍数
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    newpool->d.last = m + size;//更新内存池的last指针,指向可用内存空间(空闲内存)的首地址

    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_palloc_large(size_t size)
{
    void* p;
    ngx_uint_t         n;
    ngx_pool_large_s* large;
    //直接调用malloc开辟内存
    p = malloc(size);
    if (p == nullptr) {
        return nullptr;
    }

    n = 0;
    //先看下面的pfree函数再看这个,遍历大块内存池链表,找到alloc是空的,把新分配的这块给安置到这里
    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 (large == nullptr) {
        free(p);
        return nullptr;
    }
    //记录大块内存的起始地址
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}
//大块内存释放
void ngx_mem_pool::ngx_pfree(void* p)
{
    ngx_pool_large_s* l;

    for (l = pool->large; l; l = l->next) {
        if (p == l->alloc) {
            free(l->alloc);
            l->alloc = NULL;
            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);
        }
    }
    /*遍历小块内存 这个处理的不太好,因为除了第一块结构体是完整的(从开始到log),
    剩下的内存池结构体那块都是只有四个指针 这样释放内存相当于多释放了除开四个指针的多余部分。表现就是从第二块开始的内存池有一小部分不能使用,但是可以使用,不会报错。
    
    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char*)p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }*/

    //修改后的版本
    p = pool;
    //处理第一个内存池
    p->d.last = (u_char*)p + sizeof(ngx_pool_s);
    p->d.failed = 0;

    //从第二个开始循环到最后一个 只需要释放那四个指针,即ngx_pool_data_t就行
    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char*)p + sizeof(ngx_pool_data_t);
        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;
    //1.回调函数不为空先调回调函数 这样就把外部资源释放掉了
    for (c = pool->cleanup; c; c = c->next) {
        if (c->handler) {
            c->handler(c->data);
        }
    }
    //2.大块
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            free(l->alloc);
        }
    }
    //3.小块
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        free(p);

        if (n == nullptr) {
            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 (c == nullptr) {
        return nullptr;
        }
    //如果预置的回调函数不需要参数,那就写0,如果有参数就写需要的内存大小,然后再开辟相应的内存,然后data指向它,如果不需要参数那就是nullptr
    if (size) {
        c->data = ngx_palloc(size);
        if (c->data == nullptr) {
            return nullptr;
        }

    }
    else {
        c->data = nullptr;
    }
    //暂时置为空,还没有具体的值
    c->handler = nullptr;
    //链表的连接动作,把新创建的cleanup连接到链表上(next指向的是下一个清理操作)
    c->next = pool->cleanup;

    pool->cleanup = c;
    return c;
}


3.textNginxPool.cpp 测试文件

#include"pch.h"
#include"ngx_mem_pool.h"
#include<stdio.h>
#include<string.h>

//大块内存池里面放了两个指针将要指向外部资源
typedef struct Data stData;
struct Data
{
    char* ptr;
    FILE* pfile;
};
//两个回调函数 堆内存资源和文件资源
void func1(void* p1)
{
    char* p = (char*)p1;
    printf("free ptr mem!");
    free(p);
}
void func2(FILE* pf1)
{
    FILE *pf=(FILE*) pf1;
    printf("close file!");
    fclose(pf);
}
int main()
{
    // 最大值设置为512 create_pool可以直接实现在mempool的构造函数
    ngx_mem_pool mempool;

    if (mempool.ngx_create_pool(512) == nullptr)
    {
        printf("ngx_create_pool fail...");
        return -1;
    }

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

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

    ngx_pool_cleanup_s* c1 = mempool.ngx_pool_cleanup_add(sizeof(char*));
    c1->handler = 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;

    //可以直接实现在mempool的析构函数
    mempool.ngx_destroy_pool(); // 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存

    return 0;
}

4.改进

1.可以将create_pool放在构造函数中,将destroy_pool放在析构函数中,进一步进行封装

2.回调函数部分可以用bind绑定器或者function函数对象来实现

笔者只实现了第一点,实现第二点时,老是报错(读取访问权限冲突。 std::_Func_class<void,void * __ptr64>::_Getimpl(…) 返回 0xFFFFFFFFFFFFFFFF。完了写好之后再更新此文章)

pool.h

//pool.h
#pragma once
#include<stdlib.h>
#include<memory.h>
#include<iostream>
using namespace std;
/*
移植nginx内存池的代码,用oop来实现
*/

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

//类型前置声明
struct ngx_pool_s;

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

//大块内存的头部信息
struct ngx_pool_large_s {
	ngx_pool_large_s* next;//所有的大块内存分配被串在一条链表上
	void* alloc;//保存分配出去的大块内存的起始地址
};

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

// nginx内存池头部信息和管理成员信息
struct ngx_pool_s {
	ngx_pool_data_t d;  // 存储的是当前小块内存池的使用情况
	size_t max;  // 小块内存分配的最大值 小块和大块内存的分界线
	ngx_pool_s* current;  // 小块内存池入口指针 指向第一个可以分配的小块内存池,每次分配都是从这个指针所指的内存池开始分配的
	ngx_pool_large_s* large; // 大块内存的入口地址(大块也在一条链表上)
	ngx_pool_cleanup_s* cleanup;
	/*指向所有预置的清理函数handler回调函数的入口(串在一条链表上) 用户可以设置回调函数,
	在内存池释放之前,可以对内存池上的数据进行一个释放操作。相当于析构函数的角色。
	*/	
};
//把d调整到邻近的a的倍数上
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
//小块内存分配考虑字节对齐时的单位
#define NGX_ALIGNMENT   sizeof(unsigned long)
//把指针p调整到a邻近的倍数
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
//buf缓冲区清0(初始化为0)
#define ngx_memzero(buf, n)       (void) memset(buf, 0, n)

const int ngx_pagesize = 4096;
/*能从内存池分配到的最大内存 4096-1=4095(4k) 就是32位操作系统一个页面的大小
这是nginx内存池区分大小块内存的一个分界线,比SGI STL内存池的128字节要大得多,在SGI STL中,只要比128字节大就去调用malloc而不是内存池
*/
const int NGX_MAX_ALLOC_FROM_POOL = ngx_pagesize - 1;
//默认的内存池的大小最大为 16k
const int NGX_DEFAULT_POOL_SIZE = 16 * 1024;
//小块内存池大小按照16字节进行对齐
const int NGX_POOL_ALIGNMENT = 16;
//小块内存池最小的大小   gnx_align是把数字调整到邻近的16的倍数上
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_mem_pool(size_t size)
	{

		if (this->ngx_create_pool(size) == nullptr)
		{
			cout << "ngx_create_pool fail..." << endl;
			exit(0);
		}
	}
	//考虑内存字节对齐,从内存池申请size大小的内存
	void* ngx_palloc(size_t size);
	//和上面的函数一样,不考虑内存字节对齐
	void* ngx_pnalloc(size_t size);
	//调用的是ngx_palloc实现内存分配,但是会初始化0
	void* ngx_pcalloc(size_t size);
	//释放大块内存
	void ngx_pfree(void* p);
	//内存重置函数
	void ngx_reset_pool();

	//添加回调清理操作函数
	ngx_pool_cleanup_s* ngx_pool_cleanup_add(size_t size);

	~ngx_mem_pool()
	{
		this->ngx_destroy_pool();
	}
private:
	//指向ngx内存池的入口指针
	ngx_pool_s* pool;
	//小块内存分配
	void* ngx_palloc_small(size_t size, ngx_uint_t align);
	//分配新的小块内存池
	void* ngx_palloc_block(size_t size);
	//分配大块内存池
	void* ngx_palloc_large(size_t size);
	//创建指定size大小的内存池,但小块内存池不超过一个页面的大小
	void* ngx_create_pool(size_t size);
	//内存池的销毁函数
	void ngx_destroy_pool();
};

pool.cpp

这个没改动

text.cpp

#include"pch.h"
#include"ngx_mem_pool.h"
#include<stdio.h>
#include<string.h>

//大块内存池里面放了两个指针将要指向外部资源
typedef struct Data stData;
struct Data
{
    char* ptr;
    FILE* pfile;
};
//两个回调函数 堆内存资源和文件资源
void func1(void* p1)
{
    char* p = (char*)p1;
    printf("free ptr mem!");
    free(p);
}
void func2(FILE* pf1)
{
    FILE* pf = (FILE*)pf1;
    printf("close file!");
    fclose(pf);
}
int main()
{
    // 最大值设置为512 create_pool可以直接实现在mempool的构造函数
    ngx_mem_pool mempool(512);

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

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

    ngx_pool_cleanup_s* c1 = mempool.ngx_pool_cleanup_add(sizeof(char*));
    c1->handler = 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;

    return 0;
}



bind和function的改进还没完成(笔者标注一下不要在意)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值