本文从传统内存的弊端开始讲起,引出设置内存池的必要性,进而分析Nginx的内存池源码
1.C/C++传统内存操作的弊端
常用的内存操作函数
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字节的连续区域返回该区域的首地址.
calloc 与malloc相似,参数size为申请地址的单位元素长度,nmemb为元素个数,即在内存中中请nmemb*size字节大小的连续地址空间.内存会初始化0
realloc 给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度.ptr若为NULL,它就等同于malloc.
======================================================================================
int brk(void *addr);
void *shrk(intptr_t increment);
int posix_memalign(void **memptr,size_t alignment,size_t size);
void *memalign(size_t alignment,size_t size) ;
void *valloc(size_t size);
brk sbrk 改变进程堆区的终止处;
posix_memalign 返回size 字节的动态内存,地址是alignment的倍数。alignment必须是2的幕和void指针大小的倍数;
memalign 分配size字节的动态内存,地址是alignment的倍数的内存块。参数alignment必须是2的幕!
valloc 相当于使用页的大小作为对齐长度,使用memalign来分配内存
弊端一
高并发时较小内存块使用导致系统调用频繁,降低了系统的执行效率
弊端二
频繁使用时增加了内存的碎片,降低内存使用效率
内部碎片——已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间
产生根源:
- 内存分配必须起始于可被4,8或16整除(视处理器体系结构而定)的地址
- MMU的分页机制的限制
弊端三
没有垃圾回收机制,容易造成内存泄漏,导致内存枯竭
情形1
void log_error(char *reason)
{
char *p1;
p1 = malloc(100);
sprintf(p1,"The f1 error occurred because of '%s'.", reason);
log(p1);
}
情形2
int getkey(char *filename)
{
FILE *fp;
int key;
fp = fopen(filename, "r");
fscanf(fp, "%d", &key);
//fclose(fp);
return key;
}
弊端四
内存分配与释放的逻辑在程序中相隔较远时,降低程序的稳定性
ret get_stu_info(Student* _stu )
{
char* name= NULL;
name = getName(_stu->no);
//处理逻辑
if(name) {
free(name); //这里free掉了一个栈内存,报错!!!
key = NULL;
}
}
char stu_name[MAX];
char * getName(int stu_no){
//查找相应的学号并赋值给 stu_name
snprintf(stu_name,MAX,“%s”,name);
return stu_name;
}
2.弊端如何解决
内存管理维度分析
内存管理组件选型
PtMalloc (glibc自带) | TcMalloc | JeMalloc | |
---|---|---|---|
概念 | Glibc自带 | Google开源 | Jason Evans(FreeBSD开发人员) |
性能 (一次malloc/free 操作) | 300ns | 50ns | <=50ns |
弊端 | 锁机制降低性能,容易导致内存碎片 | 1%左右的额外内存开销 | 2%左右的额外内存开销 |
优点 | 传统、稳定 | 线程本地缓存,多线程分配效率高 | 线程本地缓存,多核多线程分配效率相当高 |
使用方式 | Glibc编译 | 动态链接库 | 动态链接库 |
谁在用 | 较普遍 | safari、chrome等 | facebook、firefox等 |
适用场景 | 除特别追求高效内存分配以外的 | 多线程下高效内存分配 | 多线程下高效内存分配 |
3.内存池技术
什么是内存池技术?
在真正使用内存之前,先申请一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需要时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,同一对程序所使用的内存进行同一的分配和回收。这样做的一个显著优点是,使得内存分配效率得以很大的提升
内存池如何解决弊端?
- 高并发时系统调用频繁,降低了系统的执行效率
内存池提前预先分配大块内存,统一释放,极大的减少了 malloc 和 free 等函数的调用
- 频繁使用增加了系统内存的碎片,避免了碎片的产生
内存池每次请求分配大小适度的内存块,避免了碎片的产生
- 没有垃圾回收机制,容易造成内存泄漏
在生命周期结束后统一释放内存,完全避免了内存泄露的产生
- 内存分配与释放的逻辑在程序中相隔较远时,降低程序的稳定性
在生命周期结束后统一释放内存,避免重复释放指针或释放空指针等情况
4.高性能内存池设计与实现
设计思路(分而治之)
高性能内存池结构图
关键数据结构
typedef struct {
u_char *last; II 保存当前数 据块中内存分配指针的当前位 置
u_char *end; II 保存内存块的结束位置
ngx_pool_t *next; II 内存池由多块内存块组成, 指向下一个数据块的位置
ngx_uint_t failed; II 当前数 据块内存不足引起分 配失败的次数
}ngx_pool_data_t;
struct ngx_pool_s {
ngx_pool_data_t d; II 内存池当前的 数据区指针的结构体
size_t max; // 当前数 据块最大可分配的内存大小 C Bytes)
ngx_pool_t *current; //当前正在使用的数据块的指针
ngx_pool_large_t *large; // pool 中指向大数据块的指针(大数据快是指 size > max 的数据块)
}
ngx_pool_t结构示意图(大小为1024的池)
Nginx内存池基本操作
内存池创建、销毁和重置
操作 | 函数 |
---|---|
创建内存池 | ngx_pool_t *ngx_create_pool(size_t size); |
销毁内存池 | void ngx_destory_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); |
内存申请(对齐并初始化) | void *ngx_pcalloc(ngx_pool_t *pool,size_t size); |
内存清楚 | ngx_int_t *ngx_free(ngx_pool_t *pool,void *p); |