提问人:rook 提问时间:4/25/2016 最后编辑:Stargateurrook 更新时间:8/2/2021 访问量:222176
编译用于高放射性环境的应用程序
Compiling an application for use in highly radioactive environments
问:
我们正在编译一个嵌入式 C++ 应用程序,该应用程序部署在被电离辐射轰击的环境中的屏蔽设备中。我们正在使用 GCC 和 ARM 的交叉编译。部署后,我们的应用程序会生成一些错误数据,并且崩溃的频率比我们预期的要高。硬件是为这种环境而设计的,我们的应用程序已经在这个平台上运行了好几年。
我们是否可以对代码进行更改,或者可以进行编译时改进,以识别/纠正由单个事件扰乱引起的软错误和内存损坏?是否有任何其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
答:
美国宇航局有一篇关于抗辐射软件的论文。它描述了三个主要任务:
- 定期监控内存中的错误,然后清除这些错误,
- 强大的错误恢复机制,以及
- 如果某些东西不再起作用,可以重新配置。
请注意,内存扫描速率应足够频繁,以便很少发生多位错误,因为大多数 ECC 内存可以从单位错误中恢复,而不是从多位错误中恢复。
可靠的错误恢复包括控制流传输(通常在错误发生前的某个时间点重新启动进程)、资源释放和数据恢复。
他们对数据恢复的主要建议是避免需要它,方法是将中间数据视为临时数据,以便在错误发生之前重新启动也会将数据回滚到可靠状态。这听起来类似于数据库中的“事务”概念。
他们讨论了特别适用于面向对象语言(如 C++)的技术。例如
- 基于软件的 ECC,用于连续内存对象
- 按合约编程:验证前置条件和后置条件,然后检查对象以验证其仍处于有效状态。
而且,碰巧的是,NASA已经将C++用于火星探测器等重大项目。
C++ 类抽象和封装支持多个项目和开发人员之间的快速开发和测试。
他们避免了某些可能产生问题的 C++ 功能:
- 异常
- 模板
- Iostream(无控制台)
- 多重继承
- 运算符重载(除 和
new
delete
) - 动态分配(使用专用内存池和放置以避免系统堆损坏的可能性)。
new
评论
您可能还对有关算法容错主题的丰富文献感兴趣。这包括旧的赋值:编写一个排序,当恒定数量的比较失败时(或者,稍微邪恶的版本,当失败的比较的渐近数量与比较相同时),正确地对其输入进行排序。log(n)
n
Huang 和 Abraham 在 1984 年发表的论文“基于算法的矩阵运算容错”是一个值得阅读的地方。他们的想法与同态加密计算有点相似(但实际上并不相同,因为他们正在尝试在操作级别进行错误检测/纠正)。
该论文的最新后代是 Bosilca、Delmas、Dongarra 和 Langou 的“应用于高性能计算的基于算法的容错”。
以下是一些想法和想法:
更有创意地使用ROM。
将任何可以存储的内容存储在ROM中。与其计算东西,不如将查找表存储在 ROM 中。 (确保编译器将查找表输出到只读部分!在运行时打印出内存地址进行检查!将中断向量表存储在 ROM 中。当然,请运行一些测试,看看ROM与RAM相比的可靠性如何。
将最佳 RAM 用于堆栈。
堆栈中的 SEU 可能是最有可能的崩溃源,因为它是索引变量、状态变量、返回地址和各种指针等内容通常所在的位置。
实现定时器滴答和看门狗定时器例程。
您可以在每个计时器滴答时运行“健全性检查”例程,也可以运行看门狗例程来处理系统锁定。您的主代码还可以定期递增计数器以指示进度,健全性检查例程可以确保这种情况发生。
在软件中实现纠错码。
您可以为数据添加冗余,以便能够检测和/或纠正错误。这将增加处理时间,可能会使处理器暴露在辐射下的时间更长,从而增加出错的机会,因此您必须考虑权衡。
记住缓存。
检查 CPU 缓存的大小。您最近访问或修改的数据可能位于缓存中。我相信您至少可以禁用一些缓存(以很大的性能成本);您应该尝试此操作,看看缓存对 SEU 的敏感程度。如果缓存比 RAM 更坚固,那么您可以定期读取和重写关键数据,以确保它保留在缓存中并使 RAM 恢复正常。
巧妙地使用页面错误处理程序。
如果将内存页标记为不存在,则当您尝试访问内存页时,CPU 将发出页错误。您可以创建一个页面错误处理程序,该处理程序在处理读取请求之前执行一些检查。(PC 操作系统使用它来透明地加载已交换到磁盘的页面。
对关键事物(可能是一切)使用汇编语言。
使用汇编语言,您可以知道寄存器中的内容和RAM中的内容;您知道 CPU 正在使用哪些特殊的 RAM 表,并且您可以以迂回的方式进行设计以降低风险。
用于实际查看生成的汇编语言,并计算出每个例程占用的代码量。objdump
如果您使用的是像 Linux 这样的大型操作系统,那么您就是在自找麻烦;有太多的复杂性和太多的事情出错。
请记住,这是一个概率游戏。
一位评论者说
您为捕获错误而编写的每个例程都会因相同的原因而失败。
虽然这是真的,但检查例程正常运行所需的 100 字节代码和数据出错的几率远小于其他地方出错的几率。如果您的 ROM 非常可靠,并且几乎所有代码/数据实际上都在 ROM 中,那么您的几率就更大了。
使用冗余硬件。
使用具有相同代码的 2 个或更多相同的硬件设置。如果结果不同,则应触发重置。对于 3 台或更多设备,您可以使用“投票”系统来尝试识别哪一台设备遭到入侵。
评论
在小型化卫星*的软件/固件开发和环境测试方面工作了大约 4-5 年,我想在这里分享我的经验。
*(小型化卫星比大型卫星更容易发生单粒子扰动,因为它的电子元件尺寸相对较小,尺寸有限)
非常简明扼要:没有机制可以从可检测到的错误中恢复 软件/固件本身的情况,在某处没有至少一份软件/固件的最低工作版本副本用于恢复目的 - 并且具有支持恢复(功能)的硬件。
现在,这种情况通常在硬件和软件级别都得到处理。在这里,根据您的要求,我将分享我们在软件层面可以做的事情。
。恢复目的....提供在真实环境中更新/重新编译/重新刷新软件/固件的能力。对于高度电离环境中的任何软件/固件来说,这几乎都是必备的功能。如果没有这个,您可以拥有任意数量的冗余软件/硬件,但在某一时刻,它们都会爆炸。所以,准备这个功能!
。最低工作版本...在您的代码中具有响应式、多个副本、软件/固件的最低版本。这就像 Windows 中的安全模式。不要只有一个功能齐全的软件版本,而是拥有软件/固件最低版本的多个副本。最小副本的大小通常比完整副本小得多,并且几乎总是只有以下两个或三个功能:
- 能够监听来自外部系统的命令,
- 能够更新当前的软件/固件,
- 能够监控基本操作的内务数据。
。复制。。。地方。。。某处有冗余软件/固件。
无论有没有冗余硬件,您都可以尝试在 ARM uC 中使用冗余软件/固件。这通常是通过在不同的地址中有两个或多个相同的软件/固件来完成的,这些软件/固件相互发送心跳 - 但一次只有一个处于活动状态。如果已知一个或多个软件/固件无响应,请切换到另一个软件/固件。使用这种方法的好处是,我们可以在错误发生后立即进行功能更换 - 而无需与负责检测和修复错误的任何外部系统/方(在卫星情况下,通常是任务控制中心(MCC))进行任何联系。
严格来说,如果没有冗余硬件,这样做的缺点是实际上无法消除所有单点故障。至少,您仍然会有一个单点故障,即交换机本身(或通常是代码的开头)。然而,对于在高度电离环境中受尺寸限制的设备(例如微微/飞秒卫星),在没有额外硬件的情况下将单点故障减少到一个点仍然值得考虑。有时,切换的代码段肯定比整个程序的代码少得多 - 大大降低了在其中获得单个事件的风险。
但是,如果您不这样做,则外部系统中至少应该有一个副本,该副本可以与设备接触并更新软件/固件(在卫星情况下,它又是任务控制中心)。
- 您还可以将副本保存在设备的永久内存存储中,可以触发该副本以恢复正在运行的系统的软件/固件
。可检测到的错误情况..错误必须是可检测的,通常由硬件纠错/检测电路或一小段用于纠错/检测的代码进行检测。最好将此类代码放小、多且独立于主软件/固件。它的主要任务只是检查/纠正。如果硬件电路/固件是可靠的(例如它比其他电路/固件更耐辐射 - 或具有多个电路/逻辑),那么您可以考虑使用它进行纠错。但如果不是,最好将其作为错误检测。校正可以通过外部系统/设备进行。对于纠错,您可以考虑使用基本的纠错算法,如 Hamming/Golay23,因为它们可以在电路/软件中更容易实现。但这最终取决于您团队的能力。对于错误检测,通常使用CRC。
。支持恢复的硬件 现在,来谈谈这个问题最困难的方面。归根结底,恢复要求负责恢复的硬件至少正常运行。如果硬件永久损坏(通常在其总电离剂量达到一定水平后发生),那么(可悲的是)软件无法帮助恢复。因此,对于暴露于高辐射水平的设备(如卫星)来说,硬件是最重要的问题。
除了上面预测由于单事件翻转导致固件错误的建议外,我还建议您:
子系统间通信协议中的错误检测和/或纠错算法。这是另一个几乎必须具备的,以避免从其他系统接收到不完整/错误的信号
过滤ADC读数。请勿直接使用ADC读数。通过中值滤波器、平均值滤波器或任何其他滤波器对其进行过滤 - 永远不要相信单个读数值。多抽样,而不是少抽样 - 合理。
可以使用 C 来编写在此类环境中表现稳健的程序,但前提是禁用大多数形式的编译器优化。优化编译器旨在用“更高效”的编码模式替换许多看似冗余的编码模式,并且可能不知道程序员在编译器知道没有办法保存其他任何东西时进行测试的原因是因为程序员想要通过保留其他值来阻止某些代码的执行——即使它能够保持该值的唯一方法是系统收到某种电气故障。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();
如果编译器以相对字面的方式翻译代码,并且如果所有
在 、 之后重复检查系统状态。
该系统可以对几乎任何合理的单个毛刺事件保持鲁棒性,
即使是那些会任意破坏程序计数器和堆栈的程序。如果
在调用 之后发生故障,这意味着
该激活本来是合适的(因为在故障发生之前没有其他原因会调用)。如果
故障会导致代码不恰当地到达,但有
没有后续的故障事件,代码就没有办法随后
到达时没有通过验证检查或先调用 cancel_preparations [如果堆栈出现故障,执行可能会继续到调用返回的上下文之后的某个位置,但对 的调用将发生在对 and 的调用之间,从而使后一个调用无害。prepare_for_activation()
prepare_for_activation()
prepare_for_activation()
prepare_for_activation()
trigger_activation()
trigger_activation()
prepare_for_activation()
cancel_preparations()
prepare_for_activation()
trigger_activation()
这样的代码在传统 C 中可能是安全的,但在现代 C 编译器中就不安全了。在这种环境中,这样的编译器可能非常危险,因为它们咄咄逼人,只包含与通过某种明确定义的机制可能出现的情况相关的代码,并且其产生的后果也将得到很好的定义。在某些情况下,其目的是在故障后检测和清理的代码最终可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则它可能会推断出在这种情况下需要进行此类恢复的条件不可能发生,从而消除本来可以检查它们的代码。
评论
-O0
-O2
-O0
-O0
-O2
v1=v2+0xCAFEBABE
运行应用程序的多个实例怎么样?如果崩溃是由于随机内存位更改造成的,则您的某些应用实例很可能会通过并生成准确的结果。对于具有统计背景的人来说,计算出给定位触发概率需要多少个实例才能实现您希望的最小整体误差可能非常容易。
评论
这是一个非常广泛的主题。基本上,您无法真正从内存损坏中恢复,但至少可以尝试及时失败。以下是您可以使用的一些技术:
校验和常量数据。如果您有任何长时间保持不变的配置数据(包括您配置的硬件寄存器),请在初始化时计算其校验和并定期验证。当您发现不匹配时,是时候重新初始化或重置了。
存储具有冗余的变量。如果你有一个重要的变量,把它的值写在 中,并读作 。
x
x1
x2
x3
(x1 == x2) ? x2 : x3
实施程序流监控。XOR:一个全局标志,在从主循环调用的重要函数/分支中具有唯一值。在测试覆盖率接近 100% 的无辐射环境中运行程序,应在循环结束时为您提供标志的可接受值列表。如果看到偏差,请重置。
监视堆栈指针。在主循环的开头,将堆栈指针与其预期值进行比较。偏差时复位。
你问的是一个相当复杂的话题——不容易回答。其他答案还可以,但它们只涵盖了您需要做的所有事情的一小部分。
从评论中可以看出,不可能 100% 修复硬件问题,但是使用各种技术可以减少或捕获它们的可能性很高。
如果我是你,我会创建最高安全完整性等级(SIL-4)的软件。获取 IEC 61513 文档(适用于核工业)并遵循它。
评论
可以帮助你的是看门狗。看门狗在 1980 年代广泛用于工业计算。硬件故障当时更为常见 - 另一个答案也提到了那个时期。
看门狗是一种组合的硬件/软件功能。硬件是一个简单的计数器,从数字(比如 1023)倒计时到零。可以使用TTL或其他逻辑。
该软件的设计使得一个例程可以监控所有基本系统的正确操作。如果此例程正确完成 = 发现计算机运行正常,则会将计数器设置回 1023。
整体设计是,在正常情况下,软件可以防止硬件计数器达到零。如果计数器达到零,计数器的硬件将执行其唯一的任务并重置整个系统。从计数器的角度来看,0 等于 1024,计数器继续再次倒计时。
此监视器可确保在许多故障情况下重新启动连接的计算机。我必须承认,我不熟悉能够在当今计算机上执行此类功能的硬件。与外部硬件的接口现在比以前复杂得多。
看门狗的一个固有缺点是,从系统发生故障到看门狗计数器达到零 + 重新启动时间,系统都不可用。虽然该时间通常比任何外部或人为干预都要短得多,但支持的设备需要能够在该时间范围内在没有计算机控制的情况下进行。
评论
为放射性环境编写代码与为任何关键任务应用程序编写代码并没有什么不同。
除了已经提到的内容外,这里还有一些杂项提示:
使用任何半专业嵌入式系统上都应该存在的日常“面包和黄油”安全措施:内部看门狗、内部低电压检测器、内部时钟监控器。这些东西在2016年甚至不需要提及,它们几乎是每个现代微控制器的标准配置。
如果您有一个面向安全和/或汽车的MCU,它将具有某些看门狗功能,例如给定的时间窗口,您需要在该窗口内刷新看门狗。如果您有一个任务关键型实时系统,这是首选。
一般来说,使用适合这类系统的MCU,而不是你在一包玉米片中收到的一些通用的主流绒毛。如今,几乎每个MCU制造商都有专为安全应用设计的专用MCU(TI、飞思卡尔、瑞萨、意法半导体、英飞凌等)。它们具有许多内置的安全功能,包括锁步内核:这意味着有 2 个 CPU 内核执行相同的代码,并且它们必须相互一致。
重要说明:您必须确保内部MCU寄存器的完整性。所有可写的硬件外设的控制和状态寄存器都可能位于RAM存储器中,因此容易受到攻击。
为了保护自己免受寄存器损坏,最好选择具有寄存器内置“一次写入”功能的微控制器。此外,您需要将所有硬件寄存器的默认值存储在 NVM 中,并定期将这些值复制到寄存器中。您可以以同样的方式确保重要变量的完整性。
注意:始终使用防御性编程。这意味着您必须在MCU中设置所有寄存器,而不仅仅是应用程序使用的寄存器。您不希望一些随机的硬件外围设备突然唤醒。
有各种各样的方法可以检查RAM或NVM中的错误:校验和,“行走模式”,软件ECC等。如今,最好的解决方案是不使用其中任何一种,而是使用具有内置ECC和类似检查的MCU。因为在软件中执行此操作很复杂,因此错误检查本身可能会引入错误和意外问题。
使用冗余。您可以将易失性和非易失性存储器存储在两个相同的“镜像”段中,这些段必须始终是等效的。每个段都可以附加一个 CRC 校验和。
避免在MCU外部使用外部存储器。
为所有可能的中断/异常实现默认中断服务例程/默认异常处理程序。即使是你不使用的那些。默认例程除了关闭自己的中断源外,什么都不做。
理解并接受防御性编程的概念。这意味着您的程序需要处理所有可能的情况,即使是那些理论上不会发生的情况。例子。
高质量的任务关键型固件会检测尽可能多的错误,然后以安全的方式处理或忽略这些错误。
永远不要编写依赖于指定不当行为的程序。这种行为可能会因辐射或 EMI 引起的意外硬件变化而发生巨大变化。确保您的程序没有此类废话的最佳方法是使用像 MISRA 这样的编码标准以及静态分析器工具。这也将有助于防御性编程和清除错误(为什么你不想在任何类型的应用程序中检测错误?
重要说明:不要对静态存储持续时间变量的默认值进行任何依赖。也就是说,不要信任 or 的默认内容。从初始化点到实际使用变量点之间可能存在任何时间,RAM可能有足够的时间被损坏。相反,编写程序,以便在运行时从 NVM 设置所有此类变量,就在首次使用此类变量之前。
.data
.bss
在实践中,这意味着如果一个变量是在文件范围或 as 中声明的,则永远不应该使用它来初始化它(或者你可以,但这毫无意义,因为您无论如何都不能依赖该值)。始终在运行时,在使用前设置它。如果可以从 NVM 重复更新此类变量,请这样做。
static
=
同样,在 C++ 中,不要依赖构造函数来获取静态存储持续时间变量。让构造函数调用公共“设置”例程,您也可以稍后在运行时直接从调用方应用程序调用该例程。
如果可能,请完全删除初始化和(并调用 C++ 构造函数)的“复制”启动代码,以便在依赖此类代码编写代码时出现链接器错误。许多编译器可以选择跳过这一点,通常称为“最小/快速启动”或类似。
.data
.bss
这意味着必须检查任何外部库,以便它们不包含任何此类依赖。
实现并定义程序的安全状态,在发生严重错误时将恢复到该状态。
实现错误报告/错误日志系统总是有帮助的。
评论
.text
您需要 3+ 台从属机器,其中主设备位于辐射环境之外。所有 I/O 都通过包含投票和/或重试机制的主服务器。每个从站都必须有一个硬件看门狗,并且碰撞它们的调用应该被 CRC 等包围,以减少非自愿碰撞的可能性。碰撞应由主站控制,因此与主站的连接丢失等于在几秒钟内重新启动。
此解决方案的一个优点是,您可以对主站和从站使用相同的 API,因此冗余成为一项透明功能。
编辑:从评论中,我觉得有必要澄清“CRC的想法”。如果用 CRC 包围碰撞或对来自主站的随机数据进行摘要检查,则从站碰撞自己的看门狗的可能性接近于零。只有当被审查的从站与其他站对齐时,才会从站发送随机数据。每次碰撞后,随机数据和 CRC/摘要都会立即清除。主从碰撞频率应为看门狗超时的两倍以上。从主服务器发送的数据每次都是唯一生成的。
评论
如果您的硬件出现故障,则可以使用机械存储来恢复它。如果您的代码库很小并且有一些物理空间,则可以使用机械数据存储。
将有一个不受辐射影响的材料表面。将有多个齿轮。机械读码器将在所有齿轮上运行,并且可以灵活地上下移动。向下表示为 0,向上表示为 1。从 0 和 1 可以生成代码库。
评论
既然您特别要求软件解决方案,并且您使用的是 C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:
与其使用 (和 , 等),不如创建自己的包含倍数(至少 3)uint32_t。重载您希望执行的所有操作(* + - / << >> = == != 等),并使重载操作对每个内部值独立执行,即不要执行一次并复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,则可以将错误的值更新为具有最常见值的值。如果没有最常见的值,则可以安全地通知存在错误。uint32_t
double
int64_t
SAFE_uint32_t
这样,无论 ALU、寄存器、RAM 还是总线上发生损坏,您仍然会有多次尝试,并且很有可能捕获错误。但请注意,这仅适用于您可以替换的变量 - 例如,您的堆栈指针仍然容易受到攻击。
一个附带的故事:我遇到了类似的问题,也是在旧的ARM芯片上。事实证明,它是一个使用旧版本 GCC 的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,这些错误(有时)会损坏传递到函数中的值。在将其归咎于放射性活动之前,请确保您的设备没有任何问题,是的,有时它是编译器错误=)
评论
这个答案假设你关心的是拥有一个正常工作的系统,而不是拥有一个成本最低或快速的系统;大多数玩放射性物品的人都看重正确性/安全性而不是速度/成本
有几个人建议你可以进行硬件更改(很好 - 这里已经有很多好东西在答案中,我不打算重复所有这些),其他人建议冗余(原则上很好),但我认为没有人建议冗余在实践中如何工作。如何进行故障转移?你怎么知道什么时候“出了问题”?许多技术在一切正常的基础上工作,因此失败是一件棘手的事情。然而,一些为规模而设计的分布式计算技术预计会失败(毕竟,在足够规模的情况下,对于单个节点的任何 MTBF,许多节点中的一个节点的故障都是不可避免的);您可以将其用于您的环境。
以下是一些建议:
确保整个硬件的复制次数(大于 2,最好是奇数),并且每个硬件元素都可以相互通信。以太网是实现这一目标的一种明显方法,但还有许多其他更简单的路由可以提供更好的保护(例如CAN)。尽量减少常用组件(甚至电源)。例如,这可能意味着在多个位置对ADC输入进行采样。
n
n
确保应用程序状态位于单个位置,例如在有限状态机中。这可以完全基于 RAM,但并不排除稳定的存储。因此,它将存储在几个地方。
采用仲裁协议进行状态更改。例如,请参阅 RAFT。当您使用 C++ 时,有众所周知的库。只有在大多数节点同意的情况下,才会对 FSM 进行更改。为协议栈和仲裁协议使用已知良好的库,而不是自己滚动一个,否则当仲裁协议挂起时,您在冗余方面的所有良好工作都将付诸东流。
确保校验和(例如 CRC/SHA)您的 FSM,并将 CRC/SHA 存储在 FSM 本身中(以及在消息中传输,并对消息本身进行校验和)。让节点根据这些校验和定期检查其 FSM,对传入消息进行校验和,并检查其校验和是否与仲裁的校验和匹配。
在系统中构建尽可能多的其他内部检查,使检测到自身故障的节点重新启动(如果您有足够的节点,这比半途而废要好)。尝试让他们在重新启动期间将自己从仲裁中干净地删除,以防他们不再出现。重新启动时,让他们对软件映像(以及他们加载的任何其他内容)进行校验和,并在重新将自己引入仲裁之前进行完整的 RAM 测试。
使用硬件来支持您,但要小心。例如,您可以获取 ECC RAM,并定期读取/写入它以纠正 ECC 错误(如果错误无法纠正,则会崩溃)。然而,(从内存来看)静态RAM对电离辐射的容忍度远远高于DRAM首先,因此最好使用静态DRAM代替。另请参阅“我不会做的事情”下的第一点。
假设您在一天内有 1% 的几率发生任何给定节点的故障,让我们假设您可以完全独立地进行故障。使用 5 个节点时,您需要 3 个节点才能在一天内失败,这是 0.00001% 的几率。有了更多,好吧,你明白了。
我不会做的事情:
低估了一开始就没有问题的价值。除非重量是一个问题,否则在设备周围放置一大块金属将是一个比程序员团队所能想出的更便宜、更可靠的解决方案。EMI输入的光耦合也是一个问题,等等。无论如何,在采购组件时,请尝试采购那些额定抗电离辐射能力最好的组件。
滚动你自己的算法。人们以前做过这种事情。使用他们的工作。容错和分布式算法很难。尽可能使用他人的作品。
使用复杂的编译器设置,天真地希望您检测到更多故障。如果幸运的话,可能会检测到更多故障。更有可能的是,您将在编译器中使用测试较少的代码路径,尤其是在您自己滚动它时。
使用在您的环境中未经测试的技术。大多数编写高可用性软件的人必须模拟故障模式来检查其 HA 是否正常工作,并因此错过许多故障模式。您处于“幸运”的位置,经常按需出现故障。因此,测试每种技术,并确保其应用实际将 MTBF 提高到超过引入它的复杂性的程度(复杂性会带来错误)。特别是将其应用于我的建议,重新仲裁算法等。
评论
似乎没有人提到过一点。你说你正在 GCC 中开发并在 ARM 上交叉编译。你怎么知道你没有代码来假设可用 RAM、整数大小、指针大小、执行某个操作需要多长时间、系统将连续运行多长时间或类似的东西?这是一个非常普遍的问题。
答案通常是自动化单元测试。编写测试工具,在开发系统上执行代码,然后在目标系统上运行相同的测试工具。寻找差异!
还要检查嵌入式设备上的勘误表。你可能会发现“不要这样做,因为它会崩溃,所以启用该编译器选项,编译器就会解决它”。
简而言之,最有可能的崩溃来源是代码中的错误。在你确定情况并非如此之前,不要担心(还)更深奥的故障模式。
考虑到 supercat 的评论、现代编译器的趋势以及其他因素,我很想回到古代,在任何地方都用汇编和静态内存分配来编写整个代码。对于这种完全的可靠性,我认为组装不再产生很大的成本差异。
评论
有人提到使用较慢的芯片来防止离子轻易翻转位。以类似的方式,也许使用专门的 CPU/RAM,它实际上使用多个位来存储单个位。从而提供硬件容错能力,因为所有位都不太可能被翻转。所以 1 = 1111,但需要被击中 4 次才能真正翻转。(4 可能是一个糟糕的数字,因为如果 2 位被翻转,它已经模棱两可了)。因此,如果您使用 8,您将获得 8 倍的 ram 和一些较慢的访问时间,但更可靠的数据表示。你可以在软件层面上使用专门的编译器(为所有内容分配x个空间)或语言实现(为以这种方式分配东西的数据结构编写包装器)来做到这一点。或者具有相同逻辑结构但在固件中执行此操作的专用硬件。
这里有大量的回复,但我会试着总结一下我对此的看法。
某些东西崩溃或无法正常工作可能是由于您自己的错误造成的 - 那么当您找到问题时,它应该很容易修复。但也有可能出现硬件故障 - 即使不是不可能,也很难解决。
我建议首先尝试通过记录(堆栈、寄存器、函数调用)来捕获有问题的情况 - 要么将它们记录到文件中的某个地方,要么以某种方式直接传输它们(“哦不 - 我正在崩溃”)。
从这种错误情况中恢复是重新启动(如果软件仍然处于活动状态并启动)或硬件重置(例如硬件看门狗)。从第一个开始更容易。
如果问题与硬件相关 - 那么日志记录应该可以帮助您确定哪个函数调用问题发生,这可以为您提供内部知识,了解哪些不起作用以及在哪里工作。
此外,如果代码相对复杂 - “分而治之”是有意义的 - 这意味着你删除/禁用一些你怀疑有问题的函数调用 - 通常禁用一半的代码并启用另一半 - 你可以得到“不起作用”/“不起作用”类型的决定,之后你可以专注于代码的另一半。(问题出在哪里)
如果问题在一段时间后发生 - 那么可以怀疑堆栈溢出 - 那么最好监视堆栈点寄存器 - 如果它们不断增长。
如果你设法完全最小化你的代码,直到“hello world”类型的应用程序 - 它仍然随机失败 - 那么硬件问题是预期的 - 并且需要“硬件升级” - 这意味着发明这样的cpu / ram / ... - 硬件组合,可以更好地承受辐射。
最重要的事情可能是,如果机器完全停止/重置/无法正常工作,您如何取回日志 - 可能是bootstap应该做的第一件事 - 如果遇到问题情况,请回家。
如果在您的环境中也可以发送信号并接收响应 - 您可以尝试构建某种在线远程调试环境,但您必须至少具有通信介质的工作状态和一些处理器/一些内存处于工作状态。我所说的远程调试是指 GDB / gdb 存根类型的方法,或者您自己实现从应用程序返回所需的内容(例如,下载日志文件、下载调用堆栈、下载 ram、重新启动)
评论
也许知道硬件是否意味着“为这种环境而设计”会有所帮助。它如何纠正和/或指示 SEU 错误的存在?
在一个与太空探索相关的项目中,我们有一个定制的 MCU,它会在 SEU 错误时引发异常/中断,但会有一些延迟,即某些周期可能会在导致 SEU 异常的 insn 之后通过/执行指令。
数据缓存特别容易受到攻击,因此处理程序会使有问题的缓存行失效并重新启动程序。只是,由于异常的不精确性,以引发 insn 的异常为首的 insn 序列可能无法重新启动。
我们确定了危险的(不可重启的)序列(例如,后跟一个 insn,它会修改并且不依赖于数据),并且我对 GCC 进行了修改,因此不会发生此类序列(例如,作为最后的手段,用 a 将两个 insn 分开)。lw $3, 0x0($2)
$2
$3
nop
只是要考虑的事情......
免责声明:我不是放射性专业人士,也没有为这种应用工作过。但是我研究了关键数据长期存档的软错误和冗余,这在某种程度上是相互关联的(相同的问题,不同的目标)。
在我看来,放射性的主要问题是放射性可以切换位,因此放射性可以/将篡改任何数字存储器。这些错误通常称为软错误、位腐烂等。
那么问题来了:当你的内存不可靠时,如何可靠地计算?
为了显著降低软错误率(以计算开销为代价,因为它主要是基于软件的解决方案),您可以:
依靠良好的旧冗余方案,更具体地说,更具体地说,是更有效的纠错码(目的相同,但算法更聪明,因此您可以用更少的冗余恢复更多的位)。这有时(错误地)也称为校验和。使用这种解决方案,您必须随时将程序的完整状态存储在主变量/类(或结构体?)中,计算 ECC,并在执行任何操作之前检查 ECC 是否正确,如果不是,请修复字段。但是,此解决方案并不能保证您的软件可以正常工作(只是它可以正常工作,否则会停止工作,因为 ECC 可以告诉您是否有问题,在这种情况下,您可以停止软件,以免获得虚假结果)。
或者,您可以使用弹性算法数据结构,这在一定程度上保证了即使存在软错误,您的程序仍会给出正确的结果。这些算法可以看作是通用算法结构与原生混合的 ECC 方案的混合体,但这比这更有弹性,因为弹性方案与结构紧密绑定,因此您不需要编码额外的过程来检查 ECC,而且通常它们要快得多。这些结构提供了一种方法,可以确保程序在任何条件下都能正常工作,直到软错误的理论范围。您还可以将这些弹性结构与冗余/ECC 方案混合使用,以提高安全性(或者将最重要的数据结构编码为弹性,其余的编码为可以从主数据结构重新计算的消耗性数据,作为具有一点 ECC 或奇偶校验的普通数据结构,计算速度非常快)。
如果你对弹性数据结构感兴趣(这是算法和冗余工程中最近但令人兴奋的新领域),我建议你阅读以下文档:
Christiano,P.,Demaine,ED和Kishore,S.(2011)。具有累加开销的无损容错数据结构。在算法和数据结构中(第 243-254 页)。施普林格柏林海德堡。
Ferraro-Petrillo,U.,Grandoni,F.和Italiano,GF(2013)。数据结构对内存故障的弹性:字典的实验研究。实验算法学杂志(JEA),18,1-6。
Italiano,GF(2010 年)。弹性算法和数据结构。在算法和复杂性中(第 13-24 页)。施普林格柏林海德堡。
如果你有兴趣了解更多关于弹性数据结构领域的信息,你可以看看Giuseppe F. Italiano的作品(并按照你的参考文献工作)和Faulty-RAM模型(在Finocchi et al. 2005中引入;Finocchi 和 Italiano 2008)。
/编辑:我主要针对RAM内存和数据存储说明了软错误的预防/恢复,但我没有谈论计算(CPU)错误。其他答案已经指向像在数据库中一样使用原子事务,所以我将提出另一个更简单的方案:冗余和多数投票。
这个想法是,你只需对你需要做的每个计算进行x倍的相同计算,并将结果存储在x个不同的变量中(x >= 3)。然后,您可以比较 x 变量:
- 如果他们都同意,那么根本没有计算错误。
- 如果他们不同意,那么您可以使用多数票来获得正确的值,并且由于这意味着计算已部分损坏,因此您还可以触发系统/程序状态扫描以检查其余部分是否正常。
- 如果多数票无法确定获胜者(所有 X 值都不同),那么这是触发故障保护程序(重新启动、向用户发出警报等)的完美信号。
与ECC(实际上是O(1))相比,这种冗余方案非常快,并且在需要故障保护时为您提供清晰的信号。多数投票也(几乎)保证永远不会产生损坏的输出,并且还可以从轻微的计算错误中恢复过来,因为 x 计算给出相同输出的概率是无限小的(因为有大量可能的输出,几乎不可能随机获得 3 倍相同的结果,如果 x > 3,机会就更少了)。
因此,使用多数投票,您可以避免输出损坏,并且使用冗余 x == 3,您可以恢复 1 个错误(使用 x == 4 时,可以恢复 2 个错误,依此类推——确切的等式是 x 是计算重复的次数,因为您至少需要 2 次一致的计算才能使用多数投票进行恢复)。nb_error_recoverable == (x-2)
缺点是你需要计算 x 次而不是一次,所以你有额外的计算成本,但线性复杂性如此之近,你不会因为你获得的好处而损失太多。进行多数投票的快速方法是计算数组的模式,但您也可以使用中值滤波器。
此外,如果你想额外确保计算正确进行,如果你可以制作自己的硬件,你可以用 x 个 CPU 构建你的设备,并连接系统,以便在 x 个 CPU 之间自动复制计算,最后以机械方式进行多数投票(例如使用 AND/OR 门)。这通常在飞机和关键任务设备中实现(参见三重模块化冗余)。这样一来,您就不会有任何计算开销(因为额外的计算将并行完成),并且您有另一层保护,防止软错误(因为计算重复和多数投票将直接由硬件而不是软件管理——这更容易被破坏,因为程序只是存储在内存中的位......
首先,围绕故障设计应用程序。确保作为正常流程操作的一部分,它需要重置(具体取决于您的应用程序和软故障或硬故障类型)。这很难做到完美:需要一定程度的事务性的关键操作可能需要在汇编级别进行检查和调整,以便在关键点中断不会导致不一致的外部命令。一旦检测到任何不可恢复的内存损坏或控制流偏差,即可快速失败。如果可能,请记录失败。
其次,在可能的情况下,纠正腐败并继续。这意味着经常校验和修复常量表(如果可以的话,还有程序代码);也许在每次主要操作之前或定时中断时,并将变量存储在自动更正的结构中(同样,在每次主要操作之前或定时中断时,从 3 中获取多数票,如果是单个偏差,则进行更正)。如果可能,请记录更正。
第三,测试失败。设置一个可重复的测试环境,以随机方式翻转内存中的位。这将允许您复制损坏情况,并帮助围绕它们设计应用程序。
使用循环调度程序。这使您能够添加定期维护时间以检查关键数据的正确性。最常遇到的问题是堆栈损坏。如果您的软件是周期性的,则可以在周期之间重新初始化堆栈。不要将堆栈重用于中断调用,为每个重要的中断调用设置单独的堆栈。
与看门狗概念类似的是截止日期计时器。在调用函数之前启动硬件计时器。如果函数在截止时间计时器中断之前未返回,则重新加载堆栈并重试。如果在 3/5 次尝试后仍然失败,则需要从 ROM 重新加载。
将软件拆分为多个部分,并将这些部分隔离开来,以使用单独的内存区域和执行时间(尤其是在控制环境中)。例如:信号采集、数据预备、主算法和结果实现/传输。这意味着一个部分的故障不会导致程序其余部分的故障。因此,当我们修复信号采集时,其余任务会继续处理陈旧数据。
一切都需要CRC。如果你在RAM上执行,即使是你的.text也需要CRC。如果使用循环调度程序,请定期检查 CRC。一些编译器(不是 GCC)可以为每个部分生成 CRC,一些处理器有专用的硬件来执行 CRC 计算,但我想这将超出您的问题范围。检查CRC还会提示存储器上的ECC控制器在出现问题之前修复单位错误。
使用看门狗进行启动,不只运行一次。如果您的启动遇到问题,您需要硬件帮助。
我真的读了很多很棒的答案!
这是我的 2 美分:通过编写软件来检查内存或执行频繁的寄存器比较,建立内存/寄存器异常的统计模型。此外,以虚拟机的样式创建一个模拟器,您可以在其中试验该问题。我想如果你改变结点大小、时钟频率、供应商、外壳等,就会观察到不同的行为。
即使是我们的台式电脑内存也有一定的故障率,但这不会影响日常工作。
上一个:< 比 <= 快吗?
下一个:C 中的函数指针如何工作?
评论