您的位置:首页 > 运维架构 > Linux

定制linux内存管理

2014-09-24 23:44 211 查看
一 malloc寻根linux下的malloc实现又叫ptmalloc(ptmalloc是dlmalloc的一种变形,所以有些地方也会直接说成dlmalloc)当我们调用malloc时,根据申请的大小会分成截然不同的几种处理流程。小内存会进入“定长内存”系统;大内存则进入“变长内存”系统(

备注2)一系列空闲内存链表。链表中的空闲内存块都是一样大的。

虽然定长内存系统的分配和释放极快。但是在分配大块内存时,定长内存分配会导致大量内存碎片(内碎片),关于内存碎片分类和优化我们会在后续章节再讨论。

“变长内存”系统也会组织一系列空闲内存链表。链表中的空闲块通常是不一样大的: 


变长内存会按照大小范围组织成很多个不同的链表,例如:10K-15K属于链表1;16K-23K属于链表2;24K-39K属于链表3;

总体来说,变长内存的性能比定长内存要差。分配时需要遍历会导致耗时增加,而且变长内存通常需要分裂内存块,会导致外碎片增加。

二 细节决定成败从整体设计上来看,malloc做的相当好。但是当我们深入一些关键的细节时,会发现malloc针对一些特殊场景,会出现很严重的问题1 管理小内存块时浪费了大量的空间每个内存块需要一个size_t的空间来存放本内存块的大小,供释放时使用。以64位机器为例,size_t是8字节长的。而且内存块必须在2*size_t的长度上对齐。所以当我们malloc(26)时,实际消耗的内存是48字节,浪费了45%的内存。另外内存块有一个最小大小4*size_t。在64位机器上,最小的分配单位就是32字节。当我们需要分配大量小结构体时,malloc会导致极大的内存浪费。验证代码请参考附件中的mem_little.cpp

3 释放内存时可能因为brk而被卡住malloc的主分配区是通过brk向内核申请内存的,非主分配区通过mmap模拟brk。而brk是一个线性的分配器,类似栈。简单来说,如果我先malloc了5G内存;再malloc 16字节,抓住这16字节不释放;然后释放前面的5G内存,这5G内存是无法还给系统的。验证代码请参考附件中的mem_brk.cppmalloc的这个特性对于需要长期持有而不释放的内存是极其不友好的。而且一旦出现内存泄漏,哪怕只漏了几个字节,也可能把几个G的内存释放卡住。

5 低效的realloc当需要扩大已分配的内存块时,ptmalloc将尝试从尾部扩展内存块。但是当系统内存压力较大时,这个尝试的成功率是不高的,这时就需要分配一个全新的内存块并把旧数据拷贝过去,而这将引入极其巨大的性能消耗(

我们做的是一款http正向缓存代理服务器,约等于带强大缓存功能的nginx具体目标是在一台8核16G内存的机器上承载每秒4Gb的http流量,100万并发连接。我们需要缓存整个互联网,所以需要上亿个10字节的结构体来存储url信息;另外还需要处理每秒4Gb的http socket数据,并尝试进行缓存写入和读出。在连接建立时我们是不知道每个连接需要多大缓冲区的,这就需要大量的realloc来扩大已有缓冲区。当用户下载1GB的文件时,原始服务器先将1GB文件发给我们,我们再转发给用户,这将持续相当长的一段时间。在这个过程中,我们可以把已转发给用户的部分内存先释放掉,但是可惜malloc不支持部分free。

在设计内存管理系统时,我们主要关心三个方面:

分配和释放效率

1 内存利用率方面主要是三个问题:

如果对大内存块采用定长内存系统,前后两个定长内存块之间的大小会差的较大,从而产生较大的内碎片。这也是为什么绝大多数内存管理系统,仅对小内存块使用定长内存。

外碎片:因为空闲内存块太小导致有内存又无法分配叫外碎片如下图,经过长时间malloc-free后,空闲内存(绿色)被分隔到成很多小碎片,就无法再malloc出大块内存了

无法还给系统:在上一节中已经举例说明过这个问题,free将内存还给内存管理器后可能无法归还给系统,导致其它进程分配内存失败

2 分配释放效率:定长分配和释放往往都是O(1)的效率

如果定长内存和变长内存交错在一起,定长内存会经常和变长内存合并,使得定长内存的优势体现不出来。

五 slab定长内存池 linux内核采用的slab算法是一个极好的定长内存管理算法

同时,这还将极大的减少定长内存和变长内存交错的情况,提升分配和释放效率。要让slab达到最高效率,我们需要事先对slab系统进行初始化,声明我们需要用到定长内存块大小(通常是某个频繁使用的struct/class),最好还能手动设定一下桶的大小。这也是为什么malloc不采用slab算法的原因。

通过支持非连续内存,我们将变长内存池的问题转化成了定长内存的问题,大内存使用过程中碰到的几个最主要问题,都已经解决了。因为所有内存块都是定长的,我们不再会有任何外碎片的问题,跑上100年也不会有外碎片。



非连续内存有一个极大的缺陷:无法用常规的strcmp、memcpy系列函数处理;也无法转换成string;使用时一不小心就会踩内存。必须提供一系列函数对非连续内存池进行处理,例如jump_get_next来获取下一个字符并自动判断是否需要跳到下一个内存块。在08年阅读nginx代码时,发现nginx就已经实现了一套非连续内存池的代码,但是并没有发现任何代码在使用这个非连续内存池。对于开源这样人员多变的开发环境,确实不太适合使用非连续内存池;但是对于人员稳定性和素质有保障的商业公司,完全可以尝试用非连续内存来提升性能。

备注2:实际上这里有一个lazy的机制。第一次malloc时并没有事先准备好的定长内存,此时需要从更高层的内存块中(例如largebin或top),速度较慢。free时,如果大小属于fastbin会直接放入定长内存池中。如果属于smallbin,在尝试和相邻内存块合并失败时,也会放入定长内存池。在经过一段时间的malloc和free之后,如果没有触发大量的合并,我们就可以幸运的拥有一个快速的定长内存池了

应用层消耗

    Tcp协议栈

    copy_to_user等

 15%    其它内核函数40%可以看到copy_to_user和copy_from_user加起来占了20%的CPU。这两个内核函数属于recv和send的内核实现,负责在内核和应用层间拷贝socket数据。换句话说,如果我们通过realloc将所有socket缓冲区拷贝了一遍,就会增加约20%性能消耗<span
font-size:12pt;"="" style="word-wrap: break-word;"> 

验证代码压缩包:

 

mem_test.zip
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: