您的位置:首页 > 其它

技术系列之 内存管理(一)

2009-09-26 08:39 246 查看
服务器设计人员在一段时间的摸索后,都会发现:服务器性能的关键在于内存。从收包到解析,到消息内存的申请,到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,这个就不再多说了。

(本文首次发表于http://www.cppblog.com/CppExplore/archive/2008/02/18/42890.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: