简介
当线程遇到故障情况(例如分段错误)时,执行会暂停,并且线程会进入异常处理状态。已注册接收这些异常的处理程序会收到通知,并有机会检查或更正条件。
此功能通常由调试程序或崩溃记录器使用,它们希望在线程崩溃之前有机会与线程进行交互。对于只想跟踪任务生命周期而不需要拦截崩溃的应用,信号可能是更好的选择。
基础知识
通过使用 zx_task_create_exception_channel()
系统调用在任务(线程、进程或作业)上创建异常渠道,在用户空间中处理异常。创建的句柄是标准的 Zircon 通道,但创建为只读状态,因此只能用于接收异常消息。
发生异常时,线程会暂停,并且包含 zx_exception_info_t
和异常句柄的消息会发送到相应通道。异常的生命周期绑定到此异常句柄的生命周期,因此当接收器完成处理后,关闭此异常句柄将恢复异常。此异常句柄是不可复制的,这意味着在任何给定时间,此异常只有一个处理程序。
默认情况下,关闭异常句柄会使线程保持暂停状态,并将异常发送到下一个处理程序。如果接收器已更正异常并希望线程继续执行,则可以在关闭之前通过 zx_object_set_property()
将异常状态更改为 ZX_EXCEPTION_STATE_HANDLED
。
异常句柄
异常句柄的行为与挂起令牌类似,它会使线程保持暂停状态,直到它们关闭。此外,异常句柄还有一些函数来帮助接收器处理异常:
zx_object_set_property()
与ZX_PROP_EXCEPTION_STATE
,用于设置句柄关闭时的行为zx_exception_get_thread()
,用于获取异常线程的句柄zx_exception_get_process()
,用于获取异常进程的句柄(仅限进程或作业异常渠道)
从异常中检索的任务句柄的权限与最初传入 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 |
渠道类型还决定了异常渠道处理异常的顺序:
- 进程调试程序
- 会话串
- 进程
- 进程调试程序(可选,如果异常为
'second-chance'
) - 工作(父工作 -> 祖父工作 -> 等)
如果没有剩余的异常通道可供尝试,内核会终止相应进程,就像调用 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_t
的 synth_code
和 synth_data
字段中提供。目前,用户定义的异常只能传送到作业调试程序异常渠道。
先处理调试程序,以后再可能
在 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 针对异常的等效术语、类型和函数调用,以及异常处理程序通常会执行的操作。
锆石 | 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 没有 SIGINT
、SIGQUIT
、SIGTERM
、SIGUSR1
、SIGUSR2
等非严重异步信号。
与 Posix 的另一个显著区别是,在 Zircon 中,线程无法处理自己的异常,因为 Zircon 异常处理是由用户空间驱动的同步操作,而不是由内核调用的异步回调。
示例
如需查看使用异常的 Zircon 代码,您可以查看更多示例,包括:
src/bringup/bin/pwrbtn-monitor/crashsvc
:系统级崩溃处理程序system/utest/exception
:异常单元测试system/utest/debugger
:与调试程序相关的功能单元测试
另请参阅
zx_task_create_exception_channel()
zx_exception_get_thread()
zx_exception_get_process()
zx_object_set_property()
zx_port_wait()