如何用C语言设计一种垃圾内存回收机制

一起学嵌入式 2024-12-15 10:15

扫描关注一起学嵌入式,一起学习,一起成长


工程师们似乎认为编写垃圾回收机制是很难的,是一种只有少数智者和Hans Boehm(et al)才能理解的高深魔法。

我认为编写垃圾回收最难的地方就是内存分配,这和阅读 K&R 所写的 malloc 样例难度是相当的。

在开始之前有一些重要的事情需要说明一下:

第一,我们所写的代码是基于Linux Kernel的,注意是Linux Kernel而不是GNU/Linux。

第二,我们的代码是32bit的。

第三,请不要直接使用这些代码。我并不保证这些代码完全正确,可能其中有一些我还未发现的小的bug,但是整体思路仍然是正确的。

好了,让我们开始吧。

1

编写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 。这点我们会在初始化函数中进行设置。

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

2

标记与清扫

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

首先,我们需要扫描所有可能存在指向堆中数据(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程序编写的垃圾回收器。这些代码自身并不是完整的,它还需要一些微调来使它可以正常工作,但是大部分代码是可以独立工作的。

3

总结

一开始就打算编写完整的程序是很困难的,你编程的唯一算法就是分而治之。

先编写内存分配函数,然后编写查询内存的函数,然后是清除内存的函数。最后将它们合在一起。

当你在编程方面克服这个障碍后,就再也没有困难的实践了。你可能有一个算法不太了解,但是任何人只要有足够的时间就肯定可以通过论文或书理解这个算法。

如果有一个项目看起来令人生畏,那么将它分成完全独立的几个部分。

你可能不懂如何编写一个解释器,但你绝对可以编写一个分析器,然后看一下你还有什么需要添加的,添上它。相信自己,终会成功!

来源:https://www.lmlphp.com/user/1774/article/item/19294/
文章来源于网络,版权归原作者所有,如有侵权,请联系删除。

关注【一起学嵌入式】,回复加群进技术交流群



觉得文章不错,点击“分享”、“”、“在看” 呗

一起学嵌入式 公众号【一起学嵌入式】,RTOS、Linux编程、C/C++,以及经验分享、行业资讯、物联网等技术知
评论
  •        霍尔传感器是一种基于霍尔效应的传感器。霍尔效应指的是当通过一个导体的电流受到外部磁场的影响时,导体内部将会产生一种电场,使得在导体两端的电势差发生变化,这种电势差变化称为霍尔电势差。利用这种现象,可以设计出一种可以测量磁场强度和方向的传感器,即霍尔传感器。  霍尔传感器分为线型霍尔传感器和开关型霍尔传感器两种。  (一)开关型霍尔传感器由稳压器、霍尔元件、差分放大器,斯密特触发器和输出级组成,它输出数字量。开关型霍尔传感器还有一种特
    锦正茂科技 2024-12-14 10:58 55浏览
  • 光耦合器是现代电子系统中的关键组件,可在实现电路间信号传输的同时提供电气隔离。然而,人们经常对其功能、选择和应用感到困惑。本文旨在澄清常见的误解,并为工程师和业余爱好者提供必要的见解。什么是光耦合器?光耦合器或光隔离器由封装在一个封装中的发光二极管(LED)和光电探测器(如光电晶体管或光电二极管)组成。当电流通过LED时,LED会发光。光电探测器检测到该光,并产生相应的输出信号。这种机制允许在电气隔离输入和输出的同时传输信号,保护敏感元件免受高压和噪声的影响。关于光耦合器的常见困惑1.了解功能许
    腾恩科技-彭工 2024-12-13 16:17 44浏览
  • 霍尔传感器的原理        霍尔传感器是一种固体的传感器,其输出电压与磁场强度成比例。顾名思 义,这种器件是依赖于霍尔效应原理工作的。霍尔效应原理是在导体通电 和加有磁场的情况下,在导体的横向 上会产生电压。电子(在实践中多数载流子最常被使 用)在外部电场的驱动下会产生“漂移”,当暴露于磁场中时,这些运动 的带电粒子会受到一个垂直于电场和 磁场的力的作用。这个力会让导体的边缘充电,一边为正,一边为负。边
    锦正茂科技 2024-12-14 11:41 44浏览
  • 串口调试助手软件:XCOM 也是一款专为嵌入式开发和硬件调试设计的强大工具,如正点原子串口调试助手 XCOM V2.6。这款软件支持多种串口参数配置,满足不同开发需求,广泛应用于嵌入式系统开发、硬件调试以及电子爱好者的项目开发中。XCOM在嵌入式开发和硬件调试中的作用主要体现在以下几个方面: 1. 串口通信测试:XCOM作为一款强大的串口调试工具,允许用户通过计算机的串口进行数据的发送与接收,从而实现对串口通信的测试。这对于验证硬件设备的通信协议、确保数据传输的正确性至关重要。 2. 数据发
    丙丁先生 2024-12-15 11:56 54浏览
  • 家用国产固态继电器(SSR)已成为各行各业的基石,性能可靠、设计紧凑、效率高。这些先进的开关设备取代了传统的机电继电器,具有静音运行、使用寿命更长、可靠性更高等诸多优点。家用SSR专为从工业自动化到家用电器等各种应用而设计,展示了本地制造商的独创性和竞争力。国产固态继电器特点和优势家用SSR采用半导体技术制造,与传统继电器相比,具有很强的耐磨性。主要特点包括:静音无振动运行:SSR使用半导体元件进行开关,消除了机械噪音。响应时间快:是工业控制系统中高速开关的理想选择。耐用性:没有移动部件,即使在
    克里雅半导体科技 2024-12-13 16:49 41浏览
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-14 20:56 56浏览
  • 概述 Cyclone 10 GX器件的ALM结构与Cyclone V类似,所以在Cyclone 10 GX器件上实现TDC功能理论上是可以完全参考甚至移植自Cyclone V系列的成功案例。但是,现实却是更多的问题出现当在Cyclone 10 GX使用和Cyclone V同样策略实现TDC的时候。 本文主要记录在Cyclone 10 GX器件上实现TDC时的探索,并为后续TDC设计、测试等展开前期研究。Cyclone 10 GX ALM结构 如图1所示,Cyclone 10 GX器件的ALM结构
    coyoo 2024-12-14 17:15 51浏览
  • 一、引言在数字化时代,芯片作为现代科技的核心,其制造过程却常被视作神秘的黑箱。菊地正典的《大话芯片制造》为我们揭开了这层神秘的面纱,以通俗易懂的方式,全面系统地介绍了芯片制造的各个环节。作为一名电子信息技术专业的教育工作者,我深感这本书不仅为学生提供了宝贵的知识资源,也让我对芯片制造及其在现代社会中的作用有了更深刻的理解。二、生活中的芯片印记芯片的影响渗透到我们日常生活的每一个角落。从智能手机的闹钟唤醒,到交通卡的便捷支付,再到智能家居的智能化功能,芯片以其强大的运算和处理能力,为我们的现代生活
    月光 2024-12-16 11:52 31浏览
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-13 23:20 51浏览
  • 在现代软件开发领域,效率和可靠性是企业在竞争中取胜的关键。本文将深入探讨 ANA Systems 如何通过引入业界领先的 CI/CD 平台——CircleCI,克服传统开发流程的瓶颈,实现开发运营效率的全面提升。同时,本文还将详细解析 CircleCI 的核心优势,包括其强大的自动化功能、广泛的工具整合能力,以及为企业量身定制的支持服务,揭示其如何助力 ANA Systems 在「新一代国内旅客项目」中脱颖而出。这一案例将为企业优化开发流程、提升竞争力提供重要的实践参考。ANA Systems
    艾体宝IT 2024-12-16 16:44 3浏览
  • 光耦合器是一种重要的电子元件,其在电子信号隔离和传输中的作用不可替代。自20世纪60年代首次被研发以来,光耦合器经历了从基础隔离器件到高性能元件的不断演化,在现代电子设备中占据了重要地位。本文将深入探讨光耦合器的发展历程、技术特点以及在当今科技领域中的广泛应用。光耦合器的诞生背景光耦合器的诞生源于20世纪60年代,为了解决电子信号在不同电路之间传输时的隔离问题,科学家们设计了一种基于光信号传递的全新器件。光耦合器通过发光二极管(LED)将电信号转化为光信号,再由光敏器件接收并重新转换为电信号,从
    腾恩科技-彭工 2024-12-13 16:18 40浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦