欢迎光临
我们一直在努力

wic是什么公司libumem内存不释放问题剖析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


libumem是由sun公司的Jeff bonwick和Jonathan Adams于2001年开发的一个用户态内存管理库。Jeff Bonwick(slab的鼻祖)在slab的基础上扩展了magazine layer和vmem layer层,使得umem具有更强的内存管理能力,提供弹性扩展、低碎片化、高性能特性的内存管理库,Jeff Bonwick为内存管理提供了新思路。

由于项目特殊需求需要使用umem内存管理库,在使用umem库的过程中出现一个现象:系统内存为32G,某用户进程业务使用umem_cache_alloc接口申请了4G的内存,经过业务反复震荡,最后业务确认所有内存均通过umem_cache_free释放给了umem库,但是umem没有将内存释放给系统。懵了,芭比Q了,这是为什么呢?。


由于去年小弟定位过glibc内存出现内存站岗问题,有大量内存不释放内存给OS的问题,详见微信公众号阅码场Linux glibc内存站岗问题及解决方法,很自然这个问题就还是交给小弟去定位,但是我也没有接触过umem,不知道是什么来路呀?一脸懵啊,只得从网上找资料,于是有了如下的心路历程。

umem库是2001年开发的,目前没有看到有同学去维护该内存库,也没看到有分析umem源码实现的。Sun公司早已经被Oracle收购了,而Jeff bonwick也已经离开oracle多年了,有关于umem的中文资料也非常简单,只涉及umem的使用,所幸的是英文资料找到了作者Jeff Bonwick & Jonathan Adams的一篇会议论文Magazines and Vmem:Extending the Slab Allocator to Many CPUs and Arbitrary Resources。好好研读了该论文后有了一个基本的了解,论文和代码实现还是相差很大的,道路且长,只能硬啃代码。本文篇幅有限,将简化细节,呈现libumem的框架,敬请见谅。

与glibc,uclibc、tcmalloc一样,libumem也有自己的内存缓存层,任何一个内存管理库都离不开缓存层,glibc/uclibc有fast-bin、top chunk、arena概念,tcmalloc有thread cache、central cache、span。只有加上缓存内存的申请释放速度才能得到保证,否则都是空话。

对于umem,其有magazine layer 、depot layer、slab layer、vmem layer(vmem有多个层级),在申请内存时从最低层级进行申请,如果申请不到则一级一级往上进行申请,最终是调用mmap向OS进行批量申请。
源自网络

  1. 业务申请内存时会先从cpu-cache(umem使用线程来代替per-cpu)里面申请内存,先loaded magazine里面申请一个内存(magazine的含义是弹夹,内存块相当于子弹,以栈的形式弹出内存块),如果拿不到则查看preloaded magazine里面是否为空,如果为空转2,如果不为空则对调preloaded magazine和loaded magazine直接去一个内存返回。
  2. 从depot里面申请一个full magazine(压满子弹的弹夹),将full magazine放入preloaded magazine,然后将preloaded magazine(因为loaded和preloaded都为空了才可能来depot里面申请,故此时preloaded magazine肯定是empty),在将loaded 和 preloaded对调,此时loaded就是一个full magazine了,那么就可以出栈一个内存块。若depot没有full magazine则从slab里面取一个buffer,如果有空闲的slab则转3,如果有umem cache里面没有slab则转4
  3. 从slab->slab_head链表取一个内存块,slab_head是一个链表,链接这bufctl结构体,每个bufctl结构体记录了每个buffer的地址、所属的slab,下一个bufctl的地址。对于即将要切走的buffer,需要对buffer地址进行hash挂入cache->cache_hash_table链表上,然后将buffer返回给用户。
  4. slab都是以quantum(4k)为单位从其上一级缓存层进行申请,一般情况下slab都是从umem_default_arena进行申请,如果umem_default_arena申请不到则从umem_va_arena进行申请,umem_va_arena申请不到从vmem_heap进行申请,vmem_heap申请不到则从vmem0[0]进行申请,如果vmem0[0]也申请不到则从OS进行申请。从OS申请到内存后,这里申请内存是以PROT_NONE进行申请,等内存到了umem_default_arena层才会用mmap带上PROT_READ|PROT_WIRTE|PROT_EXEC将内存再次映射为rwxp的内存,等到业务读写时会do_page_fault分配物理内存。umem缓存层级关系
  1. 在释放时,umem_cache_free会释放给magazine layer,magazine layer会缓存这些内存,如果magazine满了会将magazine还给depot layer,然后就返回了。umem给每个线程都配备了一个magazine layer层,这样做的目的是为了提高速度。申请的时候优先从线程缓存层进行申请,同样释放也优先释放给线程缓存层,这种做法与tcmalloc的thread cache的思想是一样的,glibc也有non-arena的思想,这些思想都是一样的,只为了一个目标,就是降低锁的竞争,提高线程申请内存的速度。
  2. 由于magazine是要管理数据的,所以在还给magazine时,如果magazine用完了,会从depot layer去申请空的magazine管理结构,如果申请失败则会直接将内存还给slab layer,这个释放流程能够直到OS,由slab layer释放给其上一级内存umem_default_arena,umem_default_arena释放给umem_va_arena,umem_va_arena释放给vmem_heap,vmem_heap释放给vmem0[0]。在还给umem_default_arena时,如果空闲内存有连续的4k内存(在一般的Linux系统上都是以4k为pagesize),则会调用 属性将内存将内存释放给OS,从而达到闭环。

从释放流程可以看到,在umem_cache_free时,由于umem增加了magazine layer,从代码流程可以看到释放给magazine layer或depot layer后就直接返回了,故而会导致业务释放的所有内存都缓存在magazine layer和depot layer。

那么问题来了,既然umem不会把内存释放给OS,不就是内存泄露了么,Jeff Bonwick大神岂能犯如此低级的错误?事实上,umem有一个专用的update线程,这个线程主要用于调整magazine的round,也就是一个magazine含有多少个内存块,这个内存块太小会降低umem库的性能,太大会导致umem消耗的内存太多,故而有一个线程来动态的调整magazine的rounds值,调整方法大家可以去看看小论文,再结合代码看。另外一个作用就是回收内存,将magazine layer和depot layer缓存住的内存全部释放slab,slab再往其上一级释放,直到释放给OS。

umem库采用了内核buffer/cache的思想,为了提高申请释放的速度,需要缓存内存,这里再一次强调了缓存层起到巨大的作用,内核slab的机制也采用了CPU cache layer,就是为了提高速度。umem在系统还有内存时会不断的消耗内存,直到内存消耗完,umem也申请不到内存时才会触发update线程来回收内存。

当时找到这个原因之后,可激动了,以为该问题就此可以解决,但是并没有用。实测发现内存还是回收不掉,于是写了个demo测试,尝试申请1G内存,起一个线程周期性触发释放流程,发现可以释放,但是内存在4G左右的时候就不行了,芭比Q了,为什么?

后面再次进行代码分析,结合调试,一步一步,最终发现了问题所在,前面我们提到”slab都是以quantum(4k)为单位从其上一级缓存层进行申请”,问题就出现在这里,因为umem最顶级的缓存层vmem0[0]从OS申请内存,这次申请是mmap出PROT_NONE较大的虚拟内存

static void *vmem_mmap_top_alloc(vmem_t *src, size_t size, int vmflags)
	/*
	 * Need to grow the heap
	 */
#ifdef _WIN32
	buf = VirtualAlloc(NULL, size, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
	if (buf == NULL) buf = MAP_FAILED;
#elif defined(MAP_ALIGN)
	buf = mmap((void*)CHUNKSIZE, size, FREE_PROT, FREE_FLAGS | MAP_ALIGN,-1, 0);
#else
	/*使用PROT_NONE申请内存,仅仅是申请虚拟内存,内核没有分配物理内存*/
	buf = mmap(0, size, FREE_PROT, FREE_FLAGS, -1, 0);
#endif

	if (buf != MAP_FAILED) 
	} else {
		/*
		 * Growing the heap failed.  The allocation above will
		 * already have called umem_reap().
		 */
		ASSERT((vmflags & VM_NOSLEEP) == VM_NOSLEEP);
		errno = old_errno;
		return (NULL);
	}
}

然后在umem_default_arena层会再次映射为rxwp权限的内存,等do_page_fault后再分配实际内存。

static void *
vmem_mmap_alloc(vmem_t *src, size_t size, int vmflags)

#endif

	errno = old_errno;
	return (ret);
}

最后业务释放了,umem_default_arena又会再次去映射该内存为PROT_NONE

static void
vmem_mmap_free(vmem_t *src, void *addr, size_t size)

正是因为如此反复振荡,会导致进程的内存分布呈现如下现象:
vma碎片化从maps文件可以看到其基本是交叉的,4k权限为rxwp后面跟着4k权限为—p的vma,这样导致消耗的vma越来越多,最终超过了进程的最大vma数量限制65530,超过之后再mmap内核会直接拦截,出现无法释放内存也无法申请内存。

在2001年的时候,当时内存还比较小,系统128M都属于较大的了(当时我还在玩泥巴),内核限制进程65530个vma,即便每个vma表示4k,那么65530也能表示255M内存,所以不会出现这种现象,而现在我们的系统内存动辄32G,64G的,vma碎片化的问题就会非常的明显了,故而会出现这个现象。

在调大quantum的大小为128k后,8G内存也只需要消耗1.1万个vma,不行可以再进行调大,可以根据系统规格来决定其大小,切割单位越大就会出现更严重的内存站岗问题,切割太小则会出现消耗vma太多的问题,故需要平衡,目前看128k已经完全能满足要求了,还能减少mmap系统调用的次数,提高性能。

  • 法1

可以起一个线程,在umem空闲内存超过一定阈值后调用vmem_reap函数触发内存释放流程,我们在测试时在使用方法后申请释放8G内存能回收95%左右,还有5%释放不掉是因为umem的管理数据占用了,导致内存无法释放。

  • 法2
    改造umem,在释放内存给magazine layer时,做个内存统计,如果free内存超过一定值就让内存释放给slab layer。

umem内存管理库虽然2001写的,但是其思想是非常优秀的,去阅读源码会发现里面有很多代码写的非常优秀的地方,非常值得学习。Jeff Bonwick在Sun工作了20多年,对内核有着深厚的理解,其也是slab的鼻祖,Linux内核的slab也是采用该思想,一直用了很多年。

看umem源码比看glibc库的感觉要艰难一些,可能是资料少,很多地方需要根据代码去揣摩作者的用意,经一番痛苦挣扎,终于对umem源码有一个较为清晰的理解,在分析源码过程中得到了很多快乐,学习了一些新的思想,尽管该库是20年前的库,但其思想是不会过时的。

对于该问题的修改我贴在github上,有兴趣可以看看,如下分支后续会提供内存统计功能,有问题可以邮件联系。
github:https://github.com/mrdotK/libumem/tree/umem-vma-useup

umem源码:
https://github.com/illumos/illumos-gate/tree/master/usr/src/lib/libumem

由于本人水平有限,无法保证对umem的理解都准确无误,所以读者发现其中有什么错误,请勿见怪,若方便,可以来信讨论,邮箱ldys2014@foxmail.com。

1,https://www.usenix.org/legacy/event/usenix01/full_papers/bonwick/bonwick_html/index.html
2,http://lxmdrw.blog.163.com/blog/static/27716971201142611276184/
3,https://linux.cn/thread-5239-1-1.html
4,https://blog.csdn.net/arrow_pig/article/details/6123010
5,https://nanxiao.me/tag/libumem/

赞(0)
未经允许不得转载:上海聚慕医疗器械有限公司 » wic是什么公司libumem内存不释放问题剖析

登录

找回密码

注册