CppExplore

一切像雾像雨又像风

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  29 随笔 :: 0 文章 :: 280 评论 :: 0 Trackbacks

作者: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实现的原理,知道如何使用就足够了。深入细节只能是浪费脑细胞,当然完全凭个人兴趣爱好了。
    这里举例说下简单的使用:

#include "apr_pools.h"
#include 
<stdio.h>
#include 
<new>

int main()
{
    apr_pool_t 
*root;
    apr_pool_initialize();
//初始化全局分配子(allocator),并为它设置mutext,以用于多线程环境,初始化全局池,指定全局分配

子的owner是全局池
    apr_pool_create(
&root,NULL);//创建根池(默认父池是全局池),根池生命期为进程生存期。分配子默认为全局分配子
    {
        apr_pool_t 
*child;
        apr_pool_create(
&child,root);//创建子池,指定父池为root。分配子默认为父池分配子
        void *pBuff=apr_palloc(child,sizeof(int));//从子池分配内存
        int *pInt=new (pBuff)  int(5);//随便举例下基于已分配内存后,面向对象构造函数的调用。
        printf("pInt=%d\n",*pInt);
        
{
            apr_pool_t 
*grandson;
            apr_pool_create(
&grandson,root);
            
void *pBuff2=apr_palloc(grandson,sizeof(int));
            
int *pInt2=new (pBuff2)  int(15);
            printf(
"pInt2=%d\n",*pInt2);    

            apr_pool_destroy(grandson);
        }

        apr_pool_destroy(child);
//释放子池,将内存归还给分配子
    }

    apr_pool_destroy(root);
//释放父池,
    apr_pool_terminate();//释放全局池,释放全局allocator,将内存归还给系统
    return 1;
}

    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:55 cppexplore 阅读(14405) 评论(17)  编辑 收藏 引用

评论

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

# re: 【原创】系统设计之 内存管理 2008-02-18 18:39 eXile
对于最后一个AutoFreeAlloc, 我碰巧也研究过, 其实对于你说的"没有pool回收到allocator,以供下次继续使用"的问题,它最近的实现中已经解决了.
在c++ 标准库实现SGI STL中还有一种内存池的实现, 就是用一系列固定长的内存池来实现一个不定长的内存池, 它曾经是gcc3中stl的默认实现, 但在gcc4中不再使用, 理由是在多线程情况下优化并不明显, 线程锁反而成为瓶颈. 所以有时候一个线程一个内存池也是一个选择.
  回复  更多评论
  

# re: 【原创】系统设计之 内存管理[未登录] 2008-02-18 20:11 CppExplore
@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:59 cppexplore
@空明流转
这个现在还是只能停留在美好的展望阶段,不过这一天的到来不远了。
  回复  更多评论
  

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

# re: 【原创】系统设计之 内存管理(一) 2008-03-19 16:43 xushiwei
关于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:45 xushiwei
另外,AutoFreeAlloc与apr pools的性能对比:
http://cpp.winxgui.com/cn:gc-allocators-vs-apr-pools  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(一) 2008-03-19 17:34 cppexplore
@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:51 xushiwei
to cppexplore: 请留意我的测试意图。  回复  更多评论
  

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

# re: 【原创】系统设计之 内存管理(一)[未登录] 2008-03-25 09:04 cppexplore
@xushiwei
“它是基础设施,那么它的性能调优是非常关键的”,这句话我不反对,虽然我认为对变长内存池没必要。不过你的测试代码并没有反映apr_pool的真实性能。  回复  更多评论
  

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

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

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

# re: 【原创】技术系列之 内存管理(一) 2009-03-23 17:41 舵手
内存,内存。。。  回复  更多评论
  


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理