NGINX内存池现实机制
282 次浏览
发表于 2020-06-10 22:11

问题引入

 

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

问题解决

 

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

 

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

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

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

 

下面我们通过分析NGINX内存池的代码实现来理解上述的原理。

数据结构

 

NGINX使用ngx_pool_t结构来表示一个内存池。相关数据结构定义如下。

struct ngx_pool_cleanup_s {

    ngx_pool_cleanup_pt   handler;

    void                 *data;

    ngx_pool_cleanup_t   *next;

};

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {

    ngx_pool_large_t     *next;

    void                 *alloc;

};

typedef struct {

    u_char               *last;

    u_char               *end;

    ngx_pool_t           *next;

    ngx_uint_t            failed;

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

    ngx_log_t            *log;

};

typedef struct {

    ngx_fd_t              fd;

    u_char               *name;

    ngx_log_t            *log;

} ngx_pool_cleanup_file_t;

 

结构成员意义

  • 指针last 指向当前内存池可用内存的开始地址。下一次申请内存就从last地址开始。


  • 指针end 指向当前内存池的可申请内存的结束地址。指针last和end限定的区域就是我们所谓的小内存块申请的区域。这些小内存块大小可以不同,但是不能超过下面的max数值。这些内存块并没有被有效的管理起来,所以,他们只能在整个内存池释放时才能得到释放。


  • 指针next指向下一个内存池。NGINX的内存池通过next指针进行串联,形成一个内存池链。只有第一个内存池有完整的ngx_pool_t结构,用来维护整个内存池的管理和维护信息。剩余的内存池只有ngx_pool_data_t结构来记录当前内存池的内存申请状态。


  • failed表明当前内存池内存累加申请失败的次数。


  • max表明可向内存池申请内存块大小的最大值。如果超过这一值则直接调用malloc向系统申请而不是通过内存池申请。这些通过系统malloc得到的大块内存也要记录在内存池的large单链表字段上,方便进行管理。和小块内存不同,这些大内存块可以通过ngx_pfree进行及时释放。


  • 指针current指向申请开始的内存池,由于内存池是一个链式结构,通过current指针可以避免每次都要遍历内存池节点链表。如果通过一个内存池申请内存失败的次数达到5次,current重新被赋值。这样可以避免每次都要挨个判断每一个内存池是否可以从中申请内存。从而可以节省时间。


  • 指针large用来指向直接向系统申请的大块内存链表。


  • 指针chain和指针large类似。不过它指向的是结构ngx_buf_t。chain本身的数据结构ngx_chain_t和它的成员ngx_buf_t都是从内存池中申请的,然后挂入chain这个单向列表中。


  • 指针cleanup是一个函数指针单链表,在内存池释放时被依次调用。其作用类似C++中的析构函数。


  • 指针log指向的函数指针用来记录log时被调用。

 

数据结构关联图如下

Picture1.png

图一  ngx_pool_t 数据结构

代码原理

内存池创建

 

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

 

内存块申请

 

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

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)申请并初始化为零。

内存块释放

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

内存池释放

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

总结

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

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


发表评论
发表者

皮皮鲁

老白菜

  • 21

    文章

  • 19

    关注

  • 19

    粉丝

活动推荐
版权所有©F5 Networks,Inc.保留所有权利。京ICP备16013763号-5