| RFC-0281:架构例外情况和作业调试器例外情况渠道 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 提议更新作业调试器异常渠道,以接收架构和政策异常类型,同时继续允许许多客户端 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2026-04-21 |
| 审核日期(年-月-日) | 2026-04-21 |
问题陈述
各种开发者工具需要访问 Zircon 提供的异常渠道,以便监控作业、进程和线程(统称为“任务”)在执行期间可能出现的异常。此类工具目前可能会声明任何 Zircon 任务对象的异常渠道,以实现此目的。不过,这并不总是可行。例如,当监控同一父作业中的多个同级进程时,该工具必须声明每个进程的异常渠道。在大规模场景下(例如自动化测试基础架构或监控功能齐全的 Starnix 容器),此操作的成本会非常高。相反,该工具更希望能够使用作业的异常渠道来监控该作业下的所有进程。由于作业异常渠道的独占性,以及作业调试器异常渠道缺少异常传递功能,因此目前无法实现此目的。
摘要
此 RFC 提议更改 Zircon 的异常传递机制,以便将作业调试器异常通道纳入传递路径,同时保持多个用户空间实体绑定到同一作业的能力。这使得作业调试器异常渠道与 Zircon 任务层次结构中的通用异常传递框架的设计保持一致,并允许工具利用能够保证同时高效监控多个进程的功能。
设计初衷
异常传播
源自 Zircon 线程的异常会按照精确的传递顺序传递到用户空间以供处理。处理程序通过任务句柄上的 zx_task_create_exception_channel 进行注册,并且有两种类型:
- 正常 - 称为“例外渠道”。
- 调试程序 - 称为“调试程序异常渠道”。
在 Zircon 中注册时,处理程序必须通过 zx_task_create_exception_channel 的选项指定它们是“正常”异常处理程序还是“调试器”异常处理程序。这两种类型的渠道之间没有语义差异。在收到异常时,处理程序应在释放其对异常的句柄之前,将 zx::exception 对象上的 ZX_PROP_EXCEPTION_STATE 属性设置为适当的值,以将处理标记为“完成”。处理程序可以将异常标记为 ZX_EXCEPTION_STATE_HANDLED(这将终止遍历并继续执行线程)、ZX_EXCEPTION_STATE_TRY_NEXT(将异常发送到下一个处理程序)或 ZX_EXCEPTION_STATE_THREAD_EXIT(立即终止线程,从而终止遍历)。
不同类型的例外渠道之间存在两个主要区别。第一个区别在于 Zircon 定义的遍历顺序。走动顺序决定了哪些异常渠道可以按什么顺序看到异常。对于架构和政策例外情况,遍历顺序定义为:
| 步骤 | 渠道 | 递送类型 |
|---|---|---|
| 1 | 进程调试器 | 第一机会 |
| 2 | 线程 | 第一机会 |
| 3 | 流程 | 第一机会 |
| 4 | 进程调试器 | Second-chance |
| 5 | 作业 | 第一机会 |
| 6 | 父作业 | 第一机会 |
| 7 | 祖父母作业 | 第一机会 |
| … | 向上遍历作业树,直到到达根作业 |
生成异常时,Zircon 会先将异常发送到进程调试器异常通道,然后等待处理程序关闭其异常通道或关闭其异常句柄。如果异常仍未得到处理,则会按遍历顺序传递给下一个处理程序,Zircon 同样会等待句柄关闭,然后再继续处理下一个异常。每个例外渠道都只允许有一个处理程序。另一个尝试对同一任务调用 zx_task_create_exception_channel 的处理程序将返回 ZX_ERR_ALREADY_BOUND,并且不会收到任何异常。
第二个区别在于哪些异常类型会发送到哪些渠道。一般来说,Zircon 定义了两种类型的异常:
- 架构 - 例如分段错误、页面错误或未定义的指令。
- 合成 - 例如,政策违规或各种启动和停止通知。
就本文档而言,分组略有不同:
- 严重异常 - 所有架构异常和政策违规。如果异常处理程序未处理这些异常,则保证进程终止。
- 非严重异常 - 除了违反政策之外的所有合成异常。 这些异常仅发送到“调试器”风格的异常渠道,如果未处理,不会终止进程。
这意味着,非严重异常的异常传播遍历顺序有很大不同:
| 步骤 | 渠道 | 递送类型 |
|---|---|---|
| 1 | 进程调试器 | 第一机会 |
| 2 | 作业调试器 | 第一机会 |
| 3 | 进程调试器 | Second-chance |
| 4 | 作业调试器 | Second-chance |
在发送到作业 Debugger 异常渠道后,遍历会终止,并且不会考虑其他异常渠道。从技术上讲,这些合成的非严重异常支持第二机会异常,但在实践中未使用。
受限模式下的异常传播
对于在受限模式下运行的线程(请参阅 RFC-0261),异常传播有所不同。zx_restricted_enter 的调用方充当在受限模式下执行时发生的异常的线程内处理程序。此处理程序会以如下方式逻辑注入到上表中:
| 步骤 | 渠道 | 递送类型 |
|---|---|---|
| 1 | 进程调试器 | 第一机会 |
| 2 | 在消息串中 | 第一机会 |
| 3 | 线程 | 第一机会 |
| 4 | 流程 | 第一机会 |
| 5 | 进程调试器 | Second-chance |
| 6 | 作业 | 第一机会 |
| 7 | 父作业 | 第一机会 |
| 8 | 祖父母作业 | 第一机会 |
| … | 向上遍历作业树,直到到达根作业 |
不过,这种“线程内”处理方式的一个缺点是,异常无法再通过典型的 Zircon 异常渠道进一步传播。换句话说,从 Zircon 的角度来看,发送到线程内处理程序的任何异常始终被视为已处理。
这样一来,当架构或政策异常源自受限模式时,实际的异常传递表如下所示:
| 步骤 | 渠道 | 递送类型 |
|---|---|---|
| 1 | 进程调试器 | 第一机会 |
| 2 | 在消息串中 | 第一机会 |
| 3 | 不适用 | 不适用 |
换句话说,当受限模式下发生异常时,线程内处理程序可以被视为始终将异常标记为已处理的捕获所有异常的处理程序。这意味着,典型异常传播列表中的其他实体将无法看到该异常。这也意味着,虽然进程调试器处理程序仍然可以在线程内处理程序之前接收到异常,但它会失去稍后注册“第二次机会”异常处理的能力。这是一个可接受的权衡,因为进程调试器仍会先于线程内处理程序获得检查和可能处理异常的机会,然后线程内处理程序才会看到并处理异常。
注意:上述情况仅在线程以受限模式执行时发生。如果同一线程因任何原因(例如处理系统调用)在正常模式下运行,并在正常模式下引发异常,则该线程的处理方式与没有任何受限状态的任何其他 Zircon 线程相同。
非严重异常类型不会发送到线程内异常处理程序,因此会保留与异常传播中所述相同的传送顺序。
第二机会异常处理
在处理异常时,通过进程调试器异常通道注册的异常处理程序可以将 ZX_PROP_EXCEPTION_STRATEGY 属性设置为 ZX_EXCEPTION_STRATEGY_SECOND_CHANCE,以便在进程调试器获得首次机会后,如果其他处理程序仍未处理同一异常,则稍后有机会再次处理该异常。如上所述,处理程序应注意,对于来自受限模式下线程的异常,可能不会发生这种情况。当在源自受限模式的异常上设置此属性时,Zircon 不会返回错误。
作业异常渠道争用
当前的 Zircon 异常机制规定,作业异常渠道只能由单个实体声明。当多个系统组件需要合理地观察作业级异常时,这会带来问题。例如,调试程序可能需要拦截作业中一组进程的软件断点异常,而崩溃诊断服务需要观察未处理的异常以进行诊断和报告,并且作业的某些子进程需要能够打开作业异常通道来处理其父作业中其他进程的故障。这三种情况都是在特定进程的父作业上创建异常渠道的合理用例,但只有其中一个实体可以声明该渠道。
目前,可以使用组件清单中的 job_with_available_exception_channel 标志来构建组件,但这仅适用于组件作业中的进程。调试和崩溃报告工具无法假定存在作业层次结构,使得在进程父作业之上的某个作业上存在可用的异常渠道。
作业调试器异常渠道
在 RFC-0178 中,作业调试器异常渠道的用途得到了扩展,允许在单个作业中注册多个 (ZX_EXCEPTION_CHANNEL_JOB_MAX_COUNT) 异常渠道,前提是这是一个“仅通知”渠道。换句话说,它不接收架构异常,只接收进程启动事件。
如今,作业调试器异常渠道在异常传递流水线中发挥着特殊作用。这是不同类型的异常渠道之间的第二个区别。与作业调试器异常渠道类似的进程调试器异常渠道在发送给它的异常类型方面有所不同:
| 异常类型 | 作业异常渠道 | 作业调试器异常渠道 | 处理异常渠道 | 处理调试程序异常渠道 |
|---|---|---|---|---|
| 建筑和政策例外情形 | ✅ | ❌ | ✅ | ✅ |
| “子”起始事件 | ❌ | ✅(流程) | ❌ | ✅(Threads) |
| “子”退出事件 | ❌ | ⚠️(仅限信号) | ❌ | ✅(Threads) |
今天的作业调试器异常渠道仅接收“进程启动”和“进程退出”事件,其中前者属于 zx::exception 类型,后者属于 zx::signal 类型。架构和政策例外情况不会发送到作业调试器例外情况渠道。
“子”开始和退出事件的传递
Zircon 定义的作业树具有明确的定义:作业有子级,子级可能包含零个或多个作业和零个或多个进程。进程也有子进程,由一个或多个线程组成。运行系统中能够区分作业的其他实体可以声明作业的调试器异常通道以接收进程启动事件,并且可以声明任何进程的调试器异常通道以接收线程启动和线程退出事件。Zircon 尚未提供 Zircon 作业的等效概念的通知,这超出了此 RFC 的范围。
所有这些事件的传递方式都相同:将 zx::exception 句柄传递给父级的调试器异常渠道。这样,绑定到调试程序异常渠道的客户端就可以在保证“子”实体处于暂停状态时执行任意操作,例如,调试程序可以在进程的对象句柄上设置 ZX_PROP_PROCESS_BREAK_ON_LOAD,或者在线程被销毁之前为线程的销毁执行必要的记账操作。
目前的进程退出事件比较特殊:它们会作为信号 ZX_PROCESS_TERMINATED 发送,而不是作为 zx::exception 发送到作业调试器异常渠道。这种差异不仅仅是语义上的差异:由于进程终止是作为 Zircon 信号(而非异常)发送的,因此它是完全异步的。监听此信号的实体在信号传递时无法保证任何内容,进程对象可能已被 Zircon 销毁。相比之下,发送到进程调试器异常渠道的 ThreadExiting 事件可从 Zircon 获得额外保证,确保线程的状态仍可访问。因此,为了让观看实体在程序退出期间正确停止进程,以便检查最终程序状态(主要是句柄表和内存)以及线程状态,它必须正确处理所有线程启动和退出事件,并明确注意到最终线程何时退出,这会发送 zx::exception 通知,以便实体根据需要尽可能长时间地保留该通知,而不是仅仅发送信号。
请注意,虽然 Zircon 保证提供针对典型线程拆解机制的保护,但它不保证其他进程在此期间可能对线程或其父进程执行的操作,例如发出 zx_task_kill 系统调用,这将立即终止进程及其所有线程,无论进程调试器异常通道状态如何。换句话说,处理给定线程的异常并不能保护其进程免受通过 zx_task_kill 立即终止的风险。
利益相关方
辅导员:
- abarth@google.com
审核者:
- mcgrathr@google.com
- maniscalco@google.com
- jamesr@google.com
已咨询:
- abarth@google.com
- cpu@google.com
- lindkvist@google.com
共同化:
此 RFC 的早期版本曾在 fuchsia-zircon-discuss 邮件列表中传播,并由调试和测试架构团队进行了讨论。
要求
设计必须确保将 Zircon 异常类型中所述的架构和政策异常传递到作业调试器异常渠道,并且作业调试器异常渠道继续允许 RFC-0178 中所述的多个注册者。
设计
将架构和政策例外情况发送到作业调试器渠道
我们建议增强现有的作业调试器异常渠道,使其除了目前接收的 ZX_EXCP_PROCESS_STARTING 事件之外,还能接收架构和政策异常。
例外渠道走访顺序
此更改需要修改 Zircon 传播异常的顺序。根据异常渠道类型文档,架构和政策异常的新传递顺序将为:
| 步骤 | 渠道 | 递送类型 |
|---|---|---|
| 1 | 进程调试器 | 第一机会 |
| 2 | 作业调试器 | 首次机会,N 次 |
| 3 | 线程 | 第一机会 |
| 4 | 流程 | 第一机会 |
| 5 | 进程调试器 | Second-chance |
| 6 | 作业调试器 | 复活,N 次 |
| 7 | 作业 | 第一机会 |
| 8 | 祖先作业调试器 | 首次机会,N 次 |
| 9 | 祖先作业 | 第一机会 |
| … | 向上遍历作业树,继续使用作业调试器 和作业异常渠道,直到到达根作业 |
主要变化是,作业调试器异常渠道现在将接收这些异常,最多接收 N 次,其中 N 等于 ZX_EXCEPTION_CHANNEL_JOB_MAX_COUNT。进程的父作业的作业调试程序异常渠道在遍历中紧随进程调试程序异常渠道,这意味着附加到异常线程的最近父作业的客户端将获得第一次机会和第二次机会来处理异常,就像使用进程调试程序异常渠道一样。
然后,遍历会继续向上移动到作业树,从作业调试程序到作业异常渠道,一直到根作业。这样,调试器实现就可以自由选择在作业层次结构中的哪个位置附加自身,以满足各种使用情形。
在附加到作业调试程序异常渠道时接收二度机会异常的机会仅适用于父作业 - 父作业以上的祖先作业只有首次机会来检查异常。
交付架构和政策例外情况
根据 Zircon 异常类型,作业调试器异常渠道是唯一不接收架构异常的异常渠道,这使其显得不必要地独特。此做法的原因可追溯到 RFC-0178 之前,但动机已简要提及:
不过,“debug job”在这里很特别,因为它是一个仅限通知的渠道:它唯一可以接收的异常类型是
ZX_EXCP_PROCESS_STARTING,其中ZX_PROP_EXCEPTION_STATE会被忽略。因此,您可以在一个作业上允许使用多个调试异常渠道,而无需担心出现不一致的情况。
此处的“不一致”是指向可能具有多个已注册客户端的此类异常渠道传送架构异常的顺序。由于适用于所有其他异常渠道的排他性原则不适用于作业调试器异常渠道,因此无法明确定义在同一级别下,哪个异常渠道会先于另一个异常渠道看到异常事件。
此 RFC 建议将此视为非问题。异常传递给特定作业的作业调试器处理程序的顺序由 Zircon 实现定义,处理程序有责任了解,在同一级别下,其他处理程序可能会先于它标记异常为已处理。同样,处理程序也必须知道,它们已收到后续其他实体的异常,但该异常附加到同一作业的作业调试器异常渠道。
这意味着,对于希望在作业树中的特定层级独占访问异常的处理程序,此机制无效。此类处理程序应继续使用作业的异常渠道,并处理该渠道已被其他处理程序声明的情况,例如当 zx_task_create_exception_channel 返回 ZX_ERR_ALREADY_BOUND 时。
开启受限模式
以受限模式运行的线程需要特别谨慎地处理。在受限模式下执行时触发异常的线程在异常传递到进程调试器异常通道(如 RFC-0261 中所指定)时,会保持在受限模式下。只有在进程调试器渠道完成与异常相关的业务,并使异常处于未处理状态后,线程才会从受限模式退出并进入正常模式,以便通过受限模式的特殊线程内异常处理程序进行处理。没有其他异常渠道会遇到该异常,并且没有受限模式下的异常传播中所述的第二次机会异常。
此 RFC 未提议对受限模式例外情况处理进行任何更改。后续 RFC 将详细介绍此流程以及它将如何与作业调试器异常渠道交互。
处理逻辑
作业调试器渠道将按“先到先得”的原则(根据作业调试器渠道的注册顺序)接收异常。在作业调试程序渠道中,第一个将异常标记为已处理(即在异常句柄上设置 ZX_EXCEPTION_STATE_HANDLED 属性)的客户端将终止对相应作业的作业调试程序异常渠道列表的遍历,并阻止异常进一步向上层作业树传播。
连接到作业调试器渠道的客户端必须知道,其他已注册的客户端可能会先于它们处理异常。这已编码到作业调试器渠道的合约中,因此对于希望在生产环境中运行的通用系统崩溃处理程序而言,这是一种不合适的接收异常的机制。
| 异常类型 | 作业异常渠道 | 作业调试器异常渠道 | 处理异常渠道 | 处理调试程序异常渠道 |
|---|---|---|---|---|
| 建筑和政策例外情形 | ✅ | ✅(N 次) | ✅ | ✅ |
| “子”起始事件 | ❌ | ✅(流程) | ❌ | ✅(Threads) |
| “子”退出事件 | ❌ | ⚠️(仅限流程、信号) | ❌ | ✅(Threads) |
ProcessExiting 事件
ProcessExiting 异常事件的创建和传递将在未来的 RFC 中进行讨论。ZX_PROCESS_TERMINATED 信号的传递方式保持不变。
实现
此提案不会更改 zx_task_create_exception_channel 的系统调用 API 和 ABI。根据 RFC-0178,zx_task_create_exception_channel 将继续允许创建最多 ZX_EXCEPTION_CHANNEL_JOB_MAX_COUNT 个渠道,而不是在第一个渠道之后返回 ZX_ERR_ALREADY_BOUND。
使用 ZX_EXCEPTION_CHANNEL_DEBUGGER 选项将 zx_task_create_exception_channel 的用户需要了解,他们现在还可能会收到作业子进程的架构和政策例外情况。目前,此渠道的用户很少,可以轻松地根据 Zircon 的变化进行内嵌更新。如需详细了解这些用户,请参阅下文。
性能
如果多个实体声明了作业调试程序异常渠道,则异常传递性能会受到影响,因为异常必须先传递给(可能多个)其他客户端,然后才能到达线程和/或进程将终止的根作业。尽管如此,任何单个客户端仍可能会无限期地持有异常,当有多个客户端将接收到异常时,这种情况并不会变得更糟。
工效学设计
使用 zx_task_create_exception_channel 的人体工程学和从 Zircon 到用户空间的异常传递保持不变。
向后兼容性
系统 ABI 影响
此更改会修改传递异常的系统 ABI。之前,通过注册 Job Debugger 异常渠道无法获得架构或政策例外情况。在此更改之后,注册者不仅需要了解这些异常的传递,还需要了解他们可能不是作业树中此级别的第一个异常接收者。
截至撰写本文时,Job Debugger 异常渠道只有两个值得注意的非测试用户:
这两个实体都存在于 fuchsia.git 源代码树中,并且可以轻松更新,而无需引入显式版本控制。
根据特定使用情形的调试器配置,系统可能会在沿链转发异常之前收集有关线程的信息,或者在调试器用户指示的情况下将异常标记为已处理。调试程序在各种配置和设置中可能会出现其他应用场景,这些应用场景将留给调试程序实现来根据此 ABI 更改进行适当处理。
对于性能分析器,它只对进程启动通知感兴趣,因此它可以立即关闭并忽略从该渠道接收到的任何其他 zx::exception 对象。
对 elf_runner 的影响
elf_runner 今天会生成并声明每个 ELF 组件的作业异常渠道,以便为 crashsvc(Fuchsia 的崩溃服务)提供 CrashIntrospect 协议。
这方面可以改进,以便 elf_runner 现在只需要在 RootJob 上获取单个作业调试程序异常渠道,该渠道保证在异常发送到 RootJob 的异常渠道之前接收异常,从而确保 crashsvc 仍然可以访问崩溃组件的组件信息,同时要求 elf_runner 声明的资源要少得多。
这些更改并非 elf_runner 的当务之急,因为该组件目前未使用作业调试器异常渠道,因此在初始实现中不会发生变化。
安全注意事项
RFC-0178 中介绍了允许的作业调试器渠道的上限,此提案中未对此进行修改,从而防止了针对内核的任何 DOS 向量。
异常信息通常在工程环境和生产环境中都可供声明特定 Zircon 任务的异常渠道的代码使用,因此本提案不会暴露其他敏感信息。不过,它会增加可检查异常信息的实体的允许上限。
隐私注意事项
此提案不涉及任何隐私方面的问题。
测试
将向 //zircon/system/utest/debugger 添加新的测试用例,以涵盖此功能。
文档
异常类型文档将更新,以反映将接收架构异常的异常渠道的新顺序,以及有关如何处理受限模式线程中的异常的新注释。
缺点、替代方案和未知因素
我们还考虑了另外两种主要方法,作为所提议解决方案的替代方案:
FIDL 异常服务器
这种方法会将异常处理集中在 elf_runner 等组件中,然后该组件会向多个感兴趣的客户端提供新的 FIDL 协议。
- 优点:可完全控制异常排序和政策(例如,区分单个“处理程序”和多个“仅通知”监听器)。
- 缺点:会增加用户空间组件 (
elf_runner) 的复杂性,由于不可复制,需要迭代传递异常处理,并将过滤负担转移到客户端。
对作业异常渠道的 Zircon 修改
此方法会修改 Zircon,以允许多个组件成功对作业调用 zx_task_create_exception_channel,但需要通过传递给 Zircon 系统调用的新选项来表达“处理程序”与“仅通知”兴趣。
- 优点:利用 Zircon 现有的异常处理权限和流程。允许 Zircon 同时向所有“仅通知”渠道发送异常。 通过要求客户端定位特定作业来自动完成过滤。
- 缺点:需要对 Zircon 内核 API 进行更重大的更改,引入新选项来区分“处理程序”和“仅通知”客户端。
建议的解决方案(使用作业调试器渠道)是首选方案,因为它扩展了现有的多监听器机制(作业调试器渠道)来处理新的异常类型,从而最大限度地减少了作业调试器异常渠道与进程调试器异常渠道之间的差异,并最大限度地减少了 Zircon 中的新 API 表面积。