关注、星标公众号,直达精彩内容

来源:https://www.lmlphp.com/user/1774/article/item/19294/

编辑整理:技术让梦想更伟大 | 李肖遥

人们似乎认为编写垃圾回收机制是很难的,是一种只有少数智者和Hans Boehm(et al)才能理解的高深魔法。我认为编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc样例难度是相当的。

在开始之前有一些重要的事情需要说明一下:第一,我们所写的代码是基于Linux Kernel的,注意是Linux Kernel而不是GNU/Linux。第二,我们的代码是32bit的。第三,请不要直接使用这些代码。我并不保证这些代码完全正确,可能其中有一些我还未发现的小的bug,但是整体思路仍然是正确的。好了,让我们开始吧。

编写malloc

最开始,我们需要写一个内存分配器(memmory allocator),也可以叫做内存分配函数(malloc function)。最简单的内存分配实现方法就是维护一个由空闲内存块组成的链表,这些空闲内存块在需要的时候被分割或分配。当用户请求一块内存时,一块合适大小的内存块就会从链表中被移除并分配给用户。如果链表中没有合适的空闲内存块存在,而且更大的空闲内存块已经被分割成小的内存块了或内核也正在请求更多的内存(译者注:就是链表中的空闲内存块都太小不足以分配给用户的情况)。那么此时,会释放掉一块内存并把它添加到空闲块链表中。

在链表中的每个空闲内存块都有一个头(header)用来描述内存块的信息。我们的header包含两个部分,第一部分表示内存块的大小,第二部分指向下一个空闲内存块。

将头(header)内嵌进内存块中是唯一明智的做法,而且这样还可以享有字节自动对齐的好处,这很重要。

由于我们需要同时跟踪我们“当前使用过的内存块”和“未使用的内存块”,因此除了维护空闲内存的链表外,我们还需要一条维护当前已用内存块的链表(为了方便,这两条链表后面分别写为“空闲块链表”和“已用块链表”)。我们从空闲块链表中移除的内存块会被添加到已用块链表中,反之亦然。

现在我们差不多已经做好准备来完成malloc实现的第一步了。但是再那之前,我们需要知道怎样向内核申请内存。

动态分配的内存会驻留在一个叫做堆(heap)的地方,堆是介于栈(stack)和BSS(未初始化的数据段-你所有的全局变量都存放在这里且具有默认值为0)之间的一块内存。堆(heap)的内存地址起始于(低地址)BSS段的边界,结束于一个分隔地址(这个分隔地址是已建立映射的内存和未建立映射的内存的分隔线)。为了能够从内核中获取更多的内存,我们只需提高这个分隔地址。为了提高这个分隔地址我们需要调用一个叫作 sbrk 的Unix系统的系统调用,这个函数可以根据我们提供的参数来提高分隔地址,如果函数执行成功则会返回以前的分隔地址,如果失败将会返回-1。

利用我们现在知道的知识,我们可以创建两个函数:morecore()和add_to_free_list()。当空闲块链表缺少内存块时,我们调用morecore()函数来申请更多的内存。由于每次向内核申请内存的代价是昂贵的,我们以页(page-size)为单位申请内存。页的大小在这并不是很重要的知识点,不过这有一个很简单解释:页是虚拟内存映射到物理内存的最小内存单位。接下来我们就可以使用add_to_list()将申请到的内存块加入空闲块链表。

现在我们有了两个有力的函数,接下来我们就可以直接编写malloc函数了。我们扫描空闲块链表当遇到第一块满足要求的内存块(内存块比所需内存大即满足要求)时,停止扫描,而不是扫描整个链表来寻找大小最合适的内存块,我们所采用的这种算法思想其实就是首次适应(与最佳适应相对)。

注意:有件事情需要说明一下,内存块头部结构中size这一部分的计数单位是块(Block),而不是Byte。

注意这个函数的成功与否,取决于我们第一次使用时是否使 freep = &base 。这点我们会在初始化函数中进行设置。

尽管我们的代码完全没有考虑到内存碎片,但是它能工作。既然它可以工作,我们就可以开始下一个有趣的部分-垃圾回收!

标记和清扫

我们说过垃圾回收器会很简单,因此我们尽可能的使用简单的方法:标记和清除方式。这个算法分为两个部分:

首先,我们需要扫描所有可能存在指向堆中数据(heap data)的变量的内存空间并确认这些内存空间中的变量是否指向堆中的数据。为了做到这点,对于可能内存空间中的每个字长(word-size)的数据块,我们遍历已用块链表中的内存块。如果数据块所指向的内存是在已用链表块中的某一内存块中,我们对这个内存块进行标记。

第二部分是,当扫描完所有可能的内存空间后,我们遍历已用块链表将所有未被标记的内存块移到空闲块链表中。

现在很多人会开始认为只是靠编写类似于malloc那样的简单函数来实现C的垃圾回收是不可行的,因为在函数中我们无法获得其外面的很多信息。例如,在C语言中没有函数可以返回分配到堆栈中的所有变量的哈希映射。但是只要我们意识到两个重要的事实,我们就可以绕过这些东西:

第一,在C中,你可以尝试访问任何你想访问的内存地址。因为不可能有一个数据块编译器可以访问但是其地址却不能被表示成一个可以赋值给指针的整数。如果一块内存在C程序中被使用了,那么它一定可以被这个程序访问。这是一个令不熟悉C的编程者很困惑的概念,因为很多编程语言都会限制程序访问虚拟内存,但是C不会。

第二,所有的变量都存储在内存的某个地方。这意味着如果我们可以知道变量们的通常存储位置,我们可以遍历这些内存位置来寻找每个变量的所有可能值。另外,因为内存的访问通常是字(word-size)对齐的,因此我们仅需要遍历内存区域中的每个字(word)即可。

局部变量也可以被存储在寄存器中,但是我们并不需要担心这些因为寄存器经常会用于存储局部变量,而且当函数被调用的时候他们通常会被存储在堆栈中。

现在我们有一个标记阶段的策略:遍历一系列的内存区域并查看是否有内存可能指向已用块链表。编写这样的一个函数非常的简洁明了:

为了确保我们只使用头(header)中的两个字长(two words)我们使用一种叫做标记指针(tagged pointer)的技术。利用header中的next指针指向的地址总是字对齐(word aligned)这一特点,我们可以得出指针低位的几个有效位总会是0。因此我们将next指针的最低位进行标记来表示当前块是否被标记。

现在,我们可以扫描内存区域了,但是我们应该扫描哪些内存区域呢?我们要扫描的有以下这些:

BBS(未初始化数据段)和初始化数据段。这里包含了程序的全局变量和局部变量。因为他们有可能应用堆(heap)中的一些东西,所以我们需要扫描BSS与初始化数据段。

已用的数据块。当然,如果用户分配一个指针来指向另一个已经被分配的内存块,我们不会想去释放掉那个被指向的内存块。

堆栈。因为堆栈中包含所有的局部变量,因此这可以说是最需要扫描的区域了。

我们已经了解了关于堆(heap)的一切,因此编写一个mark_from_heap函数将会非常简单:

幸运的是对于BSS段和已初始化数据段,大部分的现代unix链接器可以导出 etext 和 end 符号。etext符号的地址是初始化数据段的起点(the last address past the text segment,这个段中包含了程序的机器码),end符号是堆(heap)的起点。因此,BSS和已初始化数据段位于 &etext 与 &end 之间。这个方法足够简单,当不是平台独立的。

堆栈这部分有一点困难。堆栈的栈顶非常容易找到,只需要使用一点内联汇编即可,因为它存储在 sp 这个寄存器中。但是我们将会使用的是 bp 这个寄存器,因为它忽略了一些局部变量。

寻找堆栈的的栈底(堆栈的起点)涉及到一些技巧。出于安全因素的考虑,内核倾向于将堆栈的起点随机化,因此我们很难得到一个地址。老实说,我在寻找栈底方面并不是专家,但是我有一些点子可以帮你找到一个准确的地址。一个可能的方法是,你可以扫描调用栈(call stack)来寻找 env 指针,这个指针会被作为一个参数传递给主程序。另一种方法是从栈顶开始读取每个更大的后续地址并处理inexorible SIGSEGV。但是我们并不打算采用这两种方法中的任何一种,我们将利用linux会将栈底放入一个字符串并存于proc目录下表示该进程的文件中这一事实。这听起来很愚蠢而且非常间接。值得庆幸的是,我并不感觉这样做是滑稽的,因为它和Boehm GC中寻找栈底所用的方法完全相同。

现在我们可以编写一个简单的初始化函数。在函数中,我们打开proc文件并找到栈底。栈底是文件中第28个值,因此我们忽略前27个值。Boehm GC和我们的做法不同的是他仅使用系统调用来读取文件来避免让stdlib库使用堆(heap),但是我们并不在意这些。

现在我们知道了每个我们需要扫描的内存区域的位置,所以我们终于可以编写显示调用的回收函数了:

朋友们,所有的东西都已经在这了,一个用C为C程序编写的垃圾回收器。这些代码自身并不是完整的,它还需要一些微调来使它可以正常工作,但是大部分代码是可以独立工作的。

总结

一开始就打算编写完整的程序是很困难的,你编程的唯一算法就是分而治之。先编写内存分配函数,然后编写查询内存的函数,然后是清除内存的函数。最后将它们合在一起。

当你在编程方面克服这个障碍后,就再也没有困难的实践了。你可能有一个算法不太了解,但是任何人只要有足够的时间就肯定可以通过论文或书理解这个算法。如果有一个项目看起来令人生畏,那么将它分成完全独立的几个部分。你可能不懂如何编写一个解释器,但你绝对可以编写一个分析器,然后看一下你还有什么需要添加的,添上它。相信自己,终会成功!

免责声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击下面图片,有星球具体介绍,新用户有新人优惠券,老用户半价优惠,期待大家一起学习一起进步。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

编程实战:C语言制作垃圾回收器相关推荐

  1. web前端编程实战实例:制作静态京东首页

    <!DOCTYPE html> <html> <head><title>京东_曾柯伟</title><link rel="s ...

  2. 用 C 语言编写一个简单的垃圾回收器

    人们似乎认为编写垃圾回收机制是很难的,是一种只有少数智者和Hans Boehm(et al)才能理解的高深魔法.我认为编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc样例难 ...

  3. 《Python数据可视化编程实战》——5.5 用OpenGL制作动画

    本节书摘来异步社区<Python数据可视化编程实战>一书中的第5章,第5.5节,作者:[爱尔兰]Igor Milovanović,更多章节内容可以访问云栖社区"异步社区" ...

  4. 031、jvm实战总结:动手实验:线上系统部署如果采用G1垃圾回收器,应该如何设置参数?

     1.前文回顾 1.G1中有新 .老.大三种Region 2.新生代回收条件:新生代Eden区满的时候 3.新生代GC仍然采用复制算法 4.控制停顿时间,对Region进行挑选回收 5.进入老年的条件 ...

  5. 029、JVM实战总结:大厂面试题:最新的G1垃圾回收器的工作原理,你能聊聊吗

    1.ParNew + CMS的组合让我们有哪些痛点? 痛点:STW,且停顿时间不可控 G1垃圾回收器比~更好的垃圾回收性能 2.G1垃圾回收器 G1 同时回收新生代和老年代的对象,把java堆拆分为多 ...

  6. 小白C语言编程实战(19):质因数分解

    这是<小白C语言编程实战>系列的第19篇. 上一篇:小白C语言编程实战(18):求5位整数中,回文数的个数 文章目录 题目 要求 提示 参考代码 题目 对区间[90, 100]中的所有整数 ...

  7. JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战

    垃圾回收 一.判断对象是否可以被回收 1.引用计数计数法 内容:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一:当引用失效时,计数器就减一:任何时刻计数器为零的对象都是不可能在被使用的 ...

  8. 使用C语言调用mysql数据库编程实战以及技巧

    今天编写使用C语言调用mysql数据库编程实战以及技巧,为其他IT同行作为参考,当然有错误可以留言,共同学习. 一.mysql数据库的C语言常用接口API 1.首先当然是链接数据库mysql_real ...

  9. C语言代码示范与讲解+C语言编程规范及基础语法+编程实战

    上一篇文章:C语言程序设计概述+C语言简介+算法概述 C语言代码示范与讲解+C语言编程规范及基础语法+编程实战 一:代码示范集加讲解 1.C语言第一个代码:打印"This is the fi ...

最新文章

  1. php swiper 下拉刷新,SwipeRefreshLayout的使用(下拉刷新)
  2. Hotmail 开始支持完全 HTTPS 加密以增强安全性
  3. Kubernetes 已经成为云原生时代的安卓,这就够了吗?
  4. html 字号 宽度 像素,JS根据设备宽度设置根节点(html)font-size字体大小
  5. Python类的封装
  6. [读书笔记]TCP/IP详解V1读书笔记-4 5
  7. 小学生计算机课堂实践的重要性,浅谈小学信息技术教育重要性.doc
  8. MSON,让JSON序列化更快
  9. KinhDown_v2.4.42稳定版 百度云最新不限速下载工具
  10. PocoClassGenerator:RDBMS所有表/视图生成Dapper POCO类代码
  11. 云计算中的地域和可用区概念
  12. php in_array()函数
  13. exists查询慢_8个SQL查询效率优化原则
  14. sql 查找重复值,整行重复
  15. win10分辨率设置_电脑显示器分辨率超频教程:1080P超2K分辨率的方法
  16. 网络战争全面打响!究竟谁能更胜一筹?
  17. 鼠标放上去,变成小手状
  18. APP分享多张图片和文字到微信朋友圈(android 7.0以上适配)
  19. 用墨刀设计原型,易被忽略的8种玩法。
  20. Node.js 更新到最新版本

热门文章

  1. font-spider 压缩字体文件 html vue
  2. 华为手机鸿蒙系统自带吗,内置鸿蒙系统的四款华为手机,实力都很强,可惜都有一点瑕疵!...
  3. win10开机有东西一闪而过_win10系统开机cmd窗口一闪而过的解决方法
  4. linux搭建erp教程,10个最好的自由Linux平台ERP软件 - 51CTO.COM
  5. voided redundant navigation to current location: “/xxxxxx“
  6. agm x2 android8.0,【AGMX2评测】性能:八核骁龙小钢炮_AGM X2_手机评测-中关村在线...
  7. eclipse team没有选项_eclipse的TEAM没有选项
  8. 基于HTML电商购物项目的设计与实现——html+css+javascript+jquery+bootstarp响应式图书商城
  9. 能动就行地理解RoboRTS-0 roborts_planning
  10. 北邮20网安院面试问题汇总