异常处理

简介

当线程遇到故障条件(例如分段错误)时,执行 处于暂停状态,并且线程进入异常处理阶段。具有 都会收到通知,并有机会 检查或更正使用情况

此功能常用于调试程序或崩溃记录器,它们需要 有机会在线程崩溃之前与其交互。 适用于只想跟踪任务生命周期,而不需要 拦截崩溃,signals 可能是更好的选择。

基础知识

通过在用户空间上创建异常渠道, zx_task_create_exception_channel() 执行任务(线程、进程或作业) 系统调用。创建的标识名是标准的 Zircon channel,但以只读方式创建,因此只能使用 接收异常消息。

发生异常时,该线程会暂停,并显示包含 zx_exception_info_t,并向相应渠道发送异常句柄。通过 异常的生命周期会绑定到此异常句柄的生命周期,因此 当接收器处理完毕后,系统会继续关闭此异常句柄 例外情况。此异常句柄是不可复制的,也就是说,在任何 则此异常只有一个处理程序。

默认情况下,关闭异常句柄会使线程保持暂停状态,并发送 将异常传递给下一个处理程序。如果接收方已更正例外情况 并希望线程继续执行,则可以更改异常 在以下时间之前,通过zx_object_set_property()的状态更改为ZX_EXCEPTION_STATE_HANDLED 正在关闭。

异常句柄

异常句柄的行为类似于挂起令牌,具体方法是保留线程, 处于暂停状态,直到他们关闭为止。此外,异常句柄还包含一些函数, 以帮助接收方处理异常:

从异常中检索到的任务句柄与任务具有相同的权限 最初传递到 zx_task_create_exception_channel() 中。

示例

这个简单的示例创建了一个异常渠道和读取异常的循环 直到任务关闭。

void ExceptionHandlerLoop(zx_handle_t task) {
  // Create the exception channel.
  uint32_t options = 0;
  zx_handle_t channel;
  zx_status_t status = zx_task_create_exception_channel(task, options,
                                                        &channel);
  // ... check status ...

  while (true) {
    // Wait until we get ZX_CHANNEL_READABLE (exception) or
    // ZX_CHANNEL_PEER_CLOSED (task terminated).
    zx_signals_t signals = 0;
    status = zx_object_wait_one(channel,
                                ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED,
                                ZX_TIME_INFINITE, &signals);
    // ... check status ...

    if (signals & ZX_CHANNEL_READABLE) {
      // Read the exception info and handle from the channel.
      zx_exception_info_t info;
      zx_handle_t exception;
      status = zx_channel_read(channel, 0, &info, &exception, sizeof(info), 1,
                               nullptr, nullptr);
      // ... check status ...

      // Send the exception out to some other function for processing, which
      // returns true if the exception has been handled and we can resume the
      // thread, or false to pass the exception to the next handler.
      bool handled = process_exception(info, exception);
      if (handled) {
        uint32_t state = ZX_EXCEPTION_STATE_HANDLED;
        status = zx_object_set_property(exception, ZX_PROP_EXCEPTION_STATE,
                                        &state, sizeof(state));
        // ... check status ...
      }

      // Close the exception to finish handling.
      zx_handle_close(exception);
    } else {
      // We got ZX_CHANNEL_PEER_CLOSED, the task has terminated.
      zx_handle_close(channel);
      return;
    }
  }
}

异常类型

概括来讲,存在两种类型的例外情况:架构和合成。 架构异常包括分段错误(例如,解引用 NULL 指针)或执行未定义的指令。合成异常包括 消息串开始/停止通知等 违规行为

架构和政策例外情况被视为致命,会导致 它们就会被终止仅限调试程序的异常 - 线程 启动/停止和流程启动 - 仅供参考,并会继续执行 正常情况下,即使线程未显式恢复也是如此。例外情况包括 旨在让调试程序有机会正确响应这些生命周期事件, 因为相应的线程将会暂停,直到异常恢复为止。

异常类型是在 <zircon/syscalls/exception.h>

例外情况渠道类型

异常渠道具有不同的特征,具体取决于任务类型和 ZX_EXCEPTION_CHANNEL_DEBUGGER 标志是否会传递给 zx_task_create_exception_channel()。下表总结了 各种渠道类型之间的区别:

频道类型 get_thread get_process 建筑与政策例外情况 线程启动/停止异常 进程启动异常
线程 X X
流程 X X X
进程调试程序 X X X X
作业 X X X
作业调试程序 X X X

渠道类型还决定了例外渠道的显示顺序 有机会处理异常:

  1. 进程调试程序
  2. 会话串
  3. 进程
  4. 进程调试程序(可选,如果异常为 'second-chance'
  5. 作业(父级作业 -> 祖父级作业 -> 等)

如果没有剩余的异常通道可供尝试,内核会终止 该过程就像调用了 zx_task_kill() 一样。进程的返回代码 由 ZX_TASK_RETCODE_EXCEPTION_KILL 异常终止,且可 使用 ZX_INFO_PROCESS 通过 zx_object_get_info() 获取。

每个任务仅支持每种类型有一个异常渠道,例如 给定一个进程并附加了调试异常通道,试图创建 第二个调试异常渠道将失败,但会创建一个非调试渠道 会成功。

ZX_EXCP_PROCESS_STARTING 和作业调试程序渠道

ZX_EXCP_PROCESS_STARTING 的行为与其他异常不同。时间是 仅发送到作业调试程序异常渠道,并始终发送到所有找到的 处理程序,基本上假定为 ZX_EXCEPTION_STATE_TRY_NEXT,而不考虑 实际处理程序行为这也是唯一一个由内核定义的异常, 作业调试程序通道接收,使它们成为检测 新进程作业调试程序也可能会收到 ZX_EXCP_USER 异常,这 可以使用 zx_thread_raise_exception() 系统调用引发。

由于作业调试程序通道被视为“只读”,因此多个作业调试程序 可在以下位置创建频道(最多 ZX_EXCEPTION_CHANNEL_JOB_DEBUGGER_MAX_COUNT 个) 单个作业。在一个作业中创建多个调试渠道时, ZX_EXCP_PROCESS_STARTING 事件将依序发送到所有渠道,其中 较晚创建的渠道先于后期创建的渠道收到通知。

用户定义的异常

zx_thread_raise_exception() 系统调用可用于引发用户定义的 异常。这些异常的类型为 ZX_EXCP_USER, 值在 synth_codesynth_data 字段中提供, zx_exception_context_t。目前,可以传递用户定义的异常。 仅限作业调试程序异常渠道。

先使用进程调试程序,稍后再试

在 Zircon 中,首先尝试进程调试程序异常通道。有用 至少出于以下几个原因:

  • 允许“修正并继续”调试,例如如果某个线程出现分段错误, 调试程序用户可以修复分段错误并恢复线程,而无需执行任何 非调试程序频道。
  • 确保调试程序断点直接发送到调试程序,而无需 其他处理程序必须显式传递它们。

如果异常已设置 ZX_EXCEPTION_STRATEGY_SECOND_CHANCE 并仍然存在 尝试进程异常通道后未处理,进程调试程序 将有第二次机会。它的有用之处在于 进程会监听自己的异常并使用该异常 以确保正常运行;在本示例中,它可以使用调试程序 并在更正失败时进行检查

与任务暂停的互动

异常和线程挂起是分开处理的。 换言之,线程可以同时处于异常中,也可以被挂起。 如果线程在等待响应时挂起,就会发生这种情况 从异常处理程序中获取。线程会保持暂停状态,直到恢复为止 :

zx_handle_close(exception);
zx_handle_close(suspend_token);

顺序无关紧要。

与终止任务的交互

zx_task_kill() 会停止对任务的任何异常处理。如果在调用 线程(或其父进程/作业)处于异常状态时:

  • 线程将停止等待当前的异常处理程序
  • 其他异常处理程序不会再收到该异常
  • zx_exception_get_thread()zx_exception_get_process()(位于 未完成的异常句柄将继续提供有效的任务句柄
  • 用于设置异常状态的 zx_object_set_property() 仍会返回 ZX_OK,不过状态不会产生任何影响,因为线程不再 在处理程序上执行阻塞操作

此外,已终止的线程仍会发送 ZX_EXCP_THREAD_EXITING 异常(如果已注册进程调试处理程序),但如上所述 等待处理程序的响应。

虽然 zx_task_kill() 通常是异步的,这意味着线程可以 在系统调用返回时还没有完成终止,它会同步执行 停止异常处理,以便在返回后关闭异常句柄 不会恢复线程或将异常传递给另一个处理程序。

信号

信号是观察状态变化的核心 Zircon 机制 内核对象(通道变为可读,进程终止,事件 变为有信号状态,等等)。

与异常不同,信号不需要异常处理程序的响应。 另一方面,信号将发送给正在等待线程 处理,而不是发送到可能会绑定到的异常渠道 线程的进程

Zircon 中的一种常见模式是让消息循环等待 一个或多个对象,并在它们传入时对其进行处理。纳入例外情况 处理转换为此模式,使用 zx_object_wait_async() 等待 ZX_CHANNEL_READABLE(以及可选的 ZX_CHANNEL_PEER_CLOSED) 例外渠道:

zx_handle_t port;
zx_status_t status = zx_port_create(0, &port);
// ... check status ...

// Start waiting on relevant signals on the exception channel.
status = zx_object_wait_async(exception_channel, port, kMyExceptionKey,
                              ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
// ... check status ...

// ... add other objects to |port| with wait_async() ...

while (1) {
  zx_port_packet_t packet;
  status = zx_port_wait(port, ZX_TIME_INFINITE, &packet);
  // ... check status ...

  if (packet.key == kMyExceptionKey) {
    if (packet.signal.observed & ZX_CHANNEL_READABLE) {
      // ... extract exception from |exception_channel| and process it ...

      // wait_async() is one-shot so we need to reload it to continue
      // receiving signals.
      status = zx_object_wait_async(
          exception_channel, port, kMyExceptionKey,
          ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
      // ... check status ...
    } else {
      // Got ZX_CHANNEL_PEER_CLOSED, task has terminated.
      zx_handle_close(exception_channel);
    }
  } else {
    // ... handle other objects added to |port| ...
  }
}

与 Posix(和 Linux)对比

下表显示了 针对异常和异常情况的 Zircon 和 Posix/Linux 处理程序通常都是如此。

Zircon Posix/Linux
异常/信号 信号
ZXEXCP* SIG*
zx_task_create_exception_channel() ptrace(ATTACH,DETACH)
zx_task_suspend() kill(SIGSTOP),ptrace(KILL(SIGSTOP))
zx_handle_close(suspend_token) kill(SIGCONT)、ptrace(CONT)
zx_handle_close(exception) kill(SIGCONT)、ptrace(CONT)
zx_task_kill() kill(SIGKILL)
zx_thread_raise_exception() 终止(SIGUSR1)
不适用 kill(everything_else)
待定 signal()/sigaction()
zx_port_wait() wait*()
各种 来自 sys/wait.h 的 W*() 宏
zx_exception_info_t siginfo_t
zx_exception_context_t siginfo_t
zx_thread_read_state() ptrace(GETREGS,GETREGSET)
zx_thread_write_state() ptrace(SETREGS,SETREGSET)
zx_process_read_memory() ptrace(PEEKTEXT)
zx_process_write_memory() ptrace(POKETEXT)

Zircon 没有 SIGINTSIGQUITSIGTERM 等非严重异步信号 SIGUSR1SIGUSR2 等。

与 Posix 的另一个显著区别是,在 Zircon 中,线程不能 因为 Zircon 异常处理是同步处理, 操作,而不是由 由内核执行

示例

如需查看使用异常的 Zircon 代码,您可以查看更多示例,包括:

  • src/bringup/bin/pwrbtn-monitor/crashsvc:系统级崩溃处理程序
  • system/utest/exception:异常单元测试
  • system/utest/debugger:与调试程序相关的功能单元测试

另请参阅