• Nginx源码分析--内存池


    1.问题引入

    使用C语言编程时,一般使用malloc和free进行动态内存申请和释放。如果一不小心忘记了调用free进行释放,很容易造成内存泄露。另一方面,频繁地进行malloc和free操作,很容易造成内存碎片。与此同时,因为malloc支持多线程同时操作,所以,使用同步锁是不可避免的。当然,根据malloc的实现原理,线程在进行malloc操作的时候,如果不能获得同步锁,就会另外在进程的heap区域开辟一段子区域进行内存申请,这样有效地避免长时间等待。但是频繁尝试去获得锁也需要一定的时间开销。(建议阅读这一篇文章)

    2.问题解决

    NGINX是一个对性能要求很高的系统。大到架构设计,小到细节实现都对性能提升做了充分的考虑。所以,对应内存管理,除了封装了libc库中的malloc和free操作以外。NGINX也实现了自己的内存管理系统,有效地减少了内存碎片的产生,降低了内存泄露发生的概率,减少了同步操作(尝试)的次数。这些都对NGINX本身性能的提升有一定的帮助。

    NGINX自身的内存管理系统最重要的特点就是加强中央集权管理。把内存的申请,释放牢牢地掌握在自己手里。具体来说就是:

    NGINX本身维护自己的内存池。当进程申请内存时,先在自身内存池中里去查找,如果找到直接返回。如果所有的内存池都找不到合适的内存,NGINX本身再去向系统去申请一片大内存进行分割管理。这样,有效地减少了系统调用malloc的次数。每次都是相对大片的内存申请,也有效地减少了内存碎片的发生几率。

    内存释放进行统一管理。NGINX的内存池提供了大小两种类型的内存片管理。对应小块内存只能在整个内存池销毁时候才能释放。这样可以减少内存泄露的发送几率。

    3.数据结构分析

    3.1内存池数据块结构

    1. typedef struct {
    2. u_char *last;
    3. u_char *end;
    4. ngx_pool_t *next;
    5. ngx_uint_t failed;
    6. } ngx_pool_data_t;

    结构定义:

    • 指针last 指向当前内存池可用内存的开始地址。下一次申请内存就从last地址开始。
    • 指针end 指向当前内存池的可申请内存的结束地址。指针last和end限定的区域就是我们所谓的小内存块申请的区域。这些小内存块大小可以不同,但是不能超过下面的max数值。这些内存块并没有被有效的管理起来,所以,他们只能在整个内存池释放时才能得到释放。
    • 指针next指向下一个内存池。NGINX的内存池通过next指针进行串联,形成一个内存池链。只有第一个内存池有完整的ngx_pool_t结构,用来维护整个内存池的管理和维护信息。剩余的内存池只有ngx_pool_data_t结构来记录当前内存池的内存申请状态。
    • failed表明当前内存池内存累加申请失败的次数。

    3.2内存池头部结构

    1. struct ngx_pool_s {
    2. 2: ngx_pool_data_t d;
    3. 3: size_t max;
    4. 4: ngx_pool_t *current;
    5. 5: ngx_chain_t *chain;
    6. 6: ngx_pool_large_t *large;
    7. 7: ngx_pool_cleanup_t *cleanup;
    8. 8: ngx_log_t *log;
    9. 9: };

    结构定义:

    • max表明可向内存池申请内存块大小的最大值。如果超过这一值则直接调用malloc向系统申请而不是通过内存池申请。这些通过系统malloc得到的大块内存也要记录在内存池的large单链表字段上,方便进行管理。和小块内存不同,这些大内存块可以通过ngx_pfree进行及时释放。
    • 指针current指向申请开始的内存池,由于内存池是一个链式结构,通过current指针可以避免每次都要遍历内存池节点链表。如果通过一个内存池申请内存失败的次数达到5次,current重新被赋值。这样可以避免每次都要挨个判断每一个内存池是否可以从中申请内存。从而可以节省时间。
    • 指针large用来指向直接向系统申请的大块内存链表(malloc)。
    • 指针chain和指针large类似。不过它指向的是结构ngx_buf_t。chain本身的数据结构ngx_chain_t和它的成员ngx_buf_t都是从内存池中申请的,然后挂入chain这个单向列表中。
    • 指针cleanup是一个函数指针单链表,在内存池释放时被依次调用。其作用类似C++中的析构函数。
    • 指针log指向的函数指针用来记录log时被调用。

    4.基本API

    创建内存池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);
    内存清除ngx_int_t  ngx_pfree(ngx_pool_t *pool, void *p);

    4.1预读知识

    内存对齐:

    1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

    封装函数

    ngx_alloc:(只是对malloc进行了简单的封装)

    1. void *ngx_alloc(size_t size, ngx_log_t *log)
    2. 2: {
    3. 3: void *p;
    4. 4:
    5. 5: p = malloc(size);
    6. 6: if (p == NULL) {
    7. 7: ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
    8. 8: "malloc(%uz) failed", size);
    9. 9: }
    10. 10:
    11. 11: ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
    12. 12:
    13. 13: return p;
    14. 14: }

    注一:

    log是记录时间

    ngx_calloc:(调用malloc并初始化为0)

    1. 1: void *ngx_calloc(size_t size, ngx_log_t *log)
    2. 2: {
    3. 3: void *p;
    4. 4:
    5. 5: p = ngx_alloc(size, log);
    6. 6:
    7. 7: if (p) {
    8. 8: ngx_memzero(p, size);
    9. 9: }
    10. 10:
    11. 11: return p;
    12. 12: }

    ngx_memzero:

     #define ngx_memzero(buf, n)       (void) memset(buf, 0, n)

    ngx_free :

     1: #define ngx_free          free

     ngx_memalign

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

     这里alignment主要是针对部分unix平台需要动态的对齐,对POSIX 1003.1d提供的posix_memalign( )进行封装,在大多数情况下,编译器和C库透明地帮你处理对齐问题。nginx中通过宏NGX_HAVE_POSIX_MEMALIGN来控制;

    4.2内存池的创建

    遵循RALL

    1. ngx_pool_t *
    2. ngx_create_pool(size_t size, ngx_log_t *log)
    3. {
    4. ngx_pool_t *p;
    5. p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    6. if (p == NULL) {
    7. return NULL;
    8. }
    9. p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    10. p->d.end = (u_char *) p + size;
    11. p->d.next = NULL;
    12. p->d.failed = 0;
    13. size = size - sizeof(ngx_pool_t);
    14. //#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
    15. //内存池最大不超过4095,x86中页的大小为4K
    16. p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
    17. p->current = p;
    18. p->chain = NULL;
    19. p->large = NULL;
    20. p->cleanup = NULL;
    21. p->log = log;
    22. return p;
    23. }

    1.nginx对内存的管理分为大内存与小内存,当某一个申请的内存大于某一个值时,就需要从大内存中分配空间否则从小内存中分配空间。
    2.nginx中的内存池是在创建的时候就设定好了大小,在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。当要分配大块内存是,则是在内存池外面再分配空间进行管理的,称为大块内存池。

    4.2内存申请

    ngx_palloc

    1. void *
    2. ngx_palloc(ngx_pool_t *pool, size_t size)
    3. {
    4. #if !(NGX_DEBUG_PALLOC)
    5. if (size <= pool->max) {
    6. return ngx_palloc_small(pool, size, 1);
    7. }
    8. #endif
    9. return ngx_palloc_large(pool, size);
    10. }

    ngx_palloc_small

    1. static ngx_inline void *
    2. ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
    3. {
    4. u_char *m;
    5. ngx_pool_t *p;
    6. p = pool->current;
    7. do {
    8. m = p->d.last;
    9. //对内存地址进行对齐处理
    10. if (align) {
    11. m = ngx_align_ptr(m, NGX_ALIGNMENT);
    12. }
    13. //如果在当前内存块有效范围内,进行内存指针的移动
    14. if ((size_t) (p->d.end - m) >= size) {
    15. p->d.last = m + size;
    16. return m;
    17. }
    18. p = p->d.next;如果当前内存块有效容量不够分配,则移动到下一个内存块进行分配
    19. } while (p);
    20. return ngx_palloc_block(pool, size);
    21. }

    ngx_align_ptr,这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,C编译器帮不了你了,得自己动手

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

    2.ngx_palloc_block(ngx_pool_t *pool, size_t size)

    1. static void *
    2. ngx_palloc_block(ngx_pool_t *pool, size_t size)
    3. {
    4. u_char *m;
    5. size_t psize;
    6. ngx_pool_t *p, *new;
    7. psize = (size_t) (pool->d.end - (u_char *) pool);
    8. //计算内存池第一个内存块的大小
    9. m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    10. //分配和第一个内存块同样大小的内存块
    11. if (m == NULL) {
    12. return NULL;
    13. }
    14. new = (ngx_pool_t *) m;
    15. new->d.end = m + psize;//设置新内存块的end
    16. new->d.next = NULL;
    17. new->d.failed = 0;
    18. m += sizeof(ngx_pool_data_t);//将指针m移动到d后面的一个位置,作为起始位置
    19. m = ngx_align_ptr(m, NGX_ALIGNMENT);
    20. new->d.last = m + size;//设置新内存块的last,即申请使用size大小的内存
    21. //这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,
    22. //就忽略,不需要每次都从头找起
    23. for (p = pool->current; p->d.next; p = p->d.next) {
    24. if (p->d.failed++ > 4) {
    25. pool->current = p->d.next;
    26. }
    27. }
    28. p->d.next = new;
    29. return m;
    30. }

    3.ngx_palloc_large(ngx_pool_t *pool, size_t size)

    ngx_palloc中首先会判断申请的内存大小是否超过内存块的最大限值,如果超过,则直接调用ngx_palloc_large,进入大内存块的分配流程;

    1. 1: static void *
    2. 2: ngx_palloc_large(ngx_pool_t *pool, size_t size)
    3. 3: {
    4. 4: void *p;
    5. 5: ngx_uint_t n;
    6. 6: ngx_pool_large_t *large;
    7. 7: // 直接在系统堆中分配一块空间
    8. 8: p = ngx_alloc(size, pool->log);
    9. 9: if (p == NULL) {
    10. 10: return NULL;
    11. 11: }
    12. 12:
    13. 13: n = 0;
    14. 14: // 查找到一个空的large区,如果有,则将刚才分配的空间交由它管理
    15. 15: for (large = pool->large; large; large = large->next) {
    16. 16: if (large->alloc == NULL) {
    17. 17: large->alloc = p;
    18. 18: return p;
    19. 19: }
    20. 20:
    21. 21: if (n++ > 3) {
    22. 22: break;
    23. 23: }
    24. 24: }
    25. 25: //为了提高效率, 如果在三次内没有找到空的large结构体,则创建一个
    26. 26: large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
    27. 27: if (large == NULL) {
    28. 28: ngx_free(p);
    29. 29: return NULL;
    30. 30: }
    31. 31:
    32. 32: large->alloc = p;
    33. 33: large->next = pool->large;
    34. 34: pool->large = large;
    35. 35:
    36. 36: return p;
    37. 37: }

     4.4内存池重置

    1. void
    2. 2: ngx_reset_pool(ngx_pool_t *pool)
    3. 3: {
    4. 4: ngx_pool_t *p;
    5. 5: ngx_pool_large_t *l;
    6. 6: //释放所有大块内存
    7. 7: for (l = pool->large; l; l = l->next) {
    8. 8: if (l->alloc) {
    9. 9: ngx_free(l->alloc);
    10. 10: }
    11. 11: }
    12. 12:
    13. 13: pool->large = NULL;
    14. 14: // 重置所有小块内存区
    15. 15: for (p = pool; p; p = p->d.next) {
    16. 16: p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    17. 17: }
    18. 18: }

     2.5、内存池清理

    ngx_pfree

    1. ngx_pfree(ngx_pool_t *pool, void *p)
    2. 3: {
    3. 4: ngx_pool_large_t *l;
    4. 5: //只检查是否是大内存块,如果是大内存块则释放
    5. 6: for (l = pool->large; l; l = l->next) {
    6. 7: if (p == l->alloc) {
    7. 8: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
    8. 9: "free: %p", l->alloc);
    9. 10: ngx_free(l->alloc);
    10. 11: l->alloc = NULL;
    11. 12:
    12. 13: return NGX_OK;
    13. 14: }
    14. 15: }
    15. 16:
    16. 17: return NGX_DECLINED;
    17. 18: }

    所以说Nginx内存池中大内存块和小内存块的分配与释放是不一样的。我们在使用内存池时,可以使用ngx_palloc进行分配,使用ngx_pfree释放。而对于大内存,这样做是没有问题的,而对于小内存就不一样了,分配的小内存,不会进行释放。因为大内存块的分配只对前3个内存块进行检查,否则就直接分配内存,所以大内存块的释放必须及时。

    ngx_pool_cleanup_s

    Nginx内存池支持通过回调函数,对外部资源的清理。ngx_pool_cleanup_t是回调函数结构体,它在内存池中以链表形式保存,在内存池进行销毁时,循环调用这些回调函数对数据进行清理。

    1. 1: struct ngx_pool_cleanup_s {
    2. 2: ngx_pool_cleanup_pt handler;
    3. 3: void *data;
    4. 4: ngx_pool_cleanup_t *next;
    5. 5: };

     

    handler:是回调函数指针;

    data:回调时,将此数据传入回调函数;

    next:指向下一个回调函数结构体;

    如果我们需要添加自己的回调函数,则需要调用ngx_pool_cleanup_add来得到一个ngx_pool_cleanup_t,然后设置handler为我们的清理函数,并设置data为我们要清理的数据。这样在ngx_destroy_pool中会循环调用handler清理数据;

     4.6 内存池销毁

    ngx_destroy_pool

    ngx_destroy_pool这个函数用于销毁一个内存池:

    1. void
    2. 2: ngx_destroy_pool(ngx_pool_t *pool)
    3. 3: {
    4. 4: ngx_pool_t *p, *n;
    5. 5: ngx_pool_large_t *l;
    6. 6: ngx_pool_cleanup_t *c;
    7. 7:
    8. 8: //首先调用所有的数据清理函数
    9. 9: for (c = pool->cleanup; c; c = c->next) {
    10. 10: if (c->handler) {
    11. 11: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
    12. 12: "run cleanup: %p", c);
    13. 13: c->handler(c->data);
    14. 14: }
    15. 15: }
    16. 16:
    17. 17: //释放所有的大块内存
    18. 18: for (l = pool->large; l; l = l->next) {
    19. 19:
    20. 20: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
    21. 21:
    22. 22: if (l->alloc) {
    23. 23: ngx_free(l->alloc);
    24. 24: }
    25. 25: }
    26. 26:
    27. 27: //最后释放所有内存池中的内存块
    28. 28: for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
    29. 29: ngx_free(p);
    30. 30:
    31. 31: if (n == NULL) {
    32. 32: break;
    33. 33: }
    34. 34: }

     5.总结

    5.1内存池创建

    NGINX通过ngx_create_pool创建内存池。参数size用来指定从内存池的小块内存区域总共可以获取的内存的最大,同时也是可以申请的单个小块内存的最大size。在函数执行过程中size会转化成16的整数倍。

    5.2内存块申请

    NGINX提供了几个从内存池中申请内存的API。他们大体流程是一样的。如果申请的内存的size不大于max,如就从小内存区域试着去切分,并且移动内存池的last指针指向新申请内存地址的末端。反之,就通过系统的malloc申请一片内存并且连接到pool的large链表中。

    void *ngx_palloc(ngx_pool_t*pool,size_t size)把size大小对齐然后再申请内存,若size大于max,则改用向系统申请。

    void *ngx_pnalloc(ngx_pool_t*pool,size_t size)和上面函数唯一的区别是size不用对齐.

    void*ngx_pmemalign(ngx_pool_t *pool,size_tsize)无论size大小实际内存都向系统申请,并且加入到内存池的large链表中。其中对应的管理结构ngx_large_t是从内存池中申请。

    void *ngx_pcalloc(ngx_pool_t*pool,size_t size)申请并初始化为零。

    5.3内存块释放

    从内存池中申请的小块内存不能单独释放,只能在内存池释放时被整体被释放。大内存块因为是通过系统调用ngx_alloc申请的,所以,这些内存块可以调用ngx_pfree被单独释放。这两个函数本质上是对系统调用malloc和free的封装。

    5.4内存池释放

    通过ngx_destroy_pool可以释放创建的内存池。函数首先会调用cleanup链表中的所有函数,然后通过调用free释放large链表中所有通过系统申请的大块内存。最后释放所有的内存池结构这其中就包括所有的小块内存。

    5.5优缺点

    通过使用内存池,NGINX有效地降低了内存分片,减少了内存泄露的可能。在使用小内存时只是进行了简单粗暴地分割来分配内存。这一方面简化了操作提高了效率。但是,另一方面这些大小不一小块内存因为没有管理信息的维护而不能及时释放和重用。它们只能在整个内存池释放时才能作为一个整体能得以释放。不过因为NGINX本身运行具有的阶段化的特征,特定内存池都只在特定阶段存在,使得内存不能及时释放的影响不是很大。

    或许NGINX的内存池也能结合kernel的slab内存池的某些特性。这些slab内存池的内存块也是从一个大的内存区域切分出来,它们被有效地管理起来,可以很方便地进行释放和重用。而且在内存池释放时可以方便地释放掉所有的内存,也可以有效地杜绝内存泄露的发生。但是有些额外的管理开销所以会浪费一些内存,而且每一个内存池只能支持一个size的内存块的申请。需要把这些不同size的内存池有效地组织和管理起来。

  • 相关阅读:
    【MySQL】深入理解MySQL索引优化器原理(MySQL专栏启动)
    苏州市发改委领导一行莅临蓝海彤翔集团调研考察
    开源深度学习模型部署工具箱MMDeploy简介及安装
    基于SSH的周报管理系统
    第12章 初识SqlSugarCore之监视Redis性能
    摸鱼气象Python教案代码分享
    【leetcode 力扣刷题】字符串匹配之经典的KMP!!!
    实践是成为网工最快的方法,网络工程师实战项目整理
    【快速排序介绍】
    @ConditionalOnMissingBean 如何实现覆盖第三方组件中的 Bean
  • 原文地址:https://blog.csdn.net/qq_62309585/article/details/127945173