异常处理

简介

当线程遇到故障情况(例如分段错误)时,执行会暂停,并且线程会进入异常处理状态。已注册接收这些异常的处理程序会收到通知,并有机会检查或更正条件。

此功能通常由调试程序或崩溃记录器使用,它们希望在线程崩溃之前有机会与线程进行交互。对于只想跟踪任务生命周期而不需要拦截崩溃的应用,信号可能是更好的选择。

基础知识

通过使用 zx_task_create_exception_channel() 系统调用在任务(线程、进程或作业)上创建异常渠道,在用户空间中处理异常。创建的句柄是标准的 Zircon 通道,但创建为只读状态,因此只能用于接收异常消息。

发生异常时,线程会暂停,并且包含 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 和 Job Debugger 渠道

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 类型,用户定义的值在 zx_exception_context_tsynth_codesynth_data 字段中提供。目前,用户定义的异常只能传送到作业调试程序异常渠道。

先处理调试程序,以后再可能

在 Zircon 中,系统会先尝试进程调试程序异常通道。这至少具有几个原因:

  • 允许“修复并继续”调试,例如,如果线程出现分段错误,调试程序用户可以修复分段错误并恢复线程,而无需任何非调试程序通道看到异常。
  • 确保将调试程序断点直接发送到调试程序,而无需其他处理程序明确传递这些断点。

如果异常设置了 ZX_EXCEPTION_STRATEGY_SECOND_CHANCE,并且在尝试进程异常通道后仍未处理,进程调试程序异常通道将再获得一次机会。此功能的实用之处在于,进程监听自己的异常并使用该信息正常运行;在这种情况下,当更正失败时,它可以让调试程序进行检查。

与任务暂停交互

异常和线程挂起是单独处理的。换言之,线程可能既处于异常状态,又可能处于挂起状态。如果线程在等待异常处理程序的响应时挂起,就可能发生这种情况。线程会保持暂停状态,直到针对异常和挂起而恢复:

zx_handle_close(exception);
zx_handle_close(suspend_token);

顺序无关紧要。

与任务终止交互

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

此外,已终止的线程仍会发送 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 针对异常的等效术语、类型和函数调用,以及异常处理程序通常会执行的操作。

锆石 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() 终止(SIGKILL)
zx_thread_raise_exception() 终止(SIGUSR1)
不适用 kill(全部)
待定 signal()/sigaction()
zx_port_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 没有 SIGINTSIGQUITSIGTERMSIGUSR1SIGUSR2 等非严重异步信号。

与 Posix 的另一个显著区别是,在 Zircon 中,线程无法处理自己的异常,因为 Zircon 异常处理是由用户空间驱动的同步操作,而不是由内核调用的异步回调。

示例

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

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

另请参阅