愿打开此篇对你有所帮助。
动不动就 32GB 以上内存的服务器真需要关心内存碎片问题吗?
咳咳,这是知乎上的一个议题哈。我看了之后觉得,我不能等明天了,我今天就把nginx的内存池给剖了。
类似的我还看到一个议题哈:内存池除了减少内存申请和释放的开销之外还有什么提升性能或者方便之处?
对这些个议题我是不敢去插一嘴的,神仙打架。我就问一声儿,在座的各位,谁会设计一个好的内存池出来?
你猜猜需不需要关心内存碎片,是那些会设计的人来讨论,我们不会的话,还是先学会设计再说哈。
来来来,我们来···这样这样,内样内样···
好了哈,我觉得还是要发表一下自己的观点,因为我会设计啊,虽然不一定好。
我觉得,要从实际情况出发(又是这句废话)。
我说几点想法,然后你可以记住,以后用到的时候直接搬出来,也可以再去看看别人的想法,总结一下自己的想法。
1、首先,你的开发环境允许你写内存池。(不要跟我说你拿着Python来写个内存池哈) 2、其次,多学学开源的/不开源的优秀线程池源码设计,人家是经过千锤百炼的。比如GNU、nginx、STL等。 3、使用内存池的其中一个优点在于确定性高,这对于时间要去苛刻的实时系统来说至关重要。比方说股票系统。 4、malloc是一个通用的内存分配器。就看你怎么理解这三个字了。 5、针对特殊场景甚至可以为重要的线程单独开内存池。 6、内存池可以节省内存,提高缓存命中率。当然,你要是觉得不需要那就不需要咯。
内存池案例
英文版,可以选择跳过这一part。我就不跳过啦,英语能力很重要的哦。
作者:阿哲 链接:https://www.zhihu.com/question/21894104/answer/19693701 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
再大的内存,只要软件运行的时间足够久,都有可能产生大量的内存碎片,从而对性能和可用内存造成负面影响。 造成内存碎片的原因大致可以归为两类:
- 内存分配机制。拥有先进GC机制的语言(如Java、C#),在对抗内存碎片方面表现较好。它们的GC一般会有个Compact步骤,会移动对象在内存中的位置,将多个对象整齐无间隙地排列好,从而消除了不少内存碎片。
- 如果是使用传统malloc/free或者自己写内存分配的话,产生内存碎片的概率不小。这方面比较典型的例子就是Firefox,它以前代码里有不少自己写的allocator,内存碎片问题是非常严重的。后来Mozilla开始逐步采用jemalloc来帮助解决这个问题。
举2个例子:Firefox7的时候修改了一个内存分配行为,就一下子降低了不少内存碎片:Firefox 7 Might Solve Memory Fragmentation IssuesFirefox15的时候对addon的机制做了改动,一下子解决了大量长期困扰的addon内存问题:Firefox 15 plugs the add-on leaks
取决于软件的具体类型,对抗内存碎片可能是个长期的战争,有兴趣的可以翻翻Mozilla的MemShrink项目:MemShrink | Nicholas Nethercote 看看别人是怎么用了2年功夫把Firefox从一个超级耗内存的浏览器变成一个最节约内存的浏览器。
malloc 底层原理
- malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去
- 如果malloc找不到一块合适的空闲内存,那么调用brk等系统调用扩大堆区从而获得更多的空闲内存
- malloc调用brk后开始转入内核态,此时操作系统中的虚拟地址系统开始工作,扩大进程的堆区,操作系统并没有为此分配真正的物理内存
- brk执行结束后返回到malloc,从内核态切换到用户态,malloc找到一块合适的空闲内存后返回
- 进程拿到内存,继续干活。
- 当有代码读写新申请的内存时系统内部出现缺页中断,此时再次由用户态切换到内核态,操作系统此时真正的分配物理内存,之后再次由内核态切换回用户态,程序继续。
如果对堆和栈有所了解的朋友应该会知道,堆是像上伸展的,栈是向下延伸的,那什么向上向下啊?有点迷哈。看个图:
一切尽在不言中咯。
早前就学过STL了,里面我最喜欢的一部分就是空间配置器,所以你要我张口就来也是可以的,不过我们还是多学习几个成功案例。
jemalloc && tcmalloc
jemalloc
tcmalloc
说实话啊,这俩我都没有用过呢,也是第一次听,先把概念放这儿,之后有时间了研究研究。
nginx 内存池实现
基本数据结构
数据块:
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;
};
(图片来源网络)
大块内存:
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
(图片来源网络)
回收站:
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
};
(图片来源网络)
源码分析
ngx_create_pool 创建内存池
用于创建一个内存池,我们创建时,传入我们的初始大小:
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
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;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
ngx_destroy_pool 销毁内存池
void ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
for (c = pool->cleanup; c; c = c->next) {
if (c->handler) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"run cleanup: %p", c);
c->handler(c->data);
}
}
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
if (l->alloc) {
ngx_free(l->alloc);
}
}
#if (NGX_DEBUG)
… …
#endif
for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
遍历内存池链表,释放所有内存,包括pool,large,cleanup链表,如果指定了cleanup回调来释放,则调用cleanup的handler来释放cleanup链表中的内存。
先依次释放pool中cleanup,large类型的链表,最后释放pool本身的链表。
ngx_reset_pool 重置内存池
void ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
ngx_pool_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
pool->large = NULL;
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
}
ngx_palloc 分配内存
ngx_align_ptr:一个用来内存地址取整的宏。取整可以降低CPU读取内存的次数,提高性能。这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,得自己动手。
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
void *ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
p = pool->current;
do {
m = ngx_align_ptr(p->d.last, 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);
}
return ngx_palloc_large(pool, size);
}
static void *ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p,*new,*current;
psize = (size_t) (pool->d.end - (u_char *) pool);
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if(p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
}
ngx_palloc_large:开辟一个大内存检查后交给大内存链表管理。所以开辟的内存必定在大内存链表上。
static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
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->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
ngx_pfree 内存清理
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;
for (l = pool->large; l; l = l->next) {
if (p == l->alloc) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p", l->alloc);
ngx_free(l->alloc);
l->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
cleanup机制
pool->cleanup本身是一个链表,每个ngx_pool_cleanup_t的数据结构上,保存着内存数据的本身cleanup->data和回调清理函数cleanup->handler。
ngx_pool_cleanup_add:分配一个可以用于回调函数清理内存块的内存。内存块仍旧在p->d或p->large上(因为调用的是ngx_palloc)
*/
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
ngx_pool_run_cleanup_file:清除p->cleanup链表上的某个已打开的文件描述符fd占用的内存块(或者叫清除指定的文件描述符)
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd) {
ngx_pool_cleanup_t *c;
ngx_pool_cleanup_file_t *cf;
for (c = p->cleanup; c; c = c->next) {
if (c->handler == ngx_pool_cleanup_file) {
cf = c->data;
if (cf->fd == fd) {
c->handler(cf);
c->handler = NULL;
return;
}
}
}
}
ngx_pool_cleanup_file:ngx_pool_run_cleanup_file的回调函数。通过ngx_close_file里面去调用底层的close关闭掉对应的文件描述符。 ngx官方写的回调函数
void ngx_pool_cleanup_file(void *data) {
ngx_pool_cleanup_file_t *c = data;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
c->fd);
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
ngx_pool_delete_file:这里是删除文件的回调函数,是连文件也删除。
void ngx_pool_delete_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_err_t err;
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
c->fd, c->name);
if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
err = ngx_errno;
if (err != NGX_ENOENT) {
ngx_log_error(NGX_LOG_CRIT, c->log, err,
ngx_delete_file_n " \"%s\" failed", c->name);
}
}
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
|