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 信号,并声明 halt 令牌中止令牌的目的是防止内核中的多个内容尝试同时执行重新启动。
  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 刷新其数据之前关闭文件系统。

设计

从概念上讲,该设计非常简单,使用了许多现有的系统组件,但以一种新的方式组合在一起。从高层来看,该策略是当内核检测到内存不足时,会在内存中记录一些状态,向用户空间发送内存不足信号,内核会等待用户空间在超时时间内回调内核,用户空间会接收到该信号,多个用户空间组件会按照已建立的角色有序关闭,最后用户空间会回调内核,从而允许内核将数据存储在 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 值,处理程序代码保持不变,具体来说,它会尝试获取暂停令牌,如果无法获取,则永远休眠。如果因内存不足而导致内核启动重新启动,则发出令牌信号将允许内存监视程序完成其工作并重新启动系统。如果用户空间在内存看门狗的超时时间到期之前未调用 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 功能获得控制系统电源状态的权限。添加对根作业的访问权限会增加相应组件的权限。

此 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_powerctl 并将 cmd 的值设为 ZX_SYSTEM_POWERCTL_ACK_KERNEL_INITIATED_REBOOT,或者内核的重新启动计时器过期。作为替代方案,我们可以允许任何兼容的 zx_system_powerctl 调用来完成重新启动。兼容的调用是指也会重新启动系统的调用,无论传递的 cmd 值如何。这样可以解决用户空间在内核发出 OOM 信号之前独立决定重新启动系统的竞态情况。内核可能会根据在 zx_system_powerctl 调用中实际收到的 cmd 值写入不同的重新启动原因。这样就可以审核系统是否通常遵循预期的重新启动路径。

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

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

缺点:某些赛事仍有可能举行

现在,内核可能会检测到内存不足情况,声明停止令牌,然后用户空间调用 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_managerdriver_managerzx_system_powerctl 的请求将如前所述失败,并且根作业将崩溃,但系统不会重新启动,直到 MemoryWatchdog 的超时时间到期。这场比赛有多糟糕?它相当良性,因为用户空间会自行清理。在根作业崩溃时,用户空间已尽可能多地清理了自身。最大的缺点是重新启动的及时性会降低。

另一种可能的竞态条件是,如果 reboot-on-terminate 组件在错误的时间退出,组件可以自行配置,以便在退出时 component_manager 会重启系统。component_manager 通过调用 fuchsia.hardware.power.statecontrol/Admin.Reboot 重新启动系统。如果在 component_manager 被告知要拆除组件拓扑后,reboot-on-terminate 组件退出,则 component_manager 不会尝试重新启动系统。如果此类在终止时重新启动的组件在某个组件调用 Admin.Reboot 之后但在 power_manager 指示 component_manager 拆除拓扑之前退出,系统会崩溃,因为如果 component_managerpower_manager 的调用失败,component_manager 会出现 panic。此 RFC 并未提出针对此竞态条件的修复方案。由于我们不知道 component_manager 发生 panic 时有多少系统在运行,因此竞争可能会导致不可预测的行为。

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

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