RFC-0152:改进了 OOM 处理行为

RFC-0152:改进了 OOM 处理行为
状态已接受
区域
  • 驱动程序
  • 内核
  • 电源
说明

通过允许更多用户空间代码退出并为用户空间创建信号路径,以便在用户空间清理完成时通知内核,从而改进了内存不足处理。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-12-20
审核日期(年-月-日)2022-02-09

摘要

此 RFC 的目标是提高捕获调试数据的可靠性,并最大限度地减少在内存不足 (“OOM”)事件期间生成无用数据。为此,我们会以有序的方式关闭更多用户空间,而不是使用目前更基本的机制。

设计初衷

在系统耗尽内存时收集数据有助于确定事件的根本原因。目前,我们会收集一些数据(例如内存报告),但无法保证日志有助于我们更好地了解系统中发生的情况。目前,收集所有相关数据是一项具有挑战性的任务,因为系统中很少有部分知道系统何时会耗尽内存。此外,在发生 OOM 后分析可用数据可能很困难。处理 OOM 的方式通常会导致一系列次要影响,从而增加数据的噪声。这反复导致了混乱、冗长讨论和错误结论,造成了时间浪费。

利益相关方

主持人:hjfreyer@google.com

审核员:dgilhooley@google.com、frousseau@google.com、maniscalco@google.com、palmer@google.com、pankhurst@google.com、ppi@google.com、pshickel@google.com、rashaeqbal@google.com、shayba@google.com、surajmalhotra@google.com

咨询人员:adanis@google.com、alexlegg@google.com、geb@google.com、johngro@google.com、wez@google.com

社交化:我们最初在讨论各种相关 bug 的过程中提出了这一想法。然后,我们就拟议的解决方案咨询了关键利益相关方。

背景

在 Fuchsia 上处理内存不足 (OOM) 情况的过程如下所示:

  1. 如果可用内存继续减少,内存监控程序会检测系统的可用内存是否达到 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 阈值。内核现在已提交重新启动系统。
  2. 内存监控器为用户空间生成 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 信号,并声明停止令牌停止令牌的用途是防止内核中的多个操作同时尝试重新启动。
  3. 内存监视程序会在重新启动之前进入休眠状态 8 秒。用户空间无法向内核指明它已准备好重新启动。
  4. driver_manager 会监控 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 信号。 driver_manager 启动时,它会使用 system_get_event 系统调用订阅信号,该调用需要根作业的句柄。
  5. driver_manager 会指示 fshost 关闭,然后指示所有驱动程序停止。系统这样做是为了尽量减少丢失的数据,并在重新启动之前使硬件进入一致状态。
  6. driver_managerfshost 之外,用户空间会一直运行,直到内存监视程序的计时器到期。在此期间,通常会出现各种崩溃,因为程序会尝试访问已被分页的、但由于文件系统已不存在而无法分页的执行文件页面。这些崩溃会产生大量噪声,如果有内容监听串行日志,可能会记录这些噪声。

当前的 OOM 处理方式不会使用它,但已经有一种方法可以优雅地拆解用户空间。此机制会按反向依赖项顺序停止组件(包括文件系统和驱动程序),并让所有组件都有机会进行清理。

memory_monitor 不参与 OOM 处理,但对低内存事件感兴趣。RFC-0091 创建了 ZX_SYSTEM_EVENT_IMMINENT_OUT_OF_MEMORY 事件,当系统可用内存达到略高于 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 阈值的级别时,内核会生成此事件。memory_monitor 会监控此信号,并尝试将其内存配置数据持久化到存储空间。这是尽力而为,因为 memory_monitor 无法发出已处理事件的信号,并且系统可能会随时达到较低阈值,在 memory_monitor 能够刷新其数据之前关闭文件系统。

设计

该设计在概念上非常简单,并使用了系统的许多现有部分,但采用了新的方式进行组合。概括地说,该策略是:当内核检测到内存不足时,它会在内存中记录一些状态,向用户空间发送 OOM 信号,内核会设置超时并等待用户空间回调到内核,用户空间收到信号,多个用户空间组件在有序关闭中发挥其已建立的作用,最后用户空间回调到内核,内核可以将数据存储在 NVRAM 中并完成重新启动。

具体而言,该序列如下:

  • 内核的内存监控程序检测到系统内存非常不足,并
    • 声明暂停令牌
    • 使用 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 信号用户空间
    • 设置计时器
  • pwrbtn-monitor 会接收用户空间信号,并通过 fuchsia.hardware.power.statecontrol/Admin.Reboot 调用与 power_manager 通信。
  • 然后,power_manager 通过 fuchsia.device.manager/SystemStateTransition.SetTerminationSystemState 调用告知 driver_manager,在退出之前,它应将系统移至 REBOOT_KERNEL_INITIATED 电源状态。
  • power_manager 会通过调用 fuchsia.sys2/SystemController.Shutdown 调用告知 component_manager 拆解组件拓扑。
  • component_manager 会按相反的依赖项顺序拆解组件拓扑,最终告知 driver_manager 退出。
  • driver_manager 发现它应在退出之前执行转换到 REBOOT_KERNEL_INITIATED 状态。
  • driver_manager 调用 zx_system_powerctl,并在退出之前将 ZX_SYSTEM_POWERCTL_ACK_KERNEL_INITIATED_REBOOT 作为 cmd 值传递。
  • 内核会接收系统调用并发送停止令牌。
  • 系统会运行其余的 OOM 处理,以便将适当的信息写入 NVRAM,以便在重新启动后进行读取。
  • 内核会重启系统。

实现

控制内核 OOM 超时

此 RFC 提议添加一个内核启动选项来控制 OOM 超时。目前,OOM 超时已硬编码为 8 秒。实现此 RFC 会增加为处理 OOM 而执行的代码量,因此需要设置较长的超时时间。

对停止令牌所做的更改

此 RFC 建议将停止令牌更改为包含内核事件对象,而不是仅仅是原子布尔值。停止令牌将继续成为不可撤消的声明对象。内核中将使用停止令牌的事件对象来协调重新启动。借助停止令牌,您无需获取令牌即可向事件对象发送信号。

OOM 处理流程

目前,当内核检测到 OOM 时,会为用户空间生成 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 信号,获取停止令牌,并启动 8 秒计时器。此 RFC 建议,每当内核想要重新启动系统,但也要给用户空间执行某项操作的机会时,内核都应先获取停止令牌,然后通知用户空间,并等待一段时间。等待边界应等于用户模式在响应事件时允许运行的最长时间。这与当前实现形成鲜明对比,当前实现的超时值同时是等待时长的最大值和最小值。一旦停止令牌的事件收到信号或超时,内核将完成重新启动操作。如果发生 OOM,决定重启的内核代码位于内存监视器中,该监视器会通过创建 OOM 崩溃日志、将其存储在 NVRAM 中并重启来完成 OOM 处理。

如前所述,此 RFC 提议添加通过内核启动选项设置 OOM 超时功能。

目前,用户空间 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 处理程序位于 driver_manager 中。此 RFC 建议将处理程序移至 pwrbtn-monitorpwrbtn-monitor 是所有 build 中都存在的一个现有组件,用于控制某些硬件的电源状态。实际上,我们可以将 OOM 视为软件生成的电源按钮按下操作。由于 pwrbtn-monitor 的职责增加,我们建议将其重命名为 system-event-monitor

pwrbtn-monitor 收到信号时,它会调用 fuchsia.hardware.power.statecontrol/Admin.Reboot。我们将为 OOM 添加一个新的 RebootReasonpwrbtn-monitor 将将其传递给此调用。该调用会启动现有的用户模式正常关闭路径,该路径会按反向依赖项顺序解构组件拓扑,并以 driver_manager 更改硬件电源状态结束。此 RFC 建议,在处理 OOM 时,driver_manager 应始终使用使用 zx_system_powerctl 系统调用的重新启动路径,并将新值 ZX_SYSTEM_POWERCTL_ACK_KERNEL_INITIATED_REBOOT 作为 cmd 参数的值传递。driver_manager 中的现有路径恰好使用了 zx_system_powerctl。在 x86 上,当 driver_manager 将重启完成情况委托给板级驱动程序,并且板级驱动程序执行系统调用时,就会发生这种情况。在 arm64 上,driver_manager 会直接进行系统调用。此 RFC 中的更改正式要求在重新启动路径中使用 zx_system_powerctl

当 Zircon 收到 zx_system_powerctlcmd 值为 ZX_SYSTEM_POWERCTL_ACK_KERNEL_INITIATED_REBOOT 时,处理程序代码会尝试发出停止令牌信号。如果未声明停止令牌,则信号会失败,并且系统调用会返回错误。对于其他 cmd 值,处理程序代码保持不变,具体而言,它会尝试获取停止令牌,如果未能成功,则永久进入休眠状态。如果内核因 OOM 而发起重启,则发出令牌信号将允许内存监视程序完成其工作并重启系统。如果用户空间未在内存监视程序的超时期限到期之前调用 zx_system_powerctl,监视程序将继续其关闭过程并重新启动系统,这与当前实现保持不变。

性能

在某些情况下,OOM 处理时间预计会比目前更长。目前,在 OOM 期间,driver_manager 会停止文件系统、停止驱动程序,然后不执行任何其他操作。内核检测到 OOM 后 8 秒钟,系统会重新启动。

实现此 RFC 后,大部分用户空间都有机会对即将发生的系统重新启动做出响应。具体而言,power_manager 会通过 RebootWatcher 协议通知监听器即将重新启动。power_manager 为客户端设置了 5 秒的超时期限。power_manager 通知重启监视器后,会告知 component_manager 拆解组件拓扑。组件拓扑会按反向依赖项顺序拆解,这意味着并非所有组件都会同时停止。组件具有停止的超时期限。实现此 RFC 后,在 OOM 期间有更多代码有机会执行,并且可能会遇到各种超时。这些因素可能会导致重启时间超过 8 秒,但在大多数现代系统上,此过程的用时要比 8 秒短得多。

此 RFC 不会尝试解决在内核发出 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 信号后某些内容分配更多内存的问题。

向后兼容性

不应存在向后兼容性问题,这些更改可以作为软过渡进行。

安全注意事项

此 RFC 建议将 zx_system_get_event 系统调用的使用从 driver_manager 移至 pwrbtn-monitor。此系统调用需要根作业的句柄,这是一个非常敏感的句柄。pwrbtn-monitor 是一个专注于特定领域的小型组件,已经可以通过 fuchsia.hardware.power.statecontrol/Admin capability 控制系统电源状态。向根作业添加访问权限会增加此组件的权限。

此 RFC 还建议延长重新启动超时时间。只有在我们认为 OOM 是攻击媒介且重新启动超时时间较长时,才需要考虑这一点,因为这会给攻击者更多时间来执行漏洞利用。

测试

需要进行测试来验证在用户空间在内核超时之前关闭时和关闭时,系统是否会按预期重新启动。如果这些测试不存在,系统会添加这些测试。

我们可能还需要性能测试来分析用户空间拆解所需的时间。这些性能分析测试可用于确定内核超时值。

文档

应该更新各种 API 文档,但无需进行新的概念更新,因为此 RFC 更像是对信号的重新布线,而不是对系统行为进行根本性更改。

缺点、替代方案和未知情况

替代方案:用户空间处理程序位置

我们可以将 ZX_SYSTEM_EVENT_OUT_OF_MEMORY 的用户空间处理程序放置在多个位置。最好将处理脚本添加到 ZBI 中,并在所有商品中显示,以便尽早提供一致的处理体验。主要备选方案是 power_manager、shutdown-shim 和 component_manager。选择 pwrbtn-monitor 的主要原因是,此责任与其在响应事件时重新启动系统的总体工作相符,而 OOM 只是软件生成的事件,而不是硬件事件。

替代方案:针对所有用户空间发起的重新启动报告 NO_CRASH

目前,Zircon 会在 OOM 期间将数据写入永久性内存,而此 RFC 建议继续采用这种做法。作为替代方案,我们可以在每次用户空间触发 zx_system_powerctl 调用以重新启动系统时将相同的数据写入永久 RAM,无论内核是否检测到 OOM 并向用户空间发送 OOM 信号。如果我们这样做,那么如果在发生 OOM 后成功优雅关闭了用户空间,反馈组件会看到 Zircon 中的 NO_CRASH 重新启动原因。如果内核计时器过期,并且 Zircon 在 OOM 后重启了系统,则反馈会看到 Zircon 中的 OOM 重启原因。

此方法的缺点是,用户空间处理 OOM 时出现的问题可能会导致系统知道已重启,但不知道重启是由 OOM 引起的。在这种情况下,反馈会看到 Zircon 中存在 NO_CRASH 重启原因,但在磁盘上找不到任何持久性崩溃信息。在这种情况下,反馈功能仍会提交报告。

替代方案:允许在 OOM 重启期间向 zx_system_powerctl 发出兼容请求

此 RFC 提议,一旦内核发起的 OOM 重新启动开始,只有两种情况会完成重新启动:用户空间调用 zx_system_powerctlcmd 值为 ZX_SYSTEM_POWERCTL_ACK_KERNEL_INITIATED_REBOOT,或者内核的重新启动计时器超时。作为替代方案,我们可以允许对 zx_system_powerctl 的任何兼容调用来完成重新启动。兼容的调用会重启系统,无论传递的 cmd 值如何。这将解决以下竞态情况:在内核发出 OOM 信号之前,用户空间独立决定重新启动系统。内核可能会根据其在 zx_system_powerctl 调用中实际收到的 cmd 值写入不同的重新启动原因。这样,您就可以审核系统是否通常遵循预期的重新启动路径。

替代方案:通过内核对象发出用户空间处理完成信号

此 RFC 建议通过使用特定的 cmd 值调用 zx_system_powerctl 来完成内核发起的 OOM 重新启动。相反,可以更改内核到用户空间的信号机制,以便用户空间接收通道或事件对象。然后,用户空间可以发送消息或断言/取消断言信号,以指示可以继续重新启动。这种替代方案的优势在于,完成重新启动的组件无需访问根资源。必须拥有对根资源的访问权限才能执行 zx_system_powerctl。这种替代方案需要进行更多工作,因为它是对当前内核/用户空间信号传递方式的更大变更。

缺点:某些比赛仍有可能出现

目前,内核可能会检测到 OOM 情况,声明停止令牌,然后用户空间调用 zx_system_powerctl,因为用户空间之前决定重新启动。在这种情况下,对 zx_system_powerctl 的调用将失败。然后 driver_manager 会退出。component_manager 会继续拆解组件拓扑,最终到达 power_manager 并将其终止。由于 power_manager 被设置为对根作业至关重要,因此终止 power_manager 会导致根作业崩溃。通常,当根作业终止时,Zircon 会重新启动系统,但在本例中,由于 MemoryWatchdog 持有停止令牌,因此不会重新启动系统。而是会在 MemoryWatchdog 超时后重新启动系统。

此 RFC 允许类似的竞态。当发生 OOM 情况时,用户空间可能正在拆解,为重启做准备。pwrbtn-monitor 可能已退出,这意味着用户空间中没有任何内容可以监控来自内核的 OOM 信号。或者,pwrbtn-monitor 可能正在运行,但其尝试重新启动系统的操作将会失败,因为 power_manager 仅允许一个正在处理的请求来关闭或重新启动系统。先前的拆解请求最终会到达 driver_manager。如前所述,driver_managerzx_system_powerctl 的请求将失败,根作业将崩溃,但在 MemoryWatchdog 的超时期限结束之前,系统不会重新启动。这个竞态有多激烈?这很温和,因为用户空间会自行清理。当根作业崩溃时,用户空间已清理尽可能多的自身内容。最大的缺点是,重新启动速度会变慢。

另一种可能的竞态是,终止时重新启动组件恰好在错误的时间退出。组件可以自行配置,以便在退出时 component_manager 会重启系统。component_manager 通过调用 fuchsia.hardware.power.statecontrol/Admin.Reboot 来重启系统。如果在 component_manager 被告知拆解组件拓扑后,会终止“终止时重启”组件,则 component_manager 不会尝试重启系统。如果此类“终止时重新启动”组件在某个组件调用 Admin.Reboot 后,但在 power_manager 告知 component_manager 拆解拓扑之前退出,系统将崩溃,因为如果 component_managerpower_manager 的调用失败,它会 panic。此 RFC 未提出针对此竞态问题的修复方法。由于我们不知道 component_manager 紧急警报时系统运行了多少进程,因此争用可能会导致不可预测的行为。

未知:对完全耗尽内存的几率有何影响

此 RFC 对系统在 OOM 处理期间完全耗尽内存的风险的净影响尚不确定。由于大多数用户空间组件不会监听退出信号,并且会在其客户端退出后立即终止,因此所提议的更改可以降低系统完全耗尽内存的几率。这应该会快速开始释放内存。我们希望确实观察到退出信号的用户空间组件应立即退出,同时释放内存。由于某些系统的停机时间可能会超过现有的 8 秒超时时间,因此建议的更改可能会增加内存耗尽的几率,因为这会为某些组件分配内存留出更多时间。此 RFC 还会延迟关闭文件系统,目前文件系统会快速退出。文件系统缓存通常采用由内核管理的可丢弃内存的形式。目前尚不清楚文件系统运行时间延长是否会对内存产生显著的负面影响。