再大的内存,只要软件运行的时间足够久,都有可能产生大量的内存碎片,从而对性能和可用内存造成负面影响。 造成内存碎片的原因大致可以归为两类:
- 内存分配机制。拥有先进GC机制的语言(如Java、C#),在对抗内存碎片方面表现较好。它们的GC一般会有个Compact步骤,会移动对象在内存中的位置,将多个对象整齐无间隙地排列好,从而消除了不少内存碎片。
- 如果是使用传统malloc/free或者自己写内存分配的话,产生内存碎片的概率不小。这方面比较典型的例子就是Firefox,它以前代码里有不少自己写的allocator,内存碎片问题是非常严重的。后来Mozilla开始逐步采用jemalloc来帮助解决这个问题。
malloc 底层原理
- malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去
- 如果malloc找不到一块合适的空闲内存,那么调用brk等系统调用扩大堆区从而获得更多的空闲内存
- malloc调用brk后开始转入内核态,此时操作系统中的虚拟地址系统开始工作,扩大进程的堆区,操作系统并没有为此分配真正的物理内存
- brk执行结束后返回到malloc,从内核态切换到用户态,malloc找到一块合适的空闲内存后返回
- 进程拿到内存,继续干活。
- 当有代码读写新申请的内存时系统内部出现缺页中断,此时再次由用户态切换到内核态,操作系统此时真正的分配物理内存,之后再次由内核态切换回用户态,程序继续。
Nginx内存池设计
Nginx 使用内存池对内存进行管理,把内存分配归结为大内存分配和小内存分配,申请的内存大小比同页的内存池最大值 max 还 大,则是大内存分配,否则为小内存分配。
- 大块内存的分配请求不会直接在内存池上分配内存来满足请求,而是直接向系统申请一块内存(就像 直接使用 malloc 分配内存一样),然后将这块内存挂到内存池头部的 large 字段下。
- 小块内存分配,则是从已有的内存池数据区中分配出一部分内存。
一、nginx数据结构
// SGI STL小块和大块内存的分界点:128B
// nginx(给HTTP服务器所有的模块分配内存)小块和大块内存的分界点:4096B
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
// 内存池默认大小
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
// 内存池字节对齐,SGI STL对其是8B
#define NGX_POOL_ALIGNMENT 16
#define NGX_MIN_POOL_SIZE ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), NGX_POOL_ALIGNMENT)
// 将开辟的内存调整到16的整数倍
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
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; // 指向可用于分配空间的内存块(failed < 4)的起始地址
ngx_chain_t *chain; // 连接所有的内存池块
ngx_pool_large_t *large; // 大块内存的入口指针
ngx_pool_cleanup_t *cleanup; // 内存池块的清理操作,用户可设置回调函数,在内存池块释放之前执行清理操作
ngx_log_t *log; // 日志
};
typedef struct ngx_pool_s ngx_pool_t;
nginx内存池管理函数接口
// 创建size大小的内存池
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);
// 3个分配内存的函数
void *ngx_palloc(ngx_pool_t *pool, size_t size); // 考虑内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size); // 不考虑内存对齐
void *ngx_pcalloc(ngx_pool_t *pool, size_t size); // 内存分配了可以进行初始化,清零
// 内存对齐
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
// 释放内存
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
// 内存清理相关函数接口
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);
二、nginx向OS申请空间ngx_create_pool
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
void* ngx_alloc(size_t size, ngx_log_t *log){
void *p = malloc(size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "malloc(%uz) failed", size);
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
return p;
}
// 根据size进行内存开辟
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
ngx_pool_t *p;
// 根据系统平台定义的宏以及用户执行的size,调用不同平台的API开辟内存池,malloc
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); // 能使用的空间 = 总空间 - 头信息
// 指定的大小若大于一个页面就用一个页面,否则用指定的大小
// max = min(size, 4095),max指的是除开头信息以外的内存块的大小
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
p->current = p; // 指向可用于分配空间的内存块的起始地址
p->chain = NULL;
p->large = NULL; // 小块内存直接在内存块开辟,大块内存在large指向的内存开辟
p->cleanup = NULL;
p->log = log;
return p;
}
max = min(size, 4095),初始化后不再改变,申请的内存不大于max则调用ngx_palloc_small,分配小块内存。否则调用ngx_palloc_large==>ngx_alloc==>malloc,申请大块内存、
三、nginx向内存池申请空间ngx_palloc
void* ngx_palloc(ngx_pool_t *pool, size_t size){
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
// 当前分配的空间小于max,小块内存的分配
return ngx_palloc_small(pool, size, 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); // 不考虑内存对齐
}
#endif
return ngx_palloc_large(pool, size);
}
void* ngx_pcalloc(ngx_pool_t *pool, size_t size){
void *p;
p = ngx_palloc(pool, size); // 考虑内存对齐
if (p) {
ngx_memzero(p, size); // 可以初始化内存为0
}
return p;
}
当前内存池的块不够分配:
- 开辟新的内存块,修改新内存块头信息的last、end、next、failed
- 前面所有内存块的failed++
- 连接新的内存块以及前面的内存块
四、大块内存的分配与释放ngx_palloc_large
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存的起始地址
void *alloc; // 大块内存的起始地址
};
static 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;
// 并不是每次分配大块内存都重新在小块内存上分配头信息,会遍历存储大块内存头信息的链表,查找已经释放的大块内存
// for循环遍历存储大块内存信息的链表
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
// 当大块内存被ngx_pfree时,alloc为NULL
// 遍历链表,若大块内存的首地址为空,则把当前malloc的内存地址写入alloc
large->alloc = p;
return p;
}
// 遍历4次后,若还没有找到被释放过的大块内存对应的信息(只查看前4个内存块,由于是头插法,前4个内存块是不断改变的)
// 为了提高效率,直接在小块内存中申请空间保存大块内存的信息
if (n++ > 3) {
break;
}
}
// 通过指针偏移在小块内存池上分配存放大块内存头信息*next和*alloc的空间
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
// 如果在小块内存上分配存储*next和*alloc空间失败,则无法记录大块内存头信息
// 释放大块内存p
ngx_free(p);
return NULL;
}
large->alloc = p; // alloc指向大块内存的首地址
large->next = pool->large; // 这两句采用头插法,将新内存块的记录信息存放于以large为头结点的链表中
pool->large = large;
return p;
}
通过ngx_alloc,即malloc分配大块内存,通过ngx_palloc_small在小块内存上存放大块内存头信息,大块内存头信息包含* next和* alloc,分别表示下一个大块内存头信息和大块内存的起始地址
在小块内存上开辟大块内存头信息时,也不是直接就在小块内存上重新申请新的空间,而是查看前4个内存块,是否有已经被释放过的大块内存的头信息,进行复用。前4个无法复用,则直接使用ngx_palloc_small申请新的空间
五、关于小块内存不释放ngx_reset_pool
只用了last和end两个指着标识空闲的空间,是无法将已经使用的空间合理归还到内存池的,只是会重置内存池。同时还存储了指向大内存块large和清理函数cleanup的头信息
考虑到nginx的效率,小块内存分配高效,同时也不回收内存
reset内存池时,由于需要重置小块内存,而大块内存的控制信息在小块内存中保存,所以需要先释放大块内存,在重置小块内存。所以先遍历存放大块内存头信息的链表,使用ngx_free释放
重置小块内存池时,其实就是偏移了每个内存块头信息中的last指针(可用内存起始地址)
六、销毁和清空内存池ngx_destroy_pool
// 假设内存对齐为4B
typedef struct{
char* p;
char data[508];
}stData;
ngx_pool_t *pool = ngx_create_pool(512, log); // 创建一个总空间为512B的nginx内存块
stData* data_ptr = ngx_alloc(512); // 属于大内存开辟,因为可用的实际内存大小为:512-sizeof(ngx_pool_t)
data_ptr->p = (char*)malloc(10); // p指向外界堆内存,类似于C++对象中对象占用了外部资源
按照上述代码的情况,大块数据中的指针也占用了外部资源,当回收大块内存的时候,调用ngx_free,就会导致内存泄漏
ngx_free释放大块内存前,需要调用预置的回调函数(pool->cleanup->handler),释放占用的外部资源,然后再调用ngx_free释放内存池的大块内存
以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)
typedef void (*ngx_pool_cleanup_pt)(void *data);
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
// 以下结构体由ngx_pool_s.cleanup指向,也是存放在内存池的小块内存
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data; // 指向需要释放的资源
ngx_pool_cleanup_t *next; // 释放资源的函数都放在一个链表,用next指向这个链表
};
总结
通过使用内存池,NGINX有效地降低了内存分片,减少了内存泄露的可能。在使用小内存时只是进行了简单粗暴地分割来分配内存。这一方面简化了操作提高了效率。但是,另一方面这些大小不一小块内存因为没有管理信息的维护而不能及时释放和重用。它们只能在整个内存池释放时才能作为一个整体能得以释放。不过因为NGINX本身运行具有的阶段化的特征,特定内存池都只在特定阶段存在,使得内存不能及时释放的影响不是很大。
或许NGINX的内存池也能结合kernel的slab内存池的某些特性。这些slab内存池的内存块也是从一个大的内存区域切分出来,它们被有效地管理起来,可以很方便地进行释放和重用。而且在内存池释放时可以方便地释放掉所有的内存,也可以有效地杜绝内存泄露的发生。但是有些额外的管理开销所以会浪费一些内存,而且每一个内存池只能支持一个size的内存块的申请。需要把这些不同size的内存池有效地组织和管理起来