Linux高性能编程_nginx内存池

Linux高性能编程_nginx内存池

目录

大家好,这里是物联网心球。

上一篇文章Linux高性能编程_malloc原理,我们详细介绍了ptmalloc工作原理,了解到采用malloc,free分配内存存在频繁加锁和内存碎片等问题。

本文我们来分析nginx内存池,看一下nginx内存池能否解决以上问题。

1.nginx内存池实现原理

1.1 整体架构

Linux高性能编程_nginx内存池 图1

nginx内存池可以分为:小内存池和大内存池。

小内存池用于1-4096字节(内存池大小由用户自定义) 内存分配,大内存池用于大于4096字节内存分配。

学习nginx内存池,我们可以基于小内存池和大内存池管理来学习。

nginx内存池结构体定义:

struct ngx_pool_t {
    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;
    ngx_log_t      *log;
};

nginx内存池由ngx_pool_t结构体定义,结构体成员d(ngx_pool_data_t)用于管理小内存池,结构体成员large(ngx_pool_large_t)用于管理大内存池。

ngx_pool_data_t结构体定义:

typedef struct {
    u_char    *last; //小内存池起始地址
    u_char    *end; //小内存池结束地址
    ngx_pool_t *next; //下一个内存池
    ngx_uint_t failed; //失败次数,用于控制轮询次数
} ngx_pool_data_t;

ngx_pool_large_t结构体定义:

struct ngx_pool_large_t {
    ngx_pool_large_t *next; //单链表指针
    void         *alloc; //大内存地址
};

1)小内存池

Linux高性能编程_nginx内存池 图2

nginx通过ngx_memalign函数从系统申请4KB(由用户自定义 )内存空间实现一个内存池,4KB内存前56字节为内存池头部,剩余内存空间为内存池可用内存。

ngx_pool_t成员d(ngx_pool_data_t)包含last,end,next指针。

  • last指针:表示可用内存起始地址,每进行一次small内存分配后,last指针向后移动对应字节数。

  • end指针:表示可用内存结束地址,当last指针和end指针重合后,内存池被消耗完。

  • next指针:指向下一个ngx_pool_t内存池。

当小内存池内存不够用时,此时会创建一个新的内存池,旧内存池next成员指向新内存池。

Linux高性能编程_nginx内存池 图3

2)大内存池

当用户申请的内存大于4KB时,nginx无法从小内存池分配足够大的内存给用户,此时nginx通过malloc函数从系统申请一块大内存给用户。

新申请的大内存生成一个新的large节点,并插入large链表。

Linux高性能编程_nginx内存池 图4

1.2 内存池操作

1)创建内存池

 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    //申请小内存池
    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);
    //设置小内存池大小
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    //设置当前内存池
    p->current = p;
    return p;
}

通过ngx_memalign函数申请小块内存,将小块内存前56字节初始化为内存池头(ngx_pool_t),剩余内存为可用内存,将小内存池对应的指针成员指向可用内存,并设置小内存池大小。

2)分配小内存(small分配)

 void *ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char *m;
    ngx_pool_t  *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(pool, size);
}

通过内存池current成员找到当前可用内存池,以当前可用内存池为查询起始点轮询内存池链表,查找内存池是否有匹配内存块,如果有匹配内存块则分配内存,否则将创建新的内存池,从新的内存池分配内存。

3)分配大内存(large分配)

 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;
    //从large链表中查找一个空闲节点,将大内存块插入节点
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

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

    //如果没有空闲节点,申请一个新的节点
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    //将大内存块插入新的节点,并将新的节点插入large链表
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;}

大内存分配比较简单,直接通过malloc申请内存,并将申请的内存插入large链表。

4)内存回收

注意:nginx没有单独的小内存回收机制,只有在内存池销毁的时候才会回收小内存。

nginx为什么不设计小内存回收机制?

原因有以下几点:

  • 小内存回收机制设计比较复杂,实现难度高。

  • 小内存并不占用太多内存,可以延迟回收。

大内存回收代码如下:

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;

    //轮询large链表
    for (l = pool->large; l; l = l->next) {
if (p == l->alloc) {
    //查找到大内存地址
            ngx_free(l->alloc); //释放大内存
l->alloc = NULL;
//节点设置为空闲节点
            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

大内存回收比较简单,轮询large链表找到大内存地址所在的节点,释放大内存并把节点设置为空闲节点。

5)内存池销毁

 void ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t       *p, *n;
    ngx_pool_large_t  *l;
    ngx_pool_cleanup_t *c;

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

    //释放小内存池
    for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {
        ngx_free(p);
        if (n == NULL) break;
    }
}

nginx没有小内存回收机制,所以内存池销毁的时候,必须要释放小内存池,否则会出现内存泄露问题。

2.nginx内存池测试

nginx一直是高性能的代名词,nginx内存池也要满足高性能的要求。

一个高性能的内存池需要满足几个要求:

  • 无锁化

  • 较少的内存碎片

  • 高效内存申请和释放

从nginx内存池实现原理,我们可以发现nginx内存池存在以下几个缺点:

  • 缺点1: nginx内存池没有锁机制,不能保证多线程安全,所以nginx内存池不支持多线程。

  • 缺点2: nginx底层实现依赖于ptmalloc,所以依然存在内存碎片的问题。

  • 缺点3: nginx大块内存直接使用malloc函数申请,大块内存申请和释放和malloc、free没有区别。

nginx内存池高性能体现在哪里呢?

nginx内存池高性能主要体现在频繁申请和释放小块内存这一方面,ptmalloc内存池是整个程序共享一个ptmalloc内存池,而nginx根据实际的业务可以创建多个内存池,多个内存池独立工作,保证高效分配内存。

2.1 测试代码

测试项包括:无锁化测试和高效内存分配测试。

  • TEST_ITEM:选择测试项,0为无锁化测试,1为高效内存申请和释放测试。

  • WORK_THREAD_NUM:设置测试线程数量。

  • TEST_TIMES:表示总测试次数

  • ONCE_TIMES:分配内存次数,分配期间不释放内存。

  • MEM_SIZE:设置每次分配内存大小,超过内存池大小(SMALL_POOL_SIZE)会自动采用大内存分配方式。

  • ENABLE_POOL:使能nginx内存池,0:使用malloc,1:使用内存池

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "ngx_palloc.h"

#define TEST_ITEM (1) //测试项目
#define UNLOCK_TEST (0) //无锁化测试
#define HIGH_PERFORMANCE_TEST (1) //高效内存分配测试

#define WORK_THREAD_NUM (4) //线程数量
#define TEST_TIMES (10000) //测试总次数
#define ONCE_TIMES (10000) //分配内存次数,分配期间不释放内存
#define MEM_SIZE (100) //用户单次申请内存大小
#define SMALL_POOL_SIZE (4096) //内存池大小
#define ENABLE_POOL (1) //使能ngnix内存池 0:禁止 1:使能

typedef void* callback(void *);

void *unlock_test(void *arg) {
    printf("unlock test...\n");
    ngx_pool_t *pool = (ngx_pool_t *)arg;

    while (1) {
        for (int i = 0; i < TEST_TIMES; i++) {
            for (int j = 0; j < ONCE_TIMES; j++) {
#if ENABLE_POOL
                char *p = ngx_palloc(pool, MEM_SIZE);
                ngx_pfree(pool, p);
#else
                char *p = malloc(MEM_SIZE);
                free(p);
#endif
            }
        }
    }

    return NULL;
}

void *high_performance_test(void *arg) {
    printf("high performance test...\n");

    char *ptr[ONCE_TIMES] = {0};

#if ENABLE_POOL
    printf("memory pool mode\n");
    for (int i = 0; i < TEST_TIMES; i++) {
        ngx_pool_t *pool = ngx_create_pool(SMALL_POOL_SIZE);
        if (!pool) {
            printf("create pool error\n");
            return NULL;
        }
        for (int j = 0; j < ONCE_TIMES; j++) {
            char *p = ngx_palloc(pool, MEM_SIZE);
        }
        ngx_destroy_pool(pool);
    }
#else
    printf("malloc mode\n");
    for (int i = 0; i < TEST_TIMES; i++) {
        for (int j = 0; j < ONCE_TIMES; j++) {
            ptr[j] = malloc(MEM_SIZE);
            if (!ptr[j]) {
                printf("malloc error");
                return NULL;
            }
        }
        for (int j = 0; j < ONCE_TIMES; j++) {
            if (ptr[j]) {
                free(ptr[j]);
                ptr[j] = NULL;
            }
        }
    }
#endif
}

int main(int argc, char *argv[]) {
    callback *cb = NULL;
    void *arg = NULL;

#if (TEST_ITEM == UNLOCK_TEST)
    cb = unlock_test;
    ngx_pool_t *pool = ngx_create_pool(SMALL_POOL_SIZE);
    if (!pool) {
        printf("create pool error\n");
        return -1;
    }
    arg = (void *)pool;
#else
    cb = high_performance_test;
    arg = NULL;
#endif

    pthread_t th[WORK_THREAD_NUM];
    for (int i = 0; i < WORK_THREAD_NUM; i++) {
        pthread_create(&th[i], NULL, cb, arg);
    }

    for (int i = 0; i < WORK_THREAD_NUM; i++) {
        pthread_join(th[i], NULL);
    }

    return 0;
}

2.2 测试结果

1)无锁化测试

Linux高性能编程_nginx内存池 图5

多个线程同时操作同一个内存池,频繁申请和释放大块内存,程序出现double free并退出。所以nginx内存池不支持多线程同时访问。 2)高效内存分配测试 nginx内存池和malloc,free进行频繁申请和释放100字节内存对比测试。通过time命令统计程序执行的时间。 Linux高性能编程_nginx内存池 图6

nginx内存池测试结果–>:

Linux高性能编程_nginx内存池 图7

实际时间9.8秒,用户时间22秒,系统时间15秒。

malloc,free测试结果–>:

Linux高性能编程_nginx内存池 图8

实际时间21秒,用户时间1分23秒,系统时间0秒。nignx内存池效率高于malloc,free。

← 返回文章列表