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

    #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 阅读(9558) 评论(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 舵手
    内存,内存。。。 回复 更多评论 
  • 相关阅读:
    虚拟主机服务器错误404解决方法
    虚拟主机服务器错误404解决方法
    虚拟主机服务器错误404解决方法
    以太坊:Truffle开发入门
    区块链是低成本的工业化基因测序解决方案
    区块链公证和知识产权(IP)保护原理
    区块链公证和知识产权(IP)保护应用场景
    ACOUG China Tour 2019上海站,等你来约!
    解析MySQL基础架构及一条SQL语句的执行流程和流转
    jquery 查找节点
  • 原文地址:https://www.cnblogs.com/titer1/p/2430292.html
Copyright © 2011-2022 走看看