运行存在缺陷内存的 Linux
内存模块故障有几种常见原因。在通过软件解决问题之前,让我们先来看看这些原因。为了理解它们,请看图 1,它示意性地显示了一个(无故障)内存的结构。注意内存是如何布局成(大致相等)的行和列。每个交叉点标记一个存储单元的位置,通常存储单个比特。因此,存储单元的地址是其行地址和列地址的组合。内存模块分别(按顺序)接收地址的这两部分,因此它们的地址总线只有您期望大小的一半。地址的这种减半由主板上的硬件处理。

图 1. 没有问题的内存结构
第一个可能出错的地方是在这些内存模块的制造过程中。这是一个非常敏感的过程,主要是因为芯片布局的尺寸随着世界对更多内存的渴望而变得越来越小。芯片制造也非常浪费环境资源,因为需要大量高度纯化的化学品来制造您最终购买的微小的沙子衍生物产品。此外,芯片制造过程的敏感性导致许多芯片因错误而报废。
部分解决这个问题的一种方法是在芯片中构建冗余,例如,每 32 行内存单元包含 33 行;当一行出现故障时,将信号绕过它路由到额外的行。事实上,一些制造商正在应用这种技术。尽管如此,未经证实的传言声称仍然有 60% 的报废率。不用说,这个因素使内存变得昂贵。
生产过程中引起的错误可能呈现出灰尘颗粒的形状,这些灰尘颗粒在芯片上照亮或显影其中一层时落在芯片上,如图 2 所示。灰色区域是可能因灰尘颗粒而发生故障的部分。

图 2. 内存蚀刻受干扰
另一个错误来源是静电放电。这与脱下羊毛或尼龙毛衣时产生电火花的效果相同。产生火花的原因是两个相邻表面之间存在高电压。当它们足够靠近时,火花突然跳跃,使其通过的空气电离,从而突然使其导电良好,从而在很短的时间内产生高电流。实际上,这与闪电的效果相同,只是当然没有那么强大。芯片中的微小结构对这些强烈的放电非常敏感。静电放电通常会损坏“缓冲器”,即行/列与地址选择逻辑之间的连接,如图 3 所示。这实际上意味着整行、整列甚至其中的几列都变得不可用,再次以灰色区域表示。这些是我见过最多的内存故障模式。

图 3. 静电放电损坏的内存缓冲器
最后一个错误来源,到目前为止,我还没有确凿的证据表明我曾经遇到过它,是所有芯片都会经历的逐渐衰减。这是芯片上不同材料的清晰区域变得模糊和混合在一起的过程。这是一种自然效应,如果芯片保持足够凉爽,通常需要几十年才会发生。对于内存而言,没有问题,通常在达到这一点之前,它的速度和容量就过时了。
所有这些错误的重要之处在于它们是固定的——我不知道有任何技术原因可能导致随机错误。请注意,错误可能只在周围比特的特定模式中发生,但即使那样,一旦错误确定,它总是会在那种情况下发生。
此外,错误不会无缘无故地发生。在上述所有情况下,突发的能量爆发会导致错误,或者它们是由非常缓慢的过程引起的。因此,我们不需要期望错误总是会突然出现,即使是在有缺陷的内存模块上也是如此。
由于内存中的错误不会演变,因此通常可以通过巧妙地使用动态内存分配技术来规避错误。
基本上有两种检测内存错误的方法。一种方法是使用特殊的程序扫描机器中的所有内存并报告故障地址。一个擅长查找错误的程序是 memtest86,它针对基于 i386 的机器。当然,信任这种内存检查器的结果依赖于内存错误的稳定性,这通常被证明是一个可以接受的假设;我的机器已经运行了几个月,检测到的错误模式没有变化。
另一种方法是基于硬件检测。在 30 针 SIMM 时代,在内存模块上包含第九位“奇偶校验”信息非常常见。其思想是,第九位(实际上是单独存储在内存模块上的)包含使整个芯片的奇偶校验为偶数(或奇数,取决于主板的设计)的比特。写入时,会生成此比特,并在读取回时进行检查。如果此读取时检查失败,则会抛出奇偶校验错误。
奇偶校验位的现代替代方案是 ECC,即错误校正(和检测)码的缩写。这些通常基于存储比特的某些 CRC 哈希,它们可以用于检测 32 位中的最多三个错误比特,以及校正最多两个错误比特。(我实际上不确定这些精确值,但重要的是原理。)因此,使用 ECC RAM,可以检测错误,并在错误不太严重的情况下对其进行校正。
我们在 PC 工作站中使用的现代内存模块通常既没有奇偶校验也没有 ECC。另一方面,在 VA Linux 和 Sun 出售的高端(服务器)系统中,ECC 是标准配置。这些系统通常使用 ECC 内存,或者至少使用奇偶校验内存。我相信大多数普通 PC 都能够使用 ECC 内存,只是它们没有插入 ECC RAM,因为 ECC RAM 更昂贵。是的,这就是 PC 低质量世界的眨眼。
使用奇偶校验 RAM 或 ECC RAM,可以即时检测错误,而无需像 memtest86 这样的特殊程序。然而,目前,我们在此处介绍的 BadRAM 补丁尽可能保持简单(以增加被主流内核接受的机会),因此,不处理 ECC RAM 的附加可能性。
我们假设像 memtest86 这样的内存检查器在内存模块中找到错误。如前所述,此检查器列出它找到的所有错误地址。并且,由于在静电放电后可能会导致整行或整列故障,这可能会导致非常长的错误列表,数百个错误并不罕见。当然,这样的列表输入到内核中会很乏味且容易出错,并且还可能引起启动时严格限制的资源导致的问题。理想的情况是使用非常小的表示形式来表示找到的所有错误。
幸运的是,内存错误通常以规则模式布局,例如从地址 0<\#215>1234 开始,每 0<\#215>0040 字节,重复 16 次——或者稍微多一点变化,当然。
当进行二进制时,规律性通常最容易看到。这是因为行和列是通过在这些线上 spread 地址位来寻址的,并且通过解释地址的一部分来决定使用的地址线是否落在正确的区域中。
单个错误可以描述为一个地址,其中所有位都是有价值的信息。例如,我们将其写为 0<\#215>1234,0<\#215>ffff,其中第一个数字是基地址,第二个数字是掩码。掩码中的“1”位指示哪些对应的基地址位是有价值的。如果除了此地址之外,地址加 0<\#215>0040 也错误,那么我们可以简单地更改掩码来表示这一点。生成的地址/掩码对现在变为 0<\#215>1234,0<\#215>ffbf。我们可以看到这是正确的,因为覆盖的地址是所有地址 A,其中 (A & 0<\#215>ffbf)=0<\#215>1234,或者具体来说,是 0<\#215>1234 和 0<\#215>1274。如果有 16 个具有此间歇性偏移量的错误地址,那么整个事情将变为 0<\#215>1234,0<\#215>fc3f 以捕获错误地址:0<\#215>1234、0<\#215>1274、...、0<\#215>15f4。
这种方法非常适合捕获行或列上的故障,只要这些故障覆盖了比特的二次幂,这是正常的。它也适用于捕获灰尘颗粒,这些灰尘颗粒会禁用芯片上的一些相邻比特。但是,如果一个内存模块包含更多错误怎么办?或者,类似地,如果多个内存模块各自都有错误怎么办?为了涵盖这些情况,我们通常假设一个上述地址/掩码对的列表。在实践中,事实证明,五个这样的对足以应对大多数实际情况。请注意,任何错误集都可以始终压缩到尽可能少的一个这样的对中,尽管会损失好的地址,0<\#215>0000,0<\#215>0000 是捕获所有错误的最终示例,但不幸的是,没有好的地址。使用五个对,我们通常不会遇到如此极端的问题。
从 2.3 版本开始,memtest86 能够生成 BadRAM 模式,这些模式可以直接在 BadRAM 补丁 Linux 内核的命令行中输入。在上面的示例中,memtest86 将报告一系列不断演变的模式,最终导致
badram=0<\#215>1234,0<\#215>fb3f
写下这些内容后,您现在可以重新启动系统并在命令行中输入以下内容
LILO: linux badram=0<\#215>1234,0<\#215>fb3f您的系统应该可以正常启动,忽略损坏的内存部分。现在,继续并将此行添加到您的 /etc/lilo.conf
append="badram=0<\#215>1234,0<\#215>fb3f"并运行 LILO。您将不必在连续启动时再次输入地址/掩码对。
我的旧 ZX Spectrum 有一种巧妙的方法来扩展内存,增加 32K。Sinclair 内存扩展套件由 64K 芯片组成,其中已知一半的高位或低位可以工作,而另一半有故障,这使得价格低于使用 32k 芯片的“正确”扩展方法。通过选择主板上的某个开关,计算机被指示应使用扩展内存芯片的哪一半。基本上,BadRAM 补丁做的是相同的事情,只是稍微精细一些。
内存管理单元是所有 Linux 发行版良好运行的必要条件,它将“用户空间”程序访问的页面重定向到任何物理页面。看起来像是为您的用户进程分配的一长串内存,实际上是分散在内存模块上的(甚至可能被换出)。当用户级程序分配内存时,内核通过分配单个物理内存页面来提供内存。
Linux 中唯一以物理内存地址工作的部分是内核。它在内存的开头加载(显然,您在该区域中不能有错误),并且可以从用于服务用户进程的同一堆空闲内存中分配内存块。
这堆内存是在启动时用物理页面填充的。实际上,BadRAM 补丁所做的全部工作是排除那些落在命令行中输入的地址/掩码对之一中的页面。
这意味着 BadRAM 涉及的所有工作都在启动时完成。之后,唯一的效果是空闲内存堆略小。广泛的基准测试表明,这对运行时性能没有可衡量的影响。
当你制作这样的补丁时,特别是当你因它而“SlashDotted”时,你会收到很多有趣的电子邮件,其中一些想法甚至比我想象的还要奇怪。
我经常听到的一个建议是在启动时执行内存测试。虽然这看起来可能很有趣,但实际上并不可行。memtest86 做得非常出色,但需要几个小时。您不希望在启动时这样做,并且您也不希望进行糟糕的内存测试(无论如何,我们已经在大多数 PC BIOS 中都有了,BadRAM 模块通常会通过该测试)。为 memtest86 制作 LILO 启动替代方案,或者拥有 memtest86 启动软盘,一直是我的首选。
我收到一些关于主板错误的报告,这些错误损坏了物理内存的特定地址,可能是由于与板载外围设备的短路造成的。一位这样的报告者告诉我,他已在他的机器中安装了四个 512M 模块,以限制遇到错误地址的机会,但通过使用 BadRAM 补丁丢弃单个内存页,他的问题完全解决了。从 2G 中丢弃 4K 使他的机器完美运行。
我还与一位拥有来自大型家用 PC 项目的 PC 的人交谈过。他的 Compaq Deskpro 的相当专有的架构没有预见到关闭某些 ISA 卡所需的 15M-16M 内存空洞的选项。因此,在 Linux 2.2 上将内存扩展到 24M 是行不通的,因为设置 mem=24M 意味着 15M-16M 区域作为内存空洞工作,正如您所猜测的那样。但是,在添加 badram=0<\#215>00f00000,0<\#215>fff00000 以告知内核该空洞应被视为 BadRAM 后,他获得了额外的内存,并准备添加更多内存。
最后,我收到了来自人们的浪漫回应,他们曾在旧系统中工作过,这些系统检测到内存故障(使用奇偶校验方案或 ECC 方案),并对这些故障页面分配了较低的信任级别。当一个页面“正在评估”时,它最多用于加载程序代码,其想法是程序代码是硬盘的 verbatim 副本,如果错误持续存在,则可以恢复。如果它运行良好一段时间,则错误显然是虚假的,并且该页面将再次升级为“可信”。但是,如果程序存储也无法正常工作,则该页面将被停用。这种方案非常有趣,值得支持,但它会在运行时占用 CPU 周期,因此应被视为非常花哨的功能。
有几种方法可以扩展 BadRAM 的效率。当然可以利用 ECC 模块,包括具有可恢复和不可恢复错误的模块。由于我缺少这两种类型的模块,不幸的是,这超出了我目前的能力范围。此外,它会消耗 CPU 周期,并属于花哨功能的范畴。
另一种选择是利用内核中的 slab 分配器。Slab 是小的、统一的和可重用的内存块,内核在数组中分配这些块,这些数组尽可能接近整数个内存页面。通过将 BadRAM 页面用于 slab,可以更详细地利用错误地址信息,从而避免分配与错误地址重叠的 slab。理想情况下,这可以将内存损失减少到绝对零。然而,在实践中,它不会偏离标准的 BadRAM 性能太远,因为平均系统根本不使用很多 slab 页面。因此,我怀疑额外的 CPU 开销和编码工作是否值得。
我真诚地希望内存营销公司能够采纳这个想法,并开始发布基于损坏内存的(廉价)内存模块。我建议制定一个方案,根据“坏度”的对数等级对这些模块进行分类,作为 BadRAM 补丁的内核文档的一部分。
现在剩下的就是祝您在追逐损坏内存方面好运。也许一些友好的不太成熟的操作系统用户可以 spare it。
