续上篇:html
这是本系列的第二篇,预计还会有2篇,感兴趣的同窗记得关注,以便接收推送,等不及的推荐阅读原文。node
先放图镇楼: linux
来源:Linux地址空间布局 - by Gustavo Duartegit
关于图片的解释可参见上篇。程序员
开始吧。github
工具箱:面试
堆上的内存分配,最简单的实现能够是修改 program break[2](译注:参见上图中部右侧)的上界,申请对原位置和新位置之间内存的访问权限。若是就这么搞的话,堆上内存分配和栈上分配同样快(除了换页的开销,通常咱们认为栈是被锁定在内存中;译注:指栈不会被换出到磁盘,能够用mlock限制OS对一段地址空间使用swap)。但这里仍是有只猫(cat),我是说,有点毛病(catch),见鬼。(译tu注cao:这个真难翻)c#
char *block = sbrk(1024 * sizeof(char));
man 3 sbrk — 各类系统的 sbrk 使用多种不一样的参数类型,常见的包括 int, ssize_t, ptrdiff_t, intptr_t
因为这些问题,libc要求实现统一的内存分配接口,具体实现有不少[3](译注:例如glibc,jemalloc,tcmalloc等),但都给你提供了线程安全、支持任意尺寸的内存分配器……只是要付出点代价——延迟,由于得引入锁机制,以及用来维护已用/可用内存的数据结构,和额外的内存开销。还有,堆也不是惟一的选项,在大块内存分配的状况下经常也会用内存映射段(Memory Mapping Segment,MMS)。segmentfault
man 3 malloc —— 通常来讲,malloc() 从堆上分配内存, ... 当分配的内存块大于 MMAP_THRESHOLD 时,glibc的 malloc() 实现使用私有匿名映射来分配内存。
(译注:Linux 下 mmap 的 flags 参数是个 bitmap,其中有两个 bit 分别为 MAP_PRIVATE、MAP_ANONYMOUS,对应引用中提到的“私有”、“匿名”) api
因为堆空间在 start_brk 和 brk (译注:即 heap 的下界和上界,建议对照图中右侧的标注)之间老是连续的(译注:这里指的是虚拟地址空间连续,但其中每一页均可能映射到任一物理页),所以你没法在中间打个洞来减小数据段的尺寸。好比这个场景:
char *truck = malloc(1024 * 1024 * sizeof(char)); char *bike = malloc(sizeof(char)); free(truck);
堆分配器将会调大 brk,以便给 truck 腾出空间。对于 bike 也同样。可是当 truck 被释放后,brk 不能被调小,由于 bike 正占着高位的地址。结果是,你的进程能够重用 truck 的内存,但不能退还给OS,除非 bike 也被释放。固然你也能够用 mmap 来分配 truck 所需的空间,不放在堆内存段里,就能够不影响 program break ,但这仍然没法解决分配小块内存致使的空洞(换句话说就是“引发碎片化”)。
注意 free() 并不老是会缩小数据段,由于这是一个有很大潜在开销的操做(参见后文“对按需调页的解释”)。对于须要长时间运行的程序(例如守护进程)来讲这会是个问题。有个叫 malloc_trim() 的 GNU 扩展能够用来从堆顶释放内存,但可能会慢得使人蛋疼,尤为对于大量小对象的状况,因此应该尽可能少用。
有一些实际场景中通用分配器有短板,例如大量分配固定尺寸的小内存。这看起来不像是典型的场景,但实际上出现得很频繁 。例如,用于查找的数据结构(典型如树、字典树)须要分配大量节点用于构造其层次结构。在这个场景下,不只碎片化会是个问题,数据的局部性也是。cache效率高的数据结构会将key放在一块儿(最好在同一个内存页),而不是和数据混在一块儿。默认的分配器不能保证下次分配时还在同一个block,更糟的是分配小单元的额外空间开销。解决办法在此:
X
来源: Slab by wadem, on Flickr (CC-BY-SA)
(译注:原图没法打开了,另贴一张)
来源:IBM - Linux slab 分配器剖析
工具箱:
Bonwick 为内核对象缓存写的这篇文章[4]介绍了 slab 分配器的原理,也可用于用户空间。Okay,咱们对绑定在CPU上的 slab 不感兴趣 —— 就是你找分配器要一块内存,例如说一整页,而后切成不少固定大小的小块。若是每一个小块都能保存至少一个指针或一个整数,你就能够把他们串成一个链表 ,表头指向第一个空闲元素。
/* Super-simple slab. */ struct slab { void **head; }; /* Create page-aligned slab */ struct slab *slab = NULL; posix_memalign(&slab, page_size, page_size); slab->head = (void **)((char*)slab + sizeof(struct slab)); /* Create a NULL-terminated slab freelist */ char* item = (char*)slab->head; for(unsigned i = 0; i < item_count; ++i) { *((void**)item) = item + item_size; item += item_size; } *((void**)item) = NULL;
译注:
posix_memalign
分配了一页内存,而且对齐到页边界,这意味着正好拿到了一个物理页item_count
= page_size
/ item_size
;其中 item_size
能够根据应用须要指定。而后内存分配就简单到只要弹出链表的头结点就好了,内存释放则是插入头结点。这里还有个优雅的小技巧:既然 slab 对齐到了页边界,你只要将指针向下取整到 page_size 就能获得这个 slab 的指针。
/* Free an element */ struct slab *slab = (void *)((size_t)ptr & PAGESIZE_BITS); *((void**)ptr) = (void*)slab->head; slab->head = (void**)ptr; /* Allocate an element */ if((item = slab->head)) { slab->head = (void**)*item; } else { /* No elements left. */ }
译注:对于 page_size = 4KB 的页面,PAGESIZE_BITS = 0xFFFFF000,ptr & PAGESIZE_BITS 清零了低12位,正好是这一页的开始,也就是这个slab的起始地址。
太棒了,可是还有binning(译注:应该是指按不一样的长度分桶),变长存储,cache aliasing(译注:同一个物理地址中的数据出如今多个不一样的缓存行中),咖啡因(译注:这应该是做者在逗逼了),...怎么办?能够看看我以前为 Knot DNS 写的代码[6],或者其余实现了这些点的库。例如,(喘口气),glib 里有个很整齐的文档[7],把它称为“memory slices”。
译注:slab 分配器适合大量小对象的分配,能够避免常见的碎片问题;在内核中的实现还能够支持硬件缓存对齐,从而提升缓存的利用率。
工具箱:
(译注:指 GNU 的 obstack ,用stack来保存object的内存池实现)
正如slab分配器同样,内存池比通用分配器好的地方在于,你每次申请一大块内存,而后像切蛋糕同样一小块一小块切出去,直到不够用了,而后你再申请一大块。还有,当你都处理完了之后,你就能够收工,一次性释放全部空间。
是否是特别傻瓜化?由于确实如此,但只是针对特定场景如此。你不须要考虑同步,也不须要考虑释放。再没有忘记回收的坑了,数据的局部性也更加符合预期,并且对于小对象的开销也几乎为0。
这个模式特别适合不少类型的任务,包括短生命周期的重复分配(例如网络请求处理),和长生命周期的不可变数据(例如frozen set;译注:建立后再也不改变的集合)。你再也不须要逐个释放对象(译注:能够最后批量释放)。若是你能合理推测出平均须要多少内存,你还能够将多余的内存释放,以便用于其余目的。这能够将内存分配问题简化成简单的指针运算。
并且你很走运 —— GNU libc 提供了,嗬,一整套API来干这事儿。这就是 obstacks ,用栈来管理对象。它的 HTML 文档[8] 写得不咋地,不过抛开这些小缺陷,它容许你完成基于内存池的分配和回收(包括部分回收和全量回收)。
/* Define block allocator. */ #define obstack_chunk_alloc malloc #define obstack_chunk_free free /* Initialize obstack and allocate a bunch of animals. */ struct obstack animal_stack; obstack_init (&animal_stack); char *bob = obstack_alloc(&animal_stack, sizeof(animal)); char *fred = obstack_alloc(&animal_stack, sizeof(animal)); char *roger = obstack_alloc(&animal_stack, sizeof(animal)); /* Free everything after fred (i.e. fred and roger). */ obstack_free(&animal_stack, fred); /* Free everything. */ obstack_free(&animal_stack, NULL);
译注:obstack这些api实际都是宏;须要经过宏来指定找OS分配和回收整块内存用的方法,如上第二、3行所示。因为对象使用栈的方式管理(先分配的最后释放),因此释放 fred 的时候,会把 fred 和在 fred 以后分配的对象(roger)一块儿释放掉。
还有个小技巧:你能够扩展栈顶的那个对象。例如带缓冲的输入,变长数组,或者用来替代 realloc()-strcpy() 模式(译注:从新分配内存,而后把原数据拷贝过去):
/* This is wrong, I better cancel it. */ obstack_grow(&animal_stack, "long", 4); obstack_grow(&animal_stack, "fred", 5); obstack_free (&animal_stack, obstack_finish(&animal_stack)); /* This time for real. */ obstack_grow(&animal_stack, "long", 4); obstack_grow(&animal_stack, "bob", 4); char *result = obstack_finish(&animal_stack); printf("%s\n", result); /* "longbob" */
译注:前三行是做者逗逼了,看后四行就行;用 obstack_grow 扩展栈顶元素占用的内存,扩展结束后调用 obstack_finish 结束扩展,并返回栈顶元素的地址。
工具箱:
通用内存分配器不当即将内存返回给系统的缘由之一是,这个操做开销很大。系统须要作两件事:(1) 创建虚拟页到真实(real)页的映射,和 (2) 给你一个清零的真实页。这个真实页被称为帧(frame),如今你知道它们的差异了。每一帧都必须被清空,毕竟你不但愿 OS 泄漏其余进程的秘密,对吧。这里还有个小技巧,还记得 overcommit 吗?虚拟内存分配器只把这个交易刚开始的那部分当回事,而后就开始变魔术了 —— 页表里的大部分页面并不指向一个真实页,而是指向一个特殊的全 0 页面。
每次你想要访问这个页面时,就会触发一个 page fault,这意味着内核会暂停 进程的执行,分配一个真实页、更新页表,而后恢复进程,并伪装什么也没发生。这是汇总在一句话里、我能作出的最好解释了,这里[9]还有更个详细的版本。这也被称做“按需调页”(demand paging) 或 “延迟加载”(lazy loading)。
斯波克船长说“人没法召唤将来”,但这里你能够操控它。
(译注:星际迷航,斯波克说“One man cannot summon the future.”,柯克说“But one man can change the present.”)
内存管理器不是先知,他只是保守地预测你访问内存的方式,而你本身也未必更清楚(你将会怎样访问内存)。(若是你知道)你能够将一段连续的内存块锁定在物理内存中,以免后续的page fault:
char *block = malloc(1024 * sizeof(char)); mlock(block, 1024 * sizeof(char));
(译注:访问被换出到swap的页面会触发page fault,而后内存管理器会从磁盘中载入页面,这会致使较严重的性能问题;用 mlock 将这段区域锁定后,OS就不会被操做系统换出到swap;例如,在容许的状况下,MySQL会用mlock将索引保持在物理内存中)
注意:你还能够根据本身的内存使用模式,给内核提出建议
char *block = malloc(1024 * sizeof(block)); madvise(block, 1024 * sizeof(block), MADV_SEQUENTIAL);
对建议的解释是平台相关的,系统甚至可能选择忽略它,但大部分平台都处理得很好。但不是全部建议都有良好的支持,有些平台可能会改变建议的语义(如MADV_FREE移除私有脏页;译注:“脏页”,dirty page,是指分配之后有过写入,其中可能有未保存的数据),可是最经常使用的仍是MADV_SEQUENTIAL, MADV_WILLNEED, 和 MADV_DONTNEED 这神圣三人组(译注:holy trinity,圣经里的三位一体,做者用词太跳脱……)。
译注:还记得《踩坑记:go服务内存暴涨》里对 MADV_DONTNEED 和 MADV_FREE 的解释吗?这里再回顾下
又到休息点,这篇暂时到这里。
下一篇会继续翻译下一节《Fun with memory mapping》,还有不少有意思的内容,敬请关注~
顺便再贴下以前推送的几篇文章,祝过个充实的五一假期~
参考连接:
1. What a C programmer should know about memory
2. sbrk(2) - Linux man page
3. C Programming/stdlib.h/malloc
4. The Slab Allocator: An Object-Caching Kernel Memory Allocator
5. linus torvalds answers your questions
6. Knot DNS - slab.h
7. glib - memory slices