高并发内存池设计_内存池

1. 常用的内存操作函数

void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size); 
void free(void *ptr);

malloc 在内存的动态存储区中分配一块长度为size字节连续区域返回该区域的首地址.

callocmalloc相似,参数size为申请地址的单位元素长度,nmemb为元素个数,即在内存中申请nmemb*size字节大小的连续地址空间.内存会初始化0

realloc 给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度.ptr 若为NULL,它就等同于 malloc.

2. 高性能内存池设计_弊端解决之道

弊端一

高并发时较小内存块使用导致系统调用频繁降低了系统的执行效率
在这里插入图片描述

time  可执行文件
可以查看用户态调用的时间和内核态调用的时间
user:用户态
sys:内核态

在这里插入图片描述

弊端二

频繁使用时增加了系统内存的碎片降低内存使用效率
在这里插入图片描述
每次malloc分配的内存位置都是随机的,也就是每次分配的内存地址不是连续的,这就导致:如果多次调用malloc,每两次调用的malloc分配的内存中间就会产生大量的内存碎片,如下图:
在这里插入图片描述

弊端三

没有垃圾回收机制,容易造成内存泄漏,导致内存枯竭

情形一:
void log_error(char *reason) { 
    char *p1; 
    p1 = malloc(100); 
    sprintf(p1,"The f1 error occurred because of '%s'.", reason); 
    log(p1); 
    //free(p1);不调用free
}
情形二:  
int getkey(char *filename) { 
    FILE *fp; 
    int key; 
    fp = fopen(filename, "r");
    fscanf(fp, "%d", &key); 
    //fclose(fp);不调用fclose
    return key; 
}

弊端四

内存分配与释放的逻辑在程序中相隔较远时,降低程序的稳定性

程序员A写的
ret get_stu_info(  Student  * _stu  ) { 
    char  * name= NULL; 
    name = getName(_stu->no);//程序员B写的getName的返回值根本不是动态分配的
    //处理逻辑
    if(name) {
        free(name);
        name = NULL;
    }
}
程序员B写的
char stu_name[MAX];

char * getName(int stu_no){
    
    //查找相应的学号并赋值给 stu_name
     snprintf(stu_name,MAX,%s”,name);
     return stu_name;
}

3. 弊端解决之道

内存管理维度分析

最佳的解决办法:自己写内存池
在这里插入图片描述

内存管理组件选型

在这里插入图片描述

4. 高并发内存管理最佳实践

内存池技术

什么是内存池技术?
在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,统一对程序所使用的内存进行统一的分配和回收。这样做的一个显著优点是,使得内存分配效率得到很大的提升。

在这里插入图片描述

内存池如何解决弊端?

在这里插入图片描述

高并发时内存池如何实现?

在这里插入图片描述

5. 高效内存池设计和实现

eg:
// main.c
#include "mem_core.h"

#define BLOCK_SIZE 16 // 每次分配内存块大小

#define MEM_POOL_SIZE (1024 * 4) // 内存池每块大小

int main(int argc, char **argv)
{
	int i = 0, k = 0;
	int use_free = 0;

	ngx_pagesize = getpagesize(); // 设置页的大小
	// printf("pagesize: %zu\n",ngx_pagesize);

	if (argc >= 2)
	{ // 带有参数use malloc/free
		use_free = 1;
		printf("use malloc/free\n");
	}
	else
	{ // 没带参数use mempool
		printf("use mempool.\n");
	}

	if (!use_free)
	{ // 如果使用内存池
		char *ptr = NULL;

		for (k = 0; k < 1024 * 500; k++)
		{
			ngx_pool_t *mem_pool = ngx_create_pool(MEM_POOL_SIZE); // 定义一个池子

			for (i = 0; i < 1024; i++)
			{
				ptr = ngx_palloc(mem_pool, BLOCK_SIZE);

				if (!ptr)
					fprintf(stderr, "ngx_palloc failed. \n");
				else
				{
					*ptr = '\0';
					*(ptr + BLOCK_SIZE - 1) = '\0';
				}
			}

			ngx_destroy_pool(mem_pool); // 用完了释放这个池子
		}
	}
	else
	{
		char *ptr[1024];
		for (k = 0; k < 1024 * 500; k++)
		{
			for (i = 0; i < 1024; i++)
			{
				ptr[i] = malloc(BLOCK_SIZE); // 使用malloc分配内存
				if (!ptr[i])
					fprintf(stderr, "malloc failed. reason:%s\n", strerror(errno));
				else
				{
					*ptr[i] = '\0';
					*(ptr[i] + BLOCK_SIZE - 1) = '\0';
				}
			}

			for (i = 0; i < 1024; i++)
			{
				if (ptr[i])
					free(ptr[i]);
			}
		}
	}
	return 0;
}

在这里插入图片描述

实现思路 (分而治之) (重点)

  • 对于每个请求或者连接都会建立相应的内存池,建立好内存池之后,我们可以直接从内存池中申请所需要的内存,不用去管内存的释放,当内存池使用完成之后一次性销毁内存池。

  • 区分大小内存块的申请和释放,大于池尺寸的定义为大内存块使用单独的大内存块链表保存,即时分配和释放小于等于池尺寸的定义为小内存块,直接从预先分配的内存块中提取,不够就扩充池中的内存,在生命周期内对小块内存不做释放,直到最后统一销毁。(重点)

对应到代码中的这个函数:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) //size <= 数据块,分配小内存块
    {
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size); // 分配大内存块
}

Nginx 内存池结构图

在这里插入图片描述

关键数据结构

在这里插入图片描述

ngx_pool_t 结构示意图 (大小为1024的池

current指针指向的是当前从如果来需求了,去哪里去找要分配的内存;
failed是:从current指向的那个块里面,"寻找要分配的内存"的失败的次数,如果失败次数>x(系统设定的值),current就指向下一个块,开始从下一个块中寻找要分配的内存.
在这里插入图片描述
再特别说明一下:
在这里插入图片描述

Nginx 内存池基本操作

在这里插入图片描述

Nginx 内存池基本操作

在这里插入图片描述

我的疑问

在mem_pool_palloc.c中,有三个函数:请问ngx_palloc_block存在的意义是什么?ngx_palloc_small,ngx_palloc_large不能干完所有活吗?

ngx_palloc_block 函数的存在是为了在内存池中分配一块指定大小的内存块,并将其插入到内存池的节点中。在内存池中,每个节点都包含了一个指向内存块的指针和一个指向下一个节点的指针,用于管理分配的内存块。因此,ngx_palloc_block 函数的作用是为内存池添加一个新的节点,以便于后续的内存分配和管理。

虽然 ngx_palloc_small 和 ngx_palloc_large 函数都可以在内存池中分配内存,但它们的主要目的是分配小的和较大的内存块而不是添加新的节点。如果需要在内存池中分配一块指定大小的内存块,并将其插入到内存池的节点中,就需要使用 ngx_palloc_block 函数。例如,在 Nginx 中,每个 HTTP 请求都会创建一个内存池,用于管理请求相关的内存分配,其中就会使用 ngx_palloc_block 函数来添加新的节点,以便于后续的内存分配和管理。因此,ngx_palloc_block 函数是内存池中重要的一部分,可以提高内存的使用效率和程序的性能。

需要注意的是,ngx_palloc_block 函数适合分配小的内存块,因为它会在内存池中新建一个节点来管理分配的内存块,如果分配的内存块较大,就会导致内存池中节点的数量增加,可能会影响内存池的性能。而对于较大的内存块,可以使用 ngx_palloc_large 函数直接从系统中分配内存,然后将其插入到内存池的 large 链表中,避免增加节点数量,同时也可以提高内存使用效率。因此,在实际使用中,需要根据需要选择合适的内存分配函数,以提高内存的使用效率和程序的性能。

也就是说,ngx_palloc_block是用来添加新节点的,而ngx_palloc_small,ngx_palloc_large是在已有节点上分配内存.

附上关键代码:与下面这张图对应在这里插入图片描述


// mem_pool_palloc.c
/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Nginx, Inc.
 */

#include "mem_core.h"

static inline void *ngx_palloc_small(ngx_pool_t *pool, size_t size,
                                     ngx_uint_t align);       // ngx_palloc_small 函数用于在内存池中分配小的内存块,具体大小由 size 参数指定
static void *ngx_palloc_block(ngx_pool_t *pool, size_t size); // ngx_palloc_block 函数用于在内存池中分配一块指定大小的内存块。
static void *ngx_palloc_large(ngx_pool_t *pool, size_t size); // 函数用于在内存池中分配较大的内存块,具体大小由 size 参数指定

ngx_pool_t *
ngx_create_pool(size_t size)
{
    ngx_pool_t *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size); // 只申请了一块内存
    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; // 返回第一块内存的头结点
}

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);
        }
    }*/

#if (NGX_DEBUG)

    /*
     * we could allocate the pool->log from this pool
     * so we cannot use this log while free()ing the pool
     */

    for (l = pool->large; l; l = l->next)
    {
        fprintf(stderr, "free: %p", l->alloc);
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next)
    {
        fprintf(stderr, "free: %p, unused: %zu", p, p->d.end - p->d.last);

        if (n == NULL)
        {
            break;
        }
    }

#endif

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

void ngx_reset_pool(ngx_pool_t *pool) // 该函数用于将内存池中的所有节点重置为初始状态
{
    ngx_pool_t *p;
    ngx_pool_large_t *l;
    // 该函数会遍历内存池的 large 链表,释放掉所有已经分配的大内存块。然后,会遍历内存池的所有节点,将节点的 last 指针置为节点头部之后的位置,将节点的 failed 计数器置为 0,以便于后续的内存分配和管理可以重用节点。
    for (l = pool->large; l; l = l->next)
    {
        if (l->alloc)
        {
            ngx_free(l->alloc);
        }
    }

    for (p = pool; p; p = p->d.next)
    {
        p->d.last = (u_char *)p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }

    pool->current = pool;
    // pool->chain = NULL;
    pool->large = NULL;
}

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) //<=数据块,分配小内存块
    {
        return ngx_palloc_small(pool, size, 1); // 第三个参数为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); // 第三个参数为0,表示不对齐
    }
#endif

    return ngx_palloc_large(pool, size);
}

/* 该函数会先在内存池的 current 节点中查找是否有足够大小和对齐条件的空闲内存块。
如果 current 节点中的空闲内存块不足或者不满足对齐条件,就会遍历内存池的其他节点,直到找到合适的空闲内存块或者遍历完所有节点。
如果遍历完所有节点后仍没有找到合适的空闲内存块,则会调用 ngx_palloc_block 函数添加新的节点,并从新的节点中分配内存块。
*/
static inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char *m;
    ngx_pool_t *p;
    // ngx_palloc_small 函数会先获取 current 节点的 last 指针,该指针指向了当前节点上可以分配的内存空间的起始地址。
    p = pool->current;

    do
    {
        m = p->d.last; // last是能分配内存的起始位置
                       // align如果是1,说明有对齐要求
        if (align)     // 如果有对齐要求,则会调用 ngx_align_ptr 函数将起始地址向上对齐,以保证分配的内存块满足对齐条件。
        {
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
        // 函数会检查当前节点上剩余的空闲内存空间是否足够分配所需大小的内存块,如果足够,则将 last 指针后移,分配内存块,并返回内存块的起始地址。否则,函数会继续遍历下一个节点,直到遍历完所有节点或者找到合适的空闲内存块为止。
        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(pool, size);
}

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char *m;           // m 用于存储分配的内存块的首地址
    size_t psize;        // psize 存储当前内存池的大小
    ngx_pool_t *p, *new; // p 和 new 分别表示内存池的节点(ngx_pool_t)。
    // 计算内存池的大小 psize,其中 psize = (size_t)(pool->d.end - (u_char *)pool),pool->d.end 表示内存池结束的位置,(u_char *)pool 表示将内存池的首地址转换为 unsigned char 类型的指针,两者相减得到内存池的大小。
    psize = (size_t)(pool->d.end - (u_char *)pool);
    // 调用 ngx_memalign 函数分配一块大小为 psize 的内存块,并指定对齐方式为 NGX_POOL_ALIGNMENT。如果分配失败,则返回 NULL。
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize);
    if (m == NULL)
    {
        return NULL;
    }
    // 将分配的内存块地址转换为 ngx_pool_t 类型,并将其赋值给 new 变量。然后,初始化 new 节点的成员变量,包括 d.end、d.next 和 d.failed。
    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 移动到 new 节点的 d 成员变量之后,m = ngx_align_ptr(m, NGX_ALIGNMENT) 表示将 m 对齐到 NGX_ALIGNMENT 的倍数。
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size; // 将 new->d.last 设置为 m + size,表示分配的内存块的结束位置。
    // 从内存池的头节点 pool->current 开始遍历所有节点,如果某个节点的 d.failed 大于 4,则将 pool->current 设置为该节点的下一个节点,这个操作的目的是为了优化内存池的性能。
    for (p = pool->current; p->d.next; p = p->d.next)
    {
        if (p->d.failed++ > 4)
        {
            pool->current = p->d.next;
        }
    }
    // 将 new 节点插入到内存池的尾部。
    p->d.next = new;
    // 返回分配的内存块的起始地址 m
    return m;
}

static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void *p;
    ngx_uint_t n;
    ngx_pool_large_t *large;
    // 该函数会先调用 ngx_alloc 函数从系统中分配一块大小为 size 的内存块,如果分配成功,则会遍历内存池的 large 链表,查找是否有已经被释放的大内存块节点。如果有,就将该节点重用,并将分配的内存块地址赋值给该节点的 alloc 指针,然后返回分配的内存块地址。否则,会新建一个大内存块节点,并将其插入到内存池的 large 链表的头部,然后返回分配的内存块地址。
    p = ngx_alloc(size);
    if (p == NULL)
    {
        return NULL;
    }

    n = 0;

    for (large = pool->large; large; large = large->next) // 第一次来的时候不执行,因为第一次来,large不为真
    {
        if (large->alloc == NULL) // 如果有大内存块已经被释放了,就重用这个节点,这里与ngx_pfree的用法相对应
        {
            large->alloc = p;
            return p;
        }            // 注意:这里的重用指的是重用内存池中已经被释放的大内存块节点。而 ngx_palloc_small 函数中的重用是指重复使用已经分配过的小内存块,这两种重用的对象和实现方式都不同。
                     // 因为重用的是"已经释放"的节点,所以不管是否重用,我们在这个函数开头都需要使用ngx_alloc来重新分配一段内存
        if (n++ > 3) // 找重用节点,重复找3次,找不到就break
        {
            break;
        }
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); // 分配一个大内存块
    if (large == NULL)
    {
        ngx_free(p);
        return NULL;
    }

    // 链表的头插法
    large->alloc = p; // alloc:大内存块的起始地址
    large->next = pool->large;
    pool->large = large;

    return p;
}

void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
    void *p;
    ngx_pool_large_t *large;

    p = ngx_memalign(alignment, size);
    if (p == NULL)
    {
        return NULL;
    }

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

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) // 如果要是否的p是l指向的这个大内存块的起始地址
        {
            fprintf(stderr, "free: %p", l->alloc);
            ngx_free(l->alloc); // 释放掉
            l->alloc = NULL;    // 并置为空,后面就可以重用了

            return NGX_OK;
        }
    }

    return NGX_DECLINED; // ngx延迟释放
}

void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
    void *p;

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

    return p;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

踏过山河,踏过海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值