[原材料] xushiwei 内存管理

【原创】技术系列之 内存管理(一)
作者:CppExplore 网址:http://www.cppblog.com/CppExplore/

服务器设计人员在一段时间的摸索后,都会发现:服务器性能的关键在于内存。从收包到解析,到消息内存的申请,到session结构内存的申请都要小心处理,尽量减少内存数据copy,减少内存动态申请,减少内存检索。为达到这个目的,不同的地方有不同的方法,比如常见的包解析,使用缓冲区偏移以及长度来标识包内字段信息;内存使用量固定的系统,系统启动就申请好所有需要的内存,初始化好,等待使用的时候直接使用;基于license控制的系统,根据license的数量,一次性申请固定数量内存等......。本文不再总结这些特性方案,重点说下常见的通用的内存池缓存技术。

内存池可有效降低动态申请内存的次数,减少与内核态的交互,提升系统性能,减少内存碎片,增加内存空间使用率,避免内存泄漏的可能性,这么多的优点,没有理由不在系统中使用该技术。

为了给内存池技术寻找基石,先从低层的内存管理看起。

硬件层略掉不谈,可回顾《操作系统》。

一、linux内存管理策略

linux低层采用三层结构,实际使用中可以方便映射到两层或者三层结构,以适用不同的硬件结构。最下层的申请内存函数get_free_page。之上有三种类型的内存分配函数

(1)kmalloc类型。内核进程使用,基于slab技术,用于管理小于内存页的内存申请。思想出发点和应用层面的内存缓冲池同出一辙。但它针对内核结构,特别处理,应用场景固定,不考虑释放。不再深入探讨。

(2)vmalloc类型。内核进程使用。用于申请不连续内存。

(3)brk/mmap类型。用户进程使用。malloc/free实现的基础。

有关详细内容,推荐http://www.kerneltravel.net/journal/v/mem.htmhttp://www.kerneltravel.net上有不少内核相关知识。

二、malloc系统的内存管理策略

malloc系统有自己的内存池管理策略,malloc的时候,检测池中是否有足够内存,有则直接分配,无则从内存中调用brk/mmap函数分配,一般小于等于128k(可设置)的内存,使用brk函数,此时堆向上(有人有的硬件或系统向下)增长,大于128k的内存使用mmap函数申请,此时堆的位置任意,无固定增长方向。free的时候,检测标记是否是mmap申请,是则调用unmmap归还给操作系统,非则检测堆顶是否有大于128k的空间,有则通过brk归还给操作系统,无则标记未使用,仍在glibc的管理下。glibc为申请的内存存储多余的结构用于管理,因此即使是malloc(0),也会申请出内存(一般16字节,依赖于malloc的实现方式),在应用程序层面,malloc(0)申请出的内存大小是0,因为malloc返回的时候在实际的内存地址上加了16个字节偏移,而c99标准则规定malloc(0)的返回行为未定义。除了内存块头域,malloc系统还有红黑树结构保存内存块信息,不同的实现又有不同的分配策略。频繁直接调用malloc,会增加内存碎片,增加和内核态交互的可能性,降低系统性能。linux下的glibc多为Doug Lea实现,有兴趣的可以去baidu、google。

三、应用层面的内存池管理

跳过malloc,直接基于brk/mmap实现内存池,原理上是可行的,但实际中这种实现要追逐内核函数的升级,增加了维护成本,另增加了移植性的困难,据说squid的内存池是基于brk的,本人尚未阅读squid源码(了解磁盘缓存的最佳代码,以后再详细阅读),不敢妄言。本文后面的讨论的内存池都是基于malloc(或者new)实现。我们可以将内存池的实现分两个类别来讨论。

1、不定长内存池。典型的实现有apr_pool、obstack。优点是不需要为不同的数据类型创建不同的内存池,缺点是造成分配出的内存不能回收到池中。这是由于这种方案以session为粒度,以业务处理的层次性为设计基础。

(1)apr_pool。apr全称Apache portable Run-time libraries,Apache可移植运行库。可以从http://www.apache.org/网站上下载到。apache以高性能、稳定性著称,它所有模块的内存申请都由内存池模块apr_pool实现。有关apr_pool结构、实现的原理,http://blog.csdn.net/tingya/(apache源码分析类别中的apache内存池实现内幕系列)已经有了详细的讲解,结合自己下载的源码,已经足够了。本人并不推荐去看这个blog和去看详细的代码数据结构以及逻辑。明白apr_pool实现的原理,知道如何使用就足够了。深入细节只能是浪费脑细胞,当然完全凭个人兴趣爱好了。

这里举例说下简单的使用:
[原材料] xushiwei 内存管理#include"apr_pools.h"

[原材料] xushiwei 内存管理#include
<stdio.h>

[原材料] xushiwei 内存管理#include
<new>

[原材料] xushiwei 内存管理

[原材料] xushiwei 内存管理
intmain()

[原材料] xushiwei 内存管理
{

[原材料] xushiwei 内存管理apr_pool_t
root;

[原材料] xushiwei 内存管理apr_pool_initialize();
//初始化全局分配子(allocator),并为它设置mutext,以用于多线程环境,初始化全局池,指定全局分配

[原材料] xushiwei 内存管理


[原材料] xushiwei 内存管理子的owner是全局池

[原材料] xushiwei 内存管理apr_pool_create(
&root,NULL);//创建根池(默认父池是全局池),根池生命期为进程生存期。分配子默认为全局分配子

[原材料] xushiwei 内存管理
{

[原材料] xushiwei 内存管理apr_pool_t
child;

[原材料] xushiwei 内存管理apr_pool_create(
&child,root);//创建子池,指定父池为root。分配子默认为父池分配子

[原材料] xushiwei 内存管理
voidpBuff=apr_palloc(child,sizeof(int));//从子池分配内存

[原材料] xushiwei 内存管理
int
pInt=new(pBuff)int(5);//随便举例下基于已分配内存后,面向对象构造函数的调用。

[原材料] xushiwei 内存管理
printf("pInt=%dn",pInt);

[原材料] xushiwei 内存管理
{

[原材料] xushiwei 内存管理apr_pool_t
grandson;

[原材料] xushiwei 内存管理apr_pool_create(
&grandson,root);

[原材料] xushiwei 内存管理
voidpBuff2=apr_palloc(grandson,sizeof(int));

[原材料] xushiwei 内存管理
int
pInt2=new(pBuff2)int(15);

[原材料] xushiwei 内存管理printf(
"pInt2=%dn",*pInt2);

[原材料] xushiwei 内存管理

[原材料] xushiwei 内存管理apr_pool_destroy(grandson);

[原材料] xushiwei 内存管理}


[原材料] xushiwei 内存管理apr_pool_destroy(child);
//释放子池,将内存归还给分配子

[原材料] xushiwei 内存管理
}

[原材料] xushiwei 内存管理apr_pool_destroy(root);
//释放父池,

[原材料] xushiwei 内存管理
apr_pool_terminate();//释放全局池,释放全局allocator,将内存归还给系统

[原材料] xushiwei 内存管理
return1;

[原材料] xushiwei 内存管理}


[原材料] xushiwei 内存管理

apr_pool中主要有3个对象,allocator、pool、block。pool从allocator申请内存,pool销毁的时候把内存归还allocator,allocator销毁的时候把内存归还给系统,allocator有一个owner成员,是一个pool对象,allocator的owner销毁的时候,allocator被销毁。在apr_pool中并无block这个单词出现,这里大家可以把从pool从申请的内存称为block,使用apr_palloc申请block,block只能被申请,没有释放函数,只能等pool销毁的时候才能把内存归还给allocator,用于allocator以后的pool再次申请。

我给的例子中并没有出现创建allocator的函数,而是使用的默认全局allocator。apr_pool提供了一系列函数操作allocator,可以自己调用这些函数:

apr_allocator_create

apr_allocator_destroy

apr_allocator_alloc

apr_allocator_free
创建销毁allocator
apr_allocator_owner_set

apr_allocator_owner_get
设置获取owner
apr_allocator_max_free_set 设置pool销毁的时候内存是否直接归还到操作系统的阈值
apr_allocator_mutex_set

apr_allocator_mutex_get
设置获取mutex,用于多线程


另外还有设置清理函数啊等等,不说了。自己去看include里的头文件好了:apr_pool.h和apr_allocator.h两个。源码.c文件里,APR_DECLARE宏声明的函数即是暴露给外部使用的函数。大家也可以仿造Loki(后文将介绍Loki)写个顶层类重载operator new操作子,其中调用apr_palloc,使用到的数据结构继承该类,则自动从pool中申请内存,如要完善的地方很多,自行去研究吧。

可以看出来apr_pool的一个大缺点就是从池中申请的内存不能归还给内存池,只能等pool销毁的时候才能归还。为了弥补这个缺点,apr_pool的实际使用中,可以申请拥有不同生命周期的内存池(类似与上面的例子程序中不同的大括号代表不同的生命周期,实际中,尽可以把大括号中的内容想象成不同的线程中的......),以便尽可能快的回收不再使用的内存。实际中apache也是这么做的。因此apr_pool比较适合用于内存使用的生命期有明显层次的情况。

至于担心allocator中的内存一旦申请就再也不归还给操作系统(当然最后进程退出的时候你可以调用销毁allocator归还,实际中网络服务程序都是一直运行的,找不到销毁的时机)的问题,就是杞人忧天了,如果在某一时刻,系统占用的内存达到顶峰,意味着以后还会有这种情况。是否能接受这个解释,就看个人的看法和系统的业务需求了,不能接受,就使用其它的内存池。个人觉得apr_pool还是很不错的,很多服务系统的应用场景都适用。

(2)obstack。glibc自带的内存池。原理与apr_pool相同。详细使用文档可以参阅

http://www.gnu.org/software/libc/manual/html_node/Obstacks.html。推荐apr_pool,这个就不再多说了。

(3)AutoFreeAlloc。许式伟的专栏http://blog.csdn.net/xushiweizh/category/265099.aspx

这个内存池我不看好。这个也属于一个变长的内存池,内存申请类似与apr_pool的pool/block层面,一次申请大内存作为pool,用于block的申请,同样block不回收,等pool销毁的时候直接归还给操作系统。这个内存池的方案,有apr_pool中block不能回收到pool的缺点,没有pool回收到allocator,以供下次继续使用的优点,不支持多线程。适合于单线程,集中使用内存的场景,意义不是很大。

posted on 2008-02-18 16:55cppexplore阅读(9558)评论(17)编辑收藏引用[原材料] xushiwei 内存管理

评论

#re: 【原创】系统设计之 内存管理2008-02-18 17:29CornerZhang谢谢!看来这个apr_pool不错.回复更多评论



#re: 【原创】系统设计之 内存管理2008-02-18 18:39eXile对于最后一个AutoFreeAlloc, 我碰巧也研究过, 其实对于你说的"没有pool回收到allocator,以供下次继续使用"的问题,它最近的实现中已经解决了.

在c++ 标准库实现SGI STL中还有一种内存池的实现, 就是用一系列固定长的内存池来实现一个不定长的内存池, 它曾经是gcc3中stl的默认实现, 但在gcc4中不再使用, 理由是在多线程情况下优化并不明显, 线程锁反而成为瓶颈. 所以有时候一个线程一个内存池也是一个选择.

回复更多评论



#re: 【原创】系统设计之 内存管理[未登录]2008-02-18 20:11CppExplore@CornerZhang

呵呵,apache很成功,apr_pool自然不会差。



@eXile

AutoFreeAlloc的发展方向应该就是apr_pool。apr_pool已经把变长的内存池发展到极致,当然这是当前看到的,或许以后有内存池会把变长内存池推到一个新的高度。:)

支持多线程的内存池都是从单线程加锁机制实现的,都提供无锁的实现。apr_pool也是,显式构造allocator后不调用apr_allocator_mutex_set就是无锁的实现。

后面的boost和loki的无锁和有锁的实现区别更是明显。回复更多评论



#re: 【原创】系统设计之 内存管理2008-02-19 12:33空明流转多线程的池子Lock-Free的解决办法很有前途。原子操作速度很快,没有锁的消耗。回复更多评论



#re: 【原创】系统设计之 内存管理[未登录]2008-02-19 15:59cppexplore@空明流转

这个现在还是只能停留在美好的展望阶段,不过这一天的到来不远了。

回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-02-19 21:49空明流转展望已经不是展望了。就是库中用的很少,但是不排除一些服务器上已经用了这个玩意了。回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-19 16:43xushiwei关于AutoFreeAlloc,推荐博主看一下以下两篇:



http://cpp.winxgui.com/cn:a-general-gc-allocator-scopealloc

http://cpp.winxgui.com/cn:lock-free-gc-allocator

回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-19 16:45xushiwei另外,AutoFreeAlloc与apr pools的性能对比:

http://cpp.winxgui.com/cn:gc-allocators-vs-apr-pools回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-19 17:34cppexplore@xushiwei

麻烦做下修改再测试:

void doAprPools1(LogT& log)

{

log.print("===== APR Pools =====n");

std::PerformanceCounter counter;

for (int i = 0; i < N; ++i)

{

apr_pool_t alloc;

apr_pool_create(&alloc, m_pool);

int
p = (int)apr_palloc(alloc, sizeof(int));

apr_pool_destroy(alloc);

}

counter.trace(log);

}

改成

void doAprPools1(LogT& log)

{

int i;

apr_pool_t
alloc;

apr_pool_create(&alloc, m_pool);

for (i = 0; i < N; ++i)

{

int p = (int)apr_palloc(alloc, sizeof(int));

}

apr_pool_destroy(alloc);



apr_pool_t alloc2;

apr_pool_create(&alloc2, m_pool);

log.print("===== APR Pools =====n");

std::PerformanceCounter counter;

for (i = 0; i < N; ++i)

{

int
p = (int*)apr_palloc(alloc2, sizeof(int));

}

counter.trace(log);

apr_pool_destroy(alloc2);

}

至于线程锁的使用开销,这里就先不考虑了。“apr_pool也是,显式构造allocator后不调用apr_allocator_mutex_set就是无锁的实现。 ”回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-23 13:51xushiweito cppexplore: 请留意我的测试意图。回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-24 12:28cppexplore@xushiwei

你的测试代码对apr-pool不公平,首先(1)作为服务器,关心是长期运行后的性能,而不是开始几个请求的性能,一个服务器可能365天无间断服务,而只拿系统启动2分钟的性能来衡量1年的性能显然不合适,而apr-pool开始申请内存是直接new,释放的时候才组织内存池结构。(2)对于集中处理的情况(类似你的测试代码),内存的申请是从同一个池中申请的,而不是申请一块内存,就必须先申请一个池。

你的测试代码,(1)是针对apr-pool性能最差的建池阶段 (2)每申请一块内存,反复的从allocator创建销毁内存池,和实际的使用不相符

而你的内存池,则没有建池阶段,直接栈中建池,我认为用上面写过apr-pool测试代码用来测试,才对apr-pool公平。



其实我觉得对内存池做这种性能对比没意义,首先这是变长内存池,不需要考虑释放,性能对比也就只是测试申请阶段的性能,而变长内存池都是在已有大内存上的指针滑动,都是常数步骤内完成。因此和算法之间对比性能不同,完善的内存池之间根本就没有性能比较的必要。回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-03-24 15:40xushiweito cppexplore: 我的测试分了两种情况,我想你关注的是测试2的对比数据。测试1在实际中并不常见,只是我要看的是allocator的伸缩性。至于内存池的效率,我的观念和你相反,既然它是基础设施,那么它的性能调优是非常关键的,比你去优化任何其他东西都来得有效。而算法之间我比较关心复杂度,而不是非常关心性能调优(微调)。回复更多评论



#re: 【原创】系统设计之 内存管理(一)[未登录]2008-03-25 09:04cppexplore@xushiwei

“它是基础设施,那么它的性能调优是非常关键的”,这句话我不反对,虽然我认为对变长内存池没必要。不过你的测试代码并没有反映apr_pool的真实性能。回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-04-25 16:19Simon不知道apr_pool使用在商业软件中是否有版权问题?回复更多评论



#re: 【原创】系统设计之 内存管理(一)2008-04-25 16:53cppexplore它使用Apache License。允许免费修改重发布,允许商业使用,允许不公布修改后的源代码。回复更多评论



#re: 【原创】技术系列之 内存管理(一)2008-10-23 13:23cuiapr_pool 不是预先申请大块内存吗? 不然变长内存池怎么实现?回复更多评论



#re: 【原创】技术系列之 内存管理(一)2009-03-23 17:41舵手内存,内存。。。回复更多评论原文链接: https://www.cnblogs.com/titer1/archive/2012/04/02/2430292.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/45970

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 下午10:26
下一篇 2023年2月8日 下午10:27

相关推荐