编译用于高放射性环境的应用程序

Compiling an application for use in highly radioactive environments

我们正在编译一个嵌入式 C++ 应用程序,该应用程序部署在受 ionizing radiation 轰炸的环境中的屏蔽设备中。我们正在使用 GCC 和 ARM 的交叉编译。部署后,我们的应用程序会生成一些错误数据,并且比我们希望的更频繁地崩溃。硬件就是为这个环境设计的,我们的应用在这个平台上已经运行好几年了。

我们是否可以对我们的代码进行更改,或者可以对 identify/correct soft errors and memory-corruption caused by single event upsets 进行编译时改进?是否有任何其他开发人员成功地减少了软错误对长期 运行ning 应用程序的有害影响?

NASA 有 a paper on radiation-hardened 软件。它描述了三个主要任务:

  1. 定期监控内存错误,然后清除这些错误,
  2. 强大的错误恢复机制,并且
  3. 如果某些东西不再有效,可以重新配置。

请注意,内存扫描频率应足够频繁,这样多位错误就很少发生,因为大多数 ECC 内存可以从单位错误中恢复,而不是多位错误。

强大的错误恢复包括控制流传输(通常在错误发生前的某个点重新启动进程)、资源释放和数据恢复。

他们对数据恢复的主要建议是通过将中间数据视为临时数据来避免对数据恢复的需要,这样在错误发生之前重新启动也会将数据回滚到可靠状态。这听起来类似于数据库中 "transactions" 的概念。

他们讨论了特别适合面向对象语言(如 C++)的技术。例如

  1. 用于连续内存对象的基于软件的 ECC
  2. Programming by Contract:验证前置条件和后置条件,然后检查对象以验证它是否仍处于有效状态。

而且,碰巧的是,NASA 已将 C++ 用于重大项目,例如 Mars Rover

C++ class abstraction and encapsulation enabled rapid development and testing among multiple projects and developers.

他们避免了某些可能产生问题的 C++ 特性:

  1. 例外情况
  2. 模板
  3. Iostream(无控制台)
  4. 多重继承
  5. 运算符重载(newdelete 除外)
  6. 动态分配(使用专用内存池和放置 new 以避免系统堆损坏的可能性)。

您可能还对有关算法容错主题的丰富文献感兴趣。这包括旧的任务:编写一个排序,当恒定数量的比较失败时正确排序其输入(或者,稍微更邪恶的版本,当失败比较的渐近数缩放为 log(n)n 比较)。

开始阅读的地方是 Huang 和 Abraham 1984 年的论文“Algorithm-Based Fault Tolerance for Matrix Operations”。他们的想法有点类似于同态加密计算(但实际上并不相同,因为他们在操作层面尝试错误detection/correction)。

该论文最近的后代是 Bosilca、Delmas、Dongarra 和 Langou 的“Algorithm-based fault tolerance applied to high performance computing”。

这里有一些想法和想法:

更有创意地使用 ROM。

在 ROM 中存储任何内容。不是计算东西,而是将查找 table 存储在 ROM 中。 (确保您的编译器正在将您的查找 table 输出到只读部分!在 运行 时间打印出内存地址以进行检查!)将您的中断向量 table 存储在 ROM 中.当然,运行 进行一些测试以了解您的 ROM 与 RAM 相比有多可靠。

为堆栈使用最好的 RAM。

堆栈中的 SEU 可能是最有可能的崩溃源,因为它是索引变量、状态变量、return 地址和各种指针等通常存在的地方。

实施计时器滴答和看门狗计时器例程。

您可以 运行 每个计时器滴答一个 "sanity check" 例程,以及一个看门狗例程来处理系统锁定。您的主代码还可以定期递增计数器以指示进度,完整性检查例程可以确保发生这种情况。

在软件中实现error-correcting-codes

您可以为数据添加冗余,以便能够检测 and/or 更正错误。这将增加处理时间,可能使处理器暴露在辐射下的时间更长,从而增加出错的机会,因此您必须考虑权衡。

记住缓存。

检查 CPU 缓存的大小。您最近访问或修改的数据可能在缓存中。我相信你至少可以禁用一些缓存(以很大的性能成本);您应该试试这个,看看缓存对 SEU 有多敏感。如果缓存比 RAM 更耐用,那么您可以定期读取和重写关键数据以确保它保留在缓存中并使 RAM 恢复正常。

巧妙地使用页面错误处理程序。

如果您将内存页面标记为不存在,CPU 将在您尝试访问它时发出页面错误。您可以创建一个页面错误处理程序,在为读取请求提供服务之前进行一些检查。 (PC 操作系统使用它来透明地加载已交换到磁盘的页面。)

对关键的事情(可能是一切)使用汇编语言。

使用汇编语言,您知道寄存器中的内容和 RAM 中的内容;您知道 CPU 使用的是什么特殊 RAM table,并且您可以采用迂回的方式设计事物以降低风险。

使用 objdump 实际查看生成的汇编语言,并计算出每个例程占用了多少代码。

如果你使用像Linux这样的大OS那么你是在自找麻烦;有太多的复杂性和太多的错误。

记住这是概率游戏。

评论者说

Every routine you write to catch errors will be subject to failing itself from the same cause.

虽然这是事实,但检查例程正常运行所需的(比方说)100 字节代码和数据出错的几率比其他地方出错的几率小得多。如果您的 ROM 非常可靠并且几乎所有 code/data 实际上都在 ROM 中,那么您的胜算会更大。

使用冗余硬件。

使用 2 个或更多具有相同代码的相同硬件设置。如果结果不同,则应触发重置。对于 3 台或更多设备,您可以使用 "voting" 系统来尝试确定哪台设备已被入侵。

在software/firmware开发和环境测试miniaturized satellites*工作了大约4-5年,我想在这里分享我的经验。

*(小型卫星比大卫星更容易发生单事件干扰,因为它的电子元件相对较小且尺寸有限)

To be very concise and direct: there is no mechanism to recover from detectable, erroneous situation by the software/firmware itself without, at least, one copy of minimum working version of the software/firmware somewhere for recovery purpose - and with the hardware supporting the recovery (functional).

现在,这种情况在硬件和软件层面都得到了正常处理。在这里,应您的要求,我将分享我们在软件层面可以做的事情。

  1. ...恢复目的...。提供在真实环境中 update/recompile/reflash 您的 software/firmware 的能力。这是高度电离环境中任何 software/firmware 的 几乎必备的 功能。没有这个,你 可以 有多少多余的 software/hardware ,但在某一时刻,它们都会爆炸。所以,准备好这个功能吧!

  2. ...最低工作版本... 在您的代码中具有响应式、多副本、最低版本的 software/firmware。这就像 Windows 中的安全模式。与其只有一个功能齐全的软件版本,不如拥有多个 software/firmware 最低版本的副本。最小副本通常比完整副本的大小小得多,并且几乎总是以下两个或三个特征:

    1. 能够监听来自外部系统的命令,
    2. 能够更新当前 software/firmware、
    3. 能够监控基本操作的内务数据。
  3. ...复制...某处...某处有冗余software/firmware。

    1. 您可以在 没有冗余硬件的情况下,尝试在您的 ARM uC 中使用冗余 software/firmware。这通常是通过在不同的地址 中设置两个或多个相同的 software/firmware 来实现的,这些地址相互发送心跳 - 但一次只有一个处于活动状态。如果已知一个或多个 software/firmware 没有响应,请切换到另一个 software/firmware。使用这种方法的好处是我们可以在错误发生后立即进行功能替换——无需与任何负责检测和修复错误的外部 system/party 联系(在卫星情况下,通常是任务控制中心中心(MCC))。

      严格来说,在没有冗余硬件的情况下,这样做的缺点是你实际上不能消除所有个单点故障。至少,您仍然会有一个 单点故障,即交换机本身(或通常是代码的开头)。尽管如此,对于在高度电离环境(例如 pico/femto 卫星)中受尺寸限制的设备,在没有 额外硬件的情况下将单点故障减少到一个点 仍然会值得考虑。此外,用于切换的代码片段肯定会比整个程序的代码少得多 - 显着降低了其中出现 Single Event 的风险。

    2. 但如果你不这样做,你应该在你的外部系统中至少有一个副本,它可以与设备联系并更新 software/firmware(在卫星情况下,它又是任务控制中心)。

    3. 您还可以在设备的永久内存存储中拥有副本,可以触发该副本以恢复 运行 系统的 software/firmware
  4. ...可检测错误情况..错误必须可检测,通常由硬件error correction/detection circuit 或通过一小段错误代码correction/detection。最好把这样的代码小,多,并且独立从主software/firmware。它的主要任务是 for checking/correcting。如果硬件 circuit/firmware 是 可靠的 (例如它比其他硬件更抗辐射 - 或者有多个 circuits/logics),那么您可以考虑进行错误纠正用它。但如果不是,最好将其作为错误检测。校正可以通过外部system/device。对于纠错,你可以考虑使用像Hamming/Golay23这样的基本纠错算法,因为它们在circuit/software中都可以更容易地实现。但这最终取决于您团队的能力。对于错误检测,通常使用 CRC。

  5. ...支持恢复的硬件 现在,来到这个问题上最困难的方面。最终,恢复需要负责恢复的硬件 至少 正常运行。如果硬件永久损坏(通常在其 总电离剂量 达到一定水平后发生),那么(遗憾的是)软件无法帮助恢复。因此,对于暴露于高辐射水平的设备(例如卫星),硬件无疑是最重要的考虑因素。

除了以上针对单次事件扰乱预期固件错误的建议外,我还建议您:

  1. 错误检测and/or子系统间通信协议中的纠错算法。为了避免从其他系统

  2. 接收到incomplete/wrong信号,这几乎是另一个必须具备的
  3. 过滤您的 ADC 读数。 不要直接使用ADC读数。通过中值过滤器、均值过滤器或任何其他过滤器对其进行过滤 - 从不 信任单个读数值。采样更多,而不是更少 - 合理。

也许可以使用 C 编写在此类环境中表现稳健的程序,但前提是禁用大多数形式的编译器优化。优化编译器旨在用 "more efficient" 替换许多看似冗余的编码模式,并且可能不知道程序员正在测试 x==42 的原因,而编译器知道 x 不可能可能持有其他任何东西是因为程序员想要阻止某些代码的执行 x 持有其他一些值——即使在它可以持有该值的唯一方法是系统接收到某种电气的情况下故障。

将变量声明为 volatile 通常是有帮助的,但可能不是万灵药。 特别重要的是,请注意安全编码通常需要危险的 操作具有硬件联锁,需要多个步骤才能激活, 并且该代码使用以下模式编写:

... code that checks system state
if (system_state_favors_activation)
{
  prepare_for_activation();
  ... code that checks system state again
  if (system_state_is_valid)
  {
    if (system_state_favors_activation)
      trigger_activation();
  }
  else
    perform_safety_shutdown_and_restart();
}
cancel_preparations();

如果编译器以相对直白的方式翻译代码,并且如果所有 在 prepare_for_activation() 之后重复检查系统状态, 该系统可能对几乎任何可能的单一故障事件都很稳健, 即使是那些会任意破坏程序计数器和堆栈的程序。如果 在调用 prepare_for_activation() 之后出现故障,这意味着 激活是合适的(因为没有其他原因 prepare_for_activation() 会在故障之前被调用)。如果 故障导致代码不恰当地达到 prepare_for_activation(),但是 没有后续的故障事件,代码将无法随后 在没有通过验证检查或先调用 cancel_preparations 的情况下到达 trigger_activation() [如果堆栈出现故障,执行可能会在调用 prepare_for_activation() 的上下文之后继续到 trigger_activation() 之前的位置returns,但对 cancel_preparations() 的调用将发生在对 prepare_for_activation()trigger_activation() 的调用之间,从而使后者的调用无害。

这样的代码在传统 C 中可能是安全的,但在现代 C 编译器中则不然。这样的编译器在那种环境中可能非常危险,因为他们积极地努力只包含在可能通过某种明确定义的机制产生的情况下相关的代码,并且其结果的后果也将得到明确定义。在某些情况下,其目的是检测和清除故障后的代码可能最终会使事情变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,它可能会推断在这种情况下需要进行此类恢复的条件不可能发生,从而消除本应检查它们的代码。

您的应用程序的 运行 个实例怎么样。如果崩溃是由于随机内存位更改引起的,那么您的某些应用程序实例很可能会通过并产生准确的结果。可能很容易(对于具有统计背景的人)计算给定位翻转概率需要多少个实例才能实现您希望的最小整体误差。

这是一个非常广泛的主题。基本上,您无法真正从内存损坏中恢复,但您至少可以尝试迅速失败。以下是您可以使用的一些技巧:

  • 校验和常量数据。如果您有任何长期保持不变的配置数据(包括您配置的硬件寄存器),请在初始化时计算其校验和并定期对其进行验证。当您看到不匹配时,是时候重新初始化或重置了。

  • 以冗余方式存储变量。如果你有一个重要的变量x,把它的值写在x1x2x3中,读作(x1 == x2) ? x2 : x3

  • 实施程序流量监控。 XOR 一个全局标志,在从主循环调用的 important functions/branches 中具有唯一值。 运行 在接近 100% 的测试覆盖率的无辐射环境中的程序应该会在周期结束时为您提供标志的可接受值列表。如果发现偏差,请重置。

  • 监控堆栈指针。在主循环的开始,将堆栈指针与其预期值进行比较。偏差重置。

你问的是相当复杂的话题 - 不容易回答。其他答案还可以,但它们只涵盖了您需要做的所有事情的一小部分。

,不可能 100% 修复硬件问题,但是很有可能使用各种技术来减少或发现它们。

如果我是你,我会创建最高 Safety integrity level 级别 (SIL-4) 的软件。获取 IEC 61513 文档(用于核工业)并遵循它。

watchdog 对您有帮助。 80 年代,看门狗被广泛用于工业计算。那时硬件故障更为常见 - 另一个答案也指那个时期。

看门狗是一项组合的 hardware/software 功能。硬件是一个简单的计数器,从数字(比如 1023)递减到零。 TTL 或可以使用其他逻辑。

该软件的设计使得一个例程可以监控所有基本系统的正确运行。如果此例程正确完成 = 发现计算机 运行 正常,它将计数器设置回 1023。

总体设计是为了在正常情况下,软件防止硬件计数器归零。如果计数器达到零,计数器的硬件将执行其唯一任务并重置整个系统。从计数器的角度来看,零等于 1024,计数器又继续递减计数。

这个看门狗确保连接的计算机在很多很多情况下重新启动。我必须承认,我不熟悉能够在当今计算机上执行此类功能的硬件。与外部硬件的接口现在比以前复杂得多。

看门狗的一个固有缺点是系统在出现故障时不可用,直到看门狗计数器达到零 + 重新启动时间。虽然该时间通常比任何外部或人为干预短得多,但受支持的设备需要能够在该时间范围内在没有计算机控制的情况下继续运行。

为放射性环境编写代码与为任何关键任务应用程序编写代码并没有什么不同。

除了已经提到的,还有一些杂七杂八的小技巧:

  • 使用任何半专业嵌入式系统都应具备的日常“面包和黄油”安全措施:内部看门狗、内部低电压检测、内部时钟监视器。这些东西在 2016 年甚至不需要提及,它们几乎是每个现代微控制器的标准配置。

  • 如果你有一个安全and/or面向汽车的MCU,它会有一定的看门狗功能,比如给定时间window,里面需要刷新看门狗。如果您有关键任务实时系统,这是首选。

  • 一般来说,使用适合这类系统的 MCU,而不是你在一包玉米片中收到的一些通用的主流绒毛。现在几乎每个 MCU 制造商都有专门为安全应用设计的 MCU(TI、Freescale、Renesas、ST、Infineon 等)。它们具有许多内置的安全功能,包括锁步内核:这意味着有 2 个 CPU 内核执行相同的代码,并且它们必须彼此一致。

  • 重要提示:您必须确保内部 MCU 寄存器的完整性。所有可写的硬件外围设备的控制和状态寄存器都可能位于 RAM 内存中,因此容易受到攻击。

    为了保护自己免受寄存器损坏,最好选择具有内置寄存器“一次写入”功能的微控制器。此外,您需要将所有硬件寄存器的默认值存储在 NVM 中,并定期将这些值复制到您的寄存器中。同样的方法可以确保重要变量的完整性。

    注意:始终使用防御性编程。这意味着您必须在 MCU 中设置 all 寄存器,而不仅仅是应用程序使用的寄存器。您不希望某些随机硬件外围设备突然唤醒。

  • 有多种方法可以检查 RAM 或 NVM 中的错误:校验和、“行走模式”、软件 ECC 等。如今最好的解决方案是不使用任何这些方法,但是使用具有内置 ECC 和类似检查的 MCU。因为在软件中这样做很复杂,而且错误检查本身可能会引入错误和意外问题。

  • 使用冗余。您可以将易失性和非易失性内存存储在两个相同的“镜像”段中,这两个段必须始终相同。每个段都可以附加一个 CRC 校验和。

  • 避免在 MCU 外部使用外部存储器。

  • 为所有可能的 interrupts/exceptions 实现默认中断服务例程/默认异常处理程序。即使是您不使用的那些。默认例程除了关闭它自己的中断源外什么都不做。

  • 理解并接受防御性编程的概念。这意味着您的程序需要处理所有可能的情况,即使是理论上不可能发生的情况。 .

    高质量的关键任务固件会检测尽可能多的错误,然后以安全的方式处理或忽略它们。

  • 永远不要编写依赖于不明确行为的程序。由于辐射或 EMI 导致的意外硬件更改,此类行为可能会发生巨大变化。确保您的程序没有此类垃圾的最佳方法是使用像 MISRA 这样的编码标准以及静态分析器工具。这也有助于防御性编程和清除错误(为什么您不想检测任何类型的应用程序中的错误?)。

  • 重要提示:不要依赖静态存储持续时间变量的默认值。也就是说,不要相信 .data.bss 的默认内容。从初始化点到实际使用变量的点之间可能有任何时间,RAM 可能有足够的时间被破坏。相反,编写程序以便在 运行 时间内从 NVM 设置所有此类变量,就在首次使用此类变量的时间之前。

    实际上这意味着如果一个变量在文件范围内声明或声明为 static,你不应该使用 = 来初始化它(或者你可以,但这是没有意义的,因为你无论如何不能依赖价值)。始终在 运行 时间设置它,就在使用之前。如果可以从 NVM 重复更新此类变量,那么就这样做。

    与 C++ 类似,不要依赖构造函数来获取静态存储持续时间变量。让构造函数调用 public“设置”例程,您也可以稍后在 运行 时间内直接从调用者应用程序调用它。

    如果可能,请完全删除初始化 .data.bss(并调用 C++ 构造函数)的“向下复制”启动代码,以便在编写代码时出现链接器错误靠着这样。许多编译器可以选择跳过这个,通常称为“minimal/fast 启动”或类似的。

    这意味着必须检查任何外部库,以确保它们不包含任何此类依赖项。

  • 为程序实施和定义安全状态,在出现严重错误时您将恢复到该状态。

  • 实施错误report/error 日志系统总是有帮助的。

你想要3个以上的slave机器,在辐射环境之外有一个master。所有 I/O 都通过包含投票 and/or 重试机制的 master。每个从设备都必须有一个硬件看门狗,并且对它们进行碰撞的调用应该被 CRC 等包围,以减少非自愿碰撞的可能性。 Bumping 应该由 master 控制,所以与 master 失去连接等于在几秒钟内重新启动。

此解决方案的一个优点是,您可以对主服务器使用与从服务器相同的 API,因此冗余成为一种透明的功能。

编辑: 从评论中我觉得有必要澄清 "CRC idea." 如果你包围碰撞,奴隶碰撞它自己的看门狗的可能性接近于零对来自主机的随机数据进行 CRC 或摘要检查。该随机数据仅在受审查的从站与其他从站对齐时才从主站发送。随机数据和 CRC/digest 在每次碰撞后立即被清除。主从碰撞频率应大于 double 看门狗超时。 master发过来的数据每次都是唯一生成的

如果您的硬件出现故障,那么您可以使用机械存储来恢复它。如果您的代码库很小并且有一些物理 space 那么您可以使用机械数据存储。

会有material的表面不受辐射影响。多个齿轮将在那里。机械 reader 将 运行 所有齿轮,并且可以灵活地上下移动。向下表示它是 0,向上表示它是 1。您可以从 0 和 1 生成您的代码库。

既然你专门要求软件解决方案,而且你正在使用 C++,为什么不使用运算符重载来制作你自己的安全数​​据类型?例如:

而不是使用 uint32_t(和 doubleint64_t 等),制作您自己的 SAFE_uint32_t,其中包含 [=22 的倍数(最小为 3) =].重载你想要执行的所有操作(* + - / << >> = == != 等),并使重载操作独立地对每个内部值执行,即不要执行一次并复制结果。之前和之后,检查所有内部值是否匹配。如果值不匹配,您可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知有错误。

这样,无论 ALU、寄存器、RAM 还是总线发生损坏都无关紧要,您仍然可以进行多次尝试,并且很有可能发现错误。但是请注意,尽管这仅适用于您可以替换的变量 - 例如您的堆栈指针仍然容易受到影响。

旁白:我 运行 遇到了类似的问题,也是在旧的 ARM 芯片上。它原来是一个使用旧版本 GCC 的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,该错误会(有时)破坏传递给函数的值。在将其归咎于 radio-activity 之前,请确保您的设备没有任何问题,是的,有时它是一个编译器错误 =)

此答案假设您关心的是拥有一个正常工作的系统,而不是拥有一个成本最低或速度最快的系统;大多数玩放射性物品的人都重视正确性/安全性而不是速度/成本

有几个人建议您可以进行硬件更改(很好 - 这里的答案中已经有很多好东西,我不打算重复所有这些),其他人建议冗余(原则上很好),但是我认为没有人建议过这种冗余在实践中如何运作。你如何故障转移?你怎么知道什么时候有 'gone wrong'?许多技术在一切都会起作用的基础上起作用,因此失败是一件棘手的事情。然而,一些为规模设计的分布式计算技术预计失败(毕竟有足够的规模,对于单个节点的任何MTBF,许多节点中的一个节点的失败是不可避免的);您可以将其用于您的环境。

这里有一些想法:

  • 确保您的整个硬件被复制 n 次(其中 n 大于 2,最好是奇数),并且每个硬件元素都可以相互通信硬件元素。以太网是一种显而易见的方法,但还有许多其他更简单的路由可以提供更好的保护(例如 CAN)。尽量减少常用组件(甚至电源)。例如,这可能意味着在多个地方对 ADC 输入进行采样。

  • 确保您的应用程序状态在一个地方,例如在有限状态机中。这可以完全基于 RAM,但不排除稳定存储。因此它将被存储在几个地方。

  • 采用法定人数协议来改变状态。例如,参见 RAFT。当您在 C++ 中工作时,有一些众所周知的库。只有当大多数节点同意时,才会对 FSM 进行更改。为协议栈和仲裁协议使用一个已知的好库,而不是自己滚动一个,否则当仲裁协议挂起时,你在冗余方面所做的所有努力都将被浪费。

  • 确保您对 FSM 进行校验和(例如 CRC/SHA),并将 CRC/SHA 存储在 FSM 本身中(以及在消息中传输,并对消息本身进行校验和) ).让节点根据这些校验和、传入消息的校验和定期检查它们的 FSM,并检查它们的校验和是否与法定人数的校验和匹配。

  • 在您的系统中构建尽可能多的其他内部检查,使检测到自身故障的节点重新启动(如果您有足够的节点,这比继续半工作要好)。尝试让他们在重新启动期间彻底将自己从法定人数中移除,以防他们不再出现。重新启动时,让他们对软件映像(以及他们加载的任何其他内容)进行校验和,并在将自己重新引入法定人数之前进行完整的 RAM 测试。

  • 使用硬件来支持你,但要小心。例如,您可以获得 ECC RAM,并定期 read/write 通过它来纠正 ECC 错误(如果错误无法纠正,则恐慌)。然而(从内存来看)静态 RAM 比 DRAM 首先更能容忍电离辐射,因此 可能 最好改用静态 DRAM。另请参阅 'things I would not do' 下的第一点。

假设一天之内任何给定节点发生故障的几率为 1%,假设您可以使故障完全独立。如果有 5 个节点,您将需要三个才能在一天内失败,这是 0.00001% 的几率。有了更多,嗯,你明白了。

不会做的事情:

  • 低估一开始没有问题的价值。除非重量是一个问题,否则设备周围的一大块金属将会是一个比一组程序员想出的更便宜、更可靠的解决方案。 EMI 输入的光耦合也是一个问题,等等。无论如何,在采购组件时尝试采购那些额定抗电离辐射最佳的组件。

  • 推出您自己的算法。人们以前做过这件事。使用他们的作品。容错和分布式算法很难。尽可能使用其他人的作品。

  • 在天真的使用复杂的编译器设置希望你检测到更多的故障。如果你幸运的话,你可能会检测到更多的故障。更有可能的是,您将在编译器中使用经过较少测试的代码路径,特别是如果您自己滚动的话。

  • 使用在您的环境中未经测试的技术。大多数编写高可用性软件的人都必须模拟故障模式以检查他们的 HA 是否正常工作,并且错过了很多结果是故障模式。您处于 'fortunate' 经常按需失败的位置。因此,测试每项技术,并确保其应用程序实际提高 MTBF 的数量超过引入它的复杂性(复杂性会带来错误)。特别是将此应用到我关于法定人数算法等的建议中

有一点似乎没有人提到过。你说你在 GCC 中开发并交叉编译到 ARM 上。你怎么知道你没有对空闲 RAM、整数大小、指针大小、执行某个操作需要多长时间、系统将 运行 持续多长时间或各种东西做出假设的代码像那样?这是一个很常见的问题。

答案通常是自动化单元测试。编写在开发系统上执行代码的测试工具,然后 运行 在目标系统上执行相同的测试工具。寻找差异!

同时检查您的嵌入式设备上的勘误表。您可能会发现有关 "don't do this because it'll crash, so enable that compiler option and the compiler will work around it".

的内容

简而言之,最有可能的崩溃源是代码中的错误。在您完全确定情况并非如此之前,(目前)不要担心更深奥的故障模式。

考虑到 supercat 的评论、现代编译器的趋势和其他事情,我很想回到古代,在汇编和静态内存分配中编写整个代码。对于这种绝对的可靠性,我认为组装不再产生很大的成本差异。

有人提到使用较慢的芯片来防止离子轻易翻转位。以类似的方式,也许使用专门的 cpu/ram 实际上使用多个位来存储一个位。因此提供了硬件容错,因为所有位都被翻转的可能性很小。所以 1 = 1111 但需要被击中 4 次才能真正翻转。 (4 可能是一个错误的数字,因为如果翻转 2 位,它就已经不明确了)。所以如果你选择 8,你会得到 8 倍的 ram 和一些更慢的访问时间,但更可靠的数据表示。您可能可以在软件级别使用专门的编译器(为所有内容分配更多 space)或语言实现(为以这种方式分配内容的数据结构编写包装器)。或者具有相同逻辑结构但在固件中执行此操作的专用硬件。

这里有大量的回复,但我会尝试总结一下我的想法。

某些崩溃或无法正常工作可能是您自己的错误导致的 - 那么当您找到问题时应该很容易修复。但也有可能出现硬件故障 - 总体而言,即使不是不可能,也很难修复。

我建议首先尝试通过记录(堆栈、寄存器、函数调用)来捕获有问题的情况 - 通过将它们记录到文件中的某个地方,或者以某种方式直接传输它们("oh no - I'm crashing")。

从这种错误情况中恢复是重新启动(如果软件仍然存在并且正在运行)或硬件重置(例如硬件看门狗)。从第一个开始更容易。

如果问题与硬件有关 - 那么日志记录应该可以帮助您确定哪个函数调用出现了问题,并且可以让您了解什么地方不起作用以及在哪里。

此外,如果代码相对复杂 - "divide and conquer" 它是有意义的 - 这意味着你删除/禁用一些你怀疑有问题的函数调用 - 通常禁用一半代码并启用另一半 - 你可以得到"does work" / "does not work" 种决定之后你可以专注于另一半代码。 (哪里有问题)

如果一段时间后出现问题 - 那么可以怀疑堆栈溢出 - 那么最好监视堆栈点寄存器 - 如果它们不断增长。

并且如果您设法将代码完全最小化,直到出现 "hello world" 种应用程序 - 而且它仍然随机失败 - 那么硬件问题是预料之中的 - 并且需要 "hardware upgrade" - 这意味着发明这样的cpu / ram / ... - 能够更好地耐受辐射的硬件组合。

最重要的可能是如果机器完全停止/重置/不工作,您如何取回日志 - 可能是 bootstap 应该做的第一件事 - 如果发现有问题的情况,请返回家中。

如果在您的环境中也可以传输信号和接收响应 - 您可以尝试构建某种在线远程调试环境,但是您必须至少有工作的通信媒体和一些处理器/一些ram 处于工作状态。远程调试是指 GDB / gdb 存根类方法或您自己实现的您需要从应用程序中取回的内容(例如下载日志文件、下载调用堆栈、下载 ram、重启)

也许了解硬件是否意味着 "designed for this environment" 会有所帮助。它如何更正 and/or 表示存在 SEU 错误?

在一个 space 探索相关项目中,我们有一个自定义 MCU,它会引发 exception/interrupt SEU 错误,但会有一些延迟,即某些周期可能 pass/instructions在导致 SEU 异常的一个 insn 之后执行。

特别容易受到攻击的是数据缓存,因此处理程序会使有问题的缓存行无效并重新启动程序。只是,由于异常的不精确性,以引发异常的insn为首的insn序列可能无法重新启动。

我们确定了危险的(不可重启的)序列(如 lw , 0x0(),后跟一个 insn,它修改 </code> 并且不依赖于数据 <code>),并且我对 GCC 进行了修改,因此不会出现这样的序列(例如,作为最后的手段,用 nop 分隔两个 insn)。

只是需要考虑的事情...

免责声明:我不是放射性专业人士,也没有为此类应用工作。但是我为关键数据的长期归档研究了软错误和冗余,这在某种程度上是相关的(同样的问题,不同的目标)。

我认为放射性的主要问题是放射性可以转换位,因此放射性can/will篡改任何数字存储器。这些错误通常称为soft errors、bit rot 等

那么问题来了:当你的记忆力不可靠时,如何可靠地计算?

要显着降低软错误率(以计算开销为代价,因为它主要是基于软件的解决方案),您可以:

  • 依赖旧的 redundancy scheme, and more specifically the more efficient error correcting codes(目的相同,但算法更聪明,因此您可以用更少的冗余恢复更多的位)。这有时(错误地)也称为校验和。使用这种解决方案,您必须随时将程序的完整状态存储在 master variable/class(或结构?)中,计算 ECC,并在执行任何操作之前检查 ECC 是否正确,如果没有,请修复字段。然而,此解决方案并不能保证您的软件可以运行(简单地说它会在可以运行时正确运行,或者如果不能运行则停止运行,因为 ECC 可以告诉您是否有问题,在这种情况下您可以停止软件以便您不要得到假结果)。

  • 或者您可以使用弹性算法数据结构,它保证,在一定程度上,即使存在软错误。这些算法可以看作是常见算法结构与 ECC 方案的混合体,但这种方案的弹性要强得多,因为弹性方案与结构紧密相关,因此您不需要对额外的过程进行编码检查ECC,通常它们要快得多。这些结构提供了一种方法来确保您的程序在任何条件下都能正常工作,直到达到软错误的理论界限。您还可以将这些弹性结构与 redundancy/ECC 方案混合使用以获得额外的安全性(或者将您最重要的数据结构编码为弹性的,其余的,您可以从主要数据结构重新计算的可消耗数据,作为普通数据结构带有一些计算速度非常快的 ECC 或奇偶校验)。

如果您对弹性数据结构(这是算法学和冗余工程中最近出现但令人兴奋的新领域)感兴趣,我建议您阅读以下文档:

  • Resilient algorithms data structures intro by Giuseppe F.Italiano, Universita di Roma "Tor Vergata"

  • Christiano, P., Demaine, E. D., & Kishore, S. (2011)。具有附加开销的无损容错数据结构。在算法和数据结构中(第 243-254 页)。 Springer Berlin Heidelberg.

  • Ferraro-Petrillo, U., Grandoni, F., & Italiano, G. F. (2013)。对内存故障具有弹性的数据结构:字典的实验研究。实验算法学杂志 (JEA), 18, 1-6.

  • Italiano, G. F. (2010)。弹性算法和数据结构。在算法和复杂性中(第 13-24 页)。 Springer Berlin Heidelberg.

如果您有兴趣了解有关弹性数据结构领域的更多信息,您可以查看 Giuseppe F. Italiano 的作品(并通过参考资料进行操作)和 Faulty-RAM模型(在 Finocchi 等人 2005 年;Finocchi 和 Italiano 2008 年引入)。

/编辑:我主要针对 RAM 内存和数据存储从软错误中说明了 prevention/recovery,但我没有谈论 计算 (CPU) 错误。其他答案已经指出在数据库中使用原子事务,所以我将提出另一个更简单的方案:冗余和多数投票

这个想法是,你只需对你需要做的每个计算做 x 次相同的计算,并将结果存储在 x 个不同的变量中(x >= 3) .然后您可以 比较您的 x 变量:

  • 如果他们都同意,那么根本就没有计算错误。
  • 如果他们不同意,那么您可以使用多数票来获得正确的值,并且由于这意味着计算已部分损坏,您还可以触发 system/program 状态扫描以检查其余部分是否正确好的。
  • 如果多数票无法确定获胜者(所有 x 值都不同),那么这是触发故障保护程序(重启、向用户发出警报等)的完美信号。

与 ECC(实际上是 O(1))相比,此冗余方案非常快,并且当你需要故障保护。多数票也(几乎)保证永远不会产生损坏的输出,并且从较小的计算错误中恢复,因为 x 计算给出的概率相同的输出是无穷小的(因为有大量可能的输出,所以几乎不可能随机得到 3 次相同的结果,如果 x > 3 的机会就更少了)。

因此,通过多数投票,您可以避免损坏的输出,并且在冗余 x == 3 的情况下,您可以恢复 1 个错误(在 x == 4 的情况下,将有 2 个错误可恢复,等等 - 确切的等式是nb_error_recoverable == (x-2) 其中 x 是重复计算的次数,因为您需要至少 2 次一致的计算才能使用多数票恢复)。

缺点是你需要计算 x 次而不是一次,所以你有额外的计算成本,但是线性复杂度所以渐进地你不会因为你获得的好处而损失太多。进行多数表决的一种快速方法是计算数组的众数,但您也可以使用中值滤波器。

此外,如果您想额外确保计算正确进行,如果您可以制造自己的硬件,则可以使用 x CPUs 构建您的设备,并连接系统以便自动计算在 x CPUs 中复制,最后机械地完成多数表决(例如使用 AND/OR 门)。这通常在飞机和关键任务设备中实现(参见 triple modular redundancy)。这样,您不会有任何计算开销(因为额外的计算将并行完成),并且您有另一层保护免受软错误(因为计算重复和多数表决将直接由硬件管理,而不是由硬件管理软件——更容易被破坏,因为程序只是存储在内存中的位...)。

首先,围绕故障设计您的应用程序。确保作为正常流程操作的一部分,它会重置(取决于您的应用程序和软故障或硬故障类型)。这很难做到完美:需要一定程度事务性的关键操作可能需要在装配级别进行检查和调整,以便关键点的中断不会导致不一致的外部命令。 一旦检测到任何不可恢复内存损坏或控制流偏差,快速失败。如果可能,记录失败。

其次,在可能的情况下,纠正腐败并继续。这意味着经常校验和修复常量表(如果可以的话,还有程序代码);也许在每个主要操作之前或在定时中断上,并将变量存储在自动更正的结构中(再次在每个主要操作之前或在定时中断上从 3 中获得多数票,如果是单一偏差则更正)。如果可能,记录更正。

第三,测试失败。设置一个 repeatable 测试环境,伪随机翻转内存中的位。这将允许您复制损坏情况并帮助围绕它们设计您的应用程序。

使用 cyclic scheduler。这使您能够增加定期维护时间以检查关键数据的正确性。最常遇到的问题是堆栈损坏。如果您的软件是周期性的,您可以在周期之间重新初始化堆栈。不要将堆栈重复用于中断调用,为每个重要的中断调用设置一个单独的堆栈。

与 Watchdog 概念类似的是截止时间计时器。在调用函数之前启动硬件定时器。如果函数在截止时间计时器中断之前没有 return,则重新加载堆栈并重试。如果尝试 3/5 后仍然失败,则需要从 ROM 重新加载。

将您的软件拆分成多个部分并将这些部分隔离以使用单独的内存区域和执行时间(尤其是在控制环境中)。示例:信号采集、预处理数据、主要算法和结果implementation/transmission。这意味着一个部分的失败不会导致整个程序的其余部分失败。因此,当我们修复信号采集时,其余任务将继续处理陈旧数据。

一切都需要 CRC。如果您在 RAM 之外执行,即使您的 .text 也需要 CRC。如果您使用循环调度程序,请定期检查 CRC。一些编译器(不是 GCC)可以为每个部分生成 CRC,一些处理器有专门的硬件来进行 CRC 计算,但我想这会超出你的问题范围。检查 CRC 还会提示内存上的 ECC 控制器在出现问题之前修复单位错误。

使用看门狗进行启动,而不是只运行一次。如果您的启动 运行 遇到问题,您需要硬件帮助。

我真的读了很多很棒的答案!

这是我的2分:建立一个memory/register异常的统计模型,通过编写一个软件来检查内存或执行频繁的寄存器比较。此外,以虚拟机的形式创建一个模拟器,您可以在其中试验该问题。我想如果你改变结尺寸、时钟频率、供应商、外壳等,会观察到不同的行为。

即使我们的台式机内存也有一定的故障率,但这并不影响日常工作。