系列更新:C程序员应该知道的内存知识(一)C程序员应该知道的内存知识(二)C程序员应该知道的内存知识(三)内存C程序员应该知道的知识(4)这是本系列的第2篇,预计还会有2篇。有兴趣的同学记得关注接收推送,等不及的推荐阅读原文。先放镇楼:来源:Linux地址空间布局-byGustavoDuarte关于图片的解释可以参考上一篇。开始吧。了解堆上内存分配工具箱:brk()、sbrk()——修改数据段的大小[2](译注:见上图右中上界),申请访问原位置和新位置之间的内存。这样的话,堆上的内存分配和栈上的分配一样快(除去分页的开销,一般我们认为栈是锁在内存中的;译注:指的是栈不会被换出到磁盘,你可以使用mlocklimit操作系统使用swap用于一段地址空间)。但是这里还有一只猫,我是说,一个陷阱,见鬼。(翻译tunotecao:这个真的很难翻)char*block=sbrk(1024*sizeof(char));我们不能回收不再使用的内存块它不是线程安全的,因为堆是线程之间共享的接口(译注:sbrk)也很难移植,所以库函数禁止触及这个中断。man3sbrk--各种系统的sbrk使用了多种不同的参数类型,常见的有int,ssize_t,ptrdiff_t,intptr_t由于这些问题,libc需要统一的内存分配接口,具体实现有很多[3](译注:如glibc,jemalloc,tcmalloc等),但它们都为你提供了支持任意大小的线程安全的内存分配器...只是付出了代价-延迟,因为你必须引入锁机制并维护使用/available内存数据结构,以及额外的内存开销。此外,堆不是唯一的选择。在大内存分配的情况下,往往会用到内存映射段(MemoryMappingSegment,MMS)。man3malloc——一般来说,malloc()从堆上分配内存,...当分配的内存块大于MMAP_THRESHOLD时,glibc的malloc()实现使用私有匿名映射来分配内存。(译注:linux下mmap的flags参数是位图,其中两位分别是MAP_PRIVATE和MAP_ANONYMOUS,对应参考中提到的“private”和“anonymous”)下界和上界,建议比较图中右侧的标签)总是连续的(译注:这里指的是连续的虚拟地址空间,但每一页都可能映射到任意物理页),所以不能在中间打个洞来减少数据段的大小。比如这个场景:char*truck=malloc(1024*1024*sizeof(char));char*bike=malloc(sizeof(char));免费(卡车);堆分配器将增加brk来为卡车腾出空间。自行车也是如此。但是放车的时候,brk不能调低,因为自行车占了高地址。结果是您的进程可以重用卡车的内存,但除非自行车也被释放,否则不会将其返回给操作系统。当然你也可以使用mmap来分配卡车需要的空间。如果不放在堆内存段,不会影响程序break,但是这仍然不能解决分配小块内存造成的空洞(换句话说就是“造成碎片”)。注意free()并不总是收缩数据段,因为这是一个潜在的昂贵操作(参见下面的“需求分页的解释”)。这对于长时间运行的程序(例如守护进程)来说可能是个问题。有一个名为malloc_trim()可用于从堆顶部释放内存,但它可能非常慢,尤其是有很多小对象时,因此应谨慎使用。什么时候应该使用自定义分配器?有一些实际场景genericallocators有缺点,比如大量分配小的固定大小的内存。这可能看起来不是典型的场景,但它实际上经常发生。例如,用于查找的数据结构(通常是树,尝试)需要分配一个大量的节点来构建它们的层次结构。在这种情况下,不仅碎片会成为问题,而且数据局部性也会成为问题。缓存效率高的数据结构会将key放在一起(最好在同一个内存页),而不是和数据混在一起。默认分配器不能保证下一次分配仍然在同一个块中,更糟糕的是分配小单元的额外空间开销。解决方案在这里:X来源:wadem的Slab,在Flickr(CC-BY-SA)来源:IBM-Linuxslab分配器剖析Slab分配器工具箱:posix_memalign()-分配对齐的内存Bonwick的这篇文章用于内核对象缓存[4]介绍了slab分配器的原理,在用户空间也可以使用。好的,我们对绑定到CPU的slab不感兴趣——也就是说,你向分配器请求一块内存,例如,一整页,然后将其切成许多固定大小的小块。如果每个小块至少可以存储一个指针或一个整数,则可以将它们串成一个链表,头部指向第一个空闲元素。/*超级简单的slab。*/structslab{void**head;}??;/*创建页面对齐的slab*/structslab*slab=NULL;posix_memalign(&slab,page_size,page_size);slab->head=(void**)((char*)slab+sizeof(structslab));/*创建一个以NULL结尾的slabfreelist*/char*item=(char*)slab->head;for(unsignedi=0;ihead;slab->head=(void**)ptr;/*分配一个元素*/if((item=slab->head)){slab->head=(void**)*item;}else{/*没有剩余元素。*/}注解:对于page_size=4KB,PAGESIZE_BITS=0xFFFFF000的page,ptr&PAGESIZE_BITS清空了低12位,恰好是这个page的开始,也就是这个slab的起始地址。很棒,不过还有binning,变长存储,cachealiasing,coffeeBecause(译注:这应该是作者的调侃),...怎么办?看看我之前为KnotDNS[6]或实现这些要点的其他库编写的代码。例如,(gasp),在glib中有一个简洁的文档[7]称其为“内存片”。译注:slab分配器适用于大量小对象的分配,可以避免常见的碎片问题;在内核中的实现还可以支持硬件缓存对齐,从而提高缓存利用率。内存池工具箱:obstack_alloc()-从对象栈中分配内存你每次申请一大块内存,然后像切蛋糕一样切小块,直到不够用,再申请一大块。此外,当您完成所有操作后,您可以收工并立即释放所有空间。是不是特别傻?因为它确实如此,但仅限于特定场景。您不需要考虑同步,也不需要考虑释放。没有忘记回收的陷阱,数据的局部性更符合预期,小对象的开销几乎为零。这种模式特别适用于很多类型的任务,包括短暂的重复赋值(如网络请求处理),以及长期不可变的数据(如冻结集;译注:创建后不会改变的集合)。您不再需要一个一个地释放对象。如果你能合理地猜测平均需要多少内存,你也可以释放多余的内存用于其他目的。这将内存分配问题简化为简单的指针算法。而且您很幸运——GNUlibc提供了,呃,一整套API来做到这一点。这就是obstacks,使用栈来管理对象。它的HTML文档[8]写得不好,但除了这些小缺陷外,它允许您进行基于内存池的分配和释放(部分和全部)。/*定义块分配器。*/#defineobstack_chunk_allocmalloc#defineobstack_chunk_freefree/*初始化obstack并分配一堆动物。*/structobstackanimal_stack;obstack_init(&animal_stack);char*bob=obstack_alloc(&animal_stack,);char*fred=obstack_alloc(&animal_stack,sizeof(animal));char*roger=obstack_alloc(&animal_stack,sizeof(animal));/*释放fred之后的所有东西(即fred和roger)。*/obstack_free(&animal_stack,fred);/*释放一切。*/obstack_free(&animal_stack,NULL);展示。由于对象是在一个栈中管理的(先分配,最后释放),当fred被释放时,fred和fred之后分配的对象(r??oger)会一起被释放。另一个小技巧:您可以在堆栈顶部扩展对象。比如缓冲输入,变长数组,或者使用代替realloc()-strcpy()的方式(译注:重新分配内存,然后复制原始数据):/*这是错误的,我最好取消它。*/obstack_grow(&animal_stack,"long",4);obstack_grow(&animal_stack,"fred",5);obstack_free(&animal_stack,obstack_finish(&animal_stack));/*这次是真的。*/obstack_grow(&animal_stack,"long",4);obstack_grow(&animal_stack,"bob",4);char*result=obstack_finish(&animal_stack);printf("%s\n",结果);/*"longbob"*/是的,只看最后四行;使用obstack_grow扩展栈顶元素占用的内存,扩展后调用obstack_finish结束扩展,返回栈顶元素地址。需求分页的解释工具箱:mlock()-锁定/解锁内存(避免被换出交换)madvise()-建议(内核)如何处理给定的内存范围通用内存分配器不返回的原因之一内存给系统立即是这个操作是昂贵的。系统需要做两件事:(1)创建虚拟页面到真实页面的映射,以及(2)给你一个归零的真实页面。这个真实的页面叫做框架,现在你知道区别了。每一帧都必须清除,毕竟你不希望操作系统从其他进程泄露秘密,对吧?这是另一个技巧,还记得过度使用吗?虚拟内存分配器只认真对待事务的初始部分,然后发挥它的魔力——页表中的大部分页不指向真实页,而是指向一个全为零的特殊页。每次尝试访问这个页面时,都会触发页面错误,这意味着内核暂停进程的执行,分配一个真正的页面,更新页表,然后恢复进程,假装什么都没发生。这是我能用一句话给出的最好的解释,这里有更详细的版本[9]。这也称为“按需分页”或“延迟加载”。斯波克船长说“人类无法召唤未来”,但在这里你可以操纵它。(译注:星际迷航,史波克说“一个人无法召唤未来。”柯克说“但一个人可以改变现在。”)内存管理器不是先知,他只是保守地预测你访问内存的方式,而你可能不太了解(您将如何访问内存)。(如果你知道的话)你可以在物理内存中锁定一个连续的内存块以避免随后的页面错误:char*block=malloc(1024*sizeof(char));mlock(block,1024*sizeof(char));(注解:访问一个被换出到swap的page会触发pagefault,然后内存管理器会从磁盘加载page,这会造成严重的性能问题;用mlock锁定这个区域后,OS将不会被换出到swap由操作系统决定;例如,MySQL会在允许的情况下使用mlock将索引保存在物理内存中)注意:您也可以根据自己的内存使用模式向内核提出建议char*block=malloc(1024*sizeof(block));madvise(块,1024*sizeof(块),MADV_SEQUENTIAL);建议的解释取决于平台,系统甚至可能会选择忽略它,但大多数平台都能很好地处理它。但并不是所有的建议都得到很好的支持,有些平台可能会改变建议的语义(比如MADV_FREE去除私有脏页;翻译注:“dirtypage”,脏页,指的是分配后写入,可能已经未使用保存的数据),但最常用的是MADV_SEQUENTIAL、MADV_WILLNEED和MADV_DONTNEED这三者。译注:还记得《踩坑记:go服务内存暴涨》中对MADV_DONTNEED和MADV_FREE的解释吗?这里我们回顾MADV_DONTNEED:不再需要的页面,Linux会立即回收MADV_FREE:不再需要的页面,Linux会在需要的时候回收MADV_SEQUENTIAL:将被顺序访问的页面,内核可以通过预读后续页面进行优化,已经访问过的页面也可以提前回收MADV_WILLNEED:即将访问,建议提前加载内核,作为休息点,这篇文章暂时放在这里。下一篇继续翻译下一节?,还有很多有趣的内容,敬请关注~顺便把之前的文章发上来,祝大家有个充实的五一假期~《踩坑记:go服务内存暴涨》《TCP:学得越多越不懂》《UTF-8:一些好像没什么用的冷知识》《关于RSA的一些趣事》《程序员面试指北:面试官视角》欢迎关注供参考链接:1.C程序员应该了解的有关内存的知识2。sbrk(2)-Linux手册页3。C编程/stdlib.h/malloc4。Slab分配器:对象缓存内核内存分配器5。linustorvalds回答了你的问题6。KnotDNS-slab.h7.glib-内存片GNUlibc-障碍内核如何管理你的内存