内核线程信号

关于

本文档介绍了线程信号,这是一种 Zircon 内核机制,用于 实现线程挂起和终止操作。线程信号不相关 对象信号传输

目标受众群体是内核开发者和任何有兴趣了解相关内容的人 挂起和终止操作在内核中的工作原理。

暂停和终止请求

挂起和终止是可以在线程上执行的操作。以上两者皆可 操作是异步进行的,也就是说,调用方必须等待 。在内核内部,这些操作以实例的形式实现 Thread 结构体上的方法:

Thread::Suspend - 请求线程挂起其执行,直到 而是通过 Thread::Resume 恢复的。挂起用于实现 调试程序。线程挂起后,其寄存器状态 读取/写入。用户可以看到此操作 通过 zx_task_suspend() 进入模式。

Thread::Kill - 请求线程自行终止。这个 操作不会直接公开给用户模式。也就是说,尝试 zx_task_kill(),则表示线程出现错误。不过,此操作 通过进程销毁间接暴露,无论是自愿的,还是出于自愿的 非自愿的。

请注意,这两项操作都被描述为请求。来电者是 请求目标挂起,或在终止时终止其 执行。调用方无法强行暂停或终止 目标。虽然目标对象无法拒绝请求,但可以将操作推迟到 适当的时间和地点这是设计的一个关键元素。

要了解为什么这些操作是请求,请考虑使用 强制终止或挂起线程如果某个线程在 则没有机会释放资源(如互斥量) 避免其被销毁最终可能会出现内存泄漏 锁定的锁、损坏的数据结构,以及各种不良内容。

将终止和挂起建模为只能由 目标线程,我们为目标提供一种方法来释放其资源并执行 在停止执行之前,临时(针对 挂起)或永久(如果终止)。

安全要点

在介绍如何发出终止请求和挂起请求之前,我们先介绍 线程终止的安全性

线程挂起或终止总是在一个位置 执行过程中,“边缘”从内核返回之前 返回用户模式。在返回到用户模式之前,线程会展开 调用堆栈,执行任何 RAII 对象的析构函数。等到 就要返回到用户模式了 都位于内核堆栈上此时,线程可以安全地挂起或终止 其执行情况。

具体而言,线程可以在两个安全时间点挂起或 终止。他们刚刚从系统调用返回到用户模式, 在从异常/故障/中断处理程序返回用户模式之前 (简称异常处理程序)。

请注意,异常处理程序不仅会在用户模式下执行时调用,他们 在内核模式下执行时也可以调用。当返回到 系统无法安全地挂起或终止,因为外部内核模式 上下文可能仍然持有资源。换言之,异常处理程序 只有在从用户模式上下文中触发时,才是安全的。

发送信号

我们知道,终止和挂起只是请求,由 目标线程,以决定何时以及如何处理请求。我们还知道 线程挂起或终止自身的唯一安全位置是在 在返回到用户模式之前,位于内核边缘。线程信号如何 融入其中?

线程信号是请求挂起和终止的机制。每个 Thread 对象有一个字段,其中包含一组断言信号。有一点 针对挂起,THREAD_SIGNAL_SUSPEND,针对终止使用 THREAD_SIGNAL_KILL

请求挂起或终止线程的实现方式是,设置 相应的位,然后根据目标的 以某种方式对其进行戳戳,以确保其能及时到达安全点 。poke 的确切类型取决于目标线程的状态: 睡眠/被阻、暂停或运行。请注意, 休眠/阻塞、可中断和不可中断。我们将重点介绍 “可中断”和“忽略不可中断”。

休眠或已屏蔽

如果目标线程正在休眠或阻塞,那么按照定义,它并未运行, 但它在内核中。由于只有正在运行的线程可以检查其信号 必须唤醒或取消屏蔽设备。当某个线程被解除阻止或被唤醒时,该线程会获得 zx_status_t。该值通常为 ZX_OKZX_ERR_TIMED_OUT。不过, 像这样提前唤醒线程时,我们使用特殊的 zx_status_t 值, ZX_ERR_INTERNAL_INTR_KILLED(如果是终止操作)和 ZX_ERR_INTERNAL_INTR_RETRY(如果是挂起操作)。

当线程被唤醒/解除阻塞时,它将看到 zx_status_t 结果并开始 退出内核,展开其堆栈。一般来说,任何核函数 返回两个特殊值中的一个会导致其调用方立即 返回,传播该值。

最后,当堆栈展开时,线程将位于边缘,一个安全的 。在返回用户模式之前,线程会检查 再次调用其信号,并调用 arch_iframe_process_pending_signals()x86_syscall_process_pending_signals()

已暂停

与休眠/阻塞情况一样,线程必须恢复执行, 导致其遭到终止如果线程终止, ZX_ERR_INTERNAL_INTR_KILLED,然后放松到即将返回用户之前 它作用于信号

正在运行

目标线程可能正在运行用户代码或内核代码。如果正在运行 那么我们需要强制它进入内核 其 Thread 结构体的信号字段。如果它运行的是内核代码 我们必须相信它能在合理的时间范围内检查待处理的信号。

发送方无法得知目标是处于内核模式还是用户模式,因此其行为方式 都是一样的发送者发送处理器间中断 (IPI), 目标当前运行的 CPU。中断的一部分 处理程序任务是检查并视情况处理待处理信号。

如果处理程序是在用户环境中调用(即 CPU 处于用户模式) 那么暂停/终止以及 处理程序将调用 arch_iframe_process_pending_signals()

不过,如果在内核环境中调用处理程序,则处理程序将 什么也不做,因为它不知道在它所在的那一时刻线程的状态 中断。在此处暂停/终止并不安全。处理程序 将返回到调用它的内核上下文,并依赖于此 以便最终注意到信号并到达安全点。

您可能想知道 IPI 是否真的有必要。在两种情况下 这一点至关重要第一个是目标线程在用户模式下运行, 只是不会自行进入内核。在没有 线程可能无法长时间进入内核 还是会发生无限循环在这种情况下,我们需要 IPI 以确保目标线程观察并处理 。第二个是目标线程正在执行长时间的 运行中,但不检查待处理信号。这些 很少见,但确实存在。最好的例子是执行来宾操作系统 通过zx_vcpu_enter()。此中断会导致将 VMEXIT 返回给主机 内核中,它可以检查是否有待处理的信号和展开。

融会贯通

我们来看一个示例,看看这背后的原理。假设线程 A 挂起线程 B,因为 B 正在执行 zx_port_wait()。取决于 执行操作的确切时间, 不同场景。我们将简单研究每种情形。

场景 1:在系统调用前暂停,在用户模式下运行

线程 A 在线程 B 即将开始其 zx_port_wait() 之前发出挂起 系统调用。线程 B 仍处于用户模式且正在运行。线程 A 设置线程 B 的 THREAD_SIGNAL_SUSPEND 位,并向线程 B 的当前 CPU 发出一个 IPI。 线程 B 的 CPU 接受中断并调用中断处理程序。就在之前 返回到用户模式,线程 B 会检查其待处理信号。看到了 设置 THREAD_SIGNAL_SUSPEND 后,它会自行挂起。这是线程的草图 B 的调用堆栈:

suspend_self()
interrupt_handler()
---- interrupt ----
user code

稍后,在恢复线程 B 后,线程 B 将返回到用户模式,就好像 什么都没发生。

场景 2:在系统调用期间挂起,然后阻塞

在线程 B 进入内核以执行挂起后,线程 A 执行 zx_port_wait() 系统调用。线程 B 正在执行内核 尚未被屏蔽。与场景 1 一样,会话 A 发出一个 IPI,这会使线程 B 检查是否有待处理的信号:

interrupt_handler()
---- interrupt ----
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

但是,这一次中断处理程序看到它在内核中被调用 上下文而不是用户上下文,这样它就不会自行挂起。取而代之的是, 并返回到调用该函数的内核上下文。线程 B 覆盖 zx_port_wait() 操作的核心,如果遇到以下情况,应用将阻塞 没有可用的数据包。线程 B 发现没有可用的数据包 并准备阻止:

WaitQueue::BlockEtcPreamble()
WaitQueue::BlockEtc()
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

在屏蔽之前,它会检查是否有待处理的信号, 请求暂停。它不会阻塞,而是会返回 ZX_ERR_INTERNAL_INTR_RETRY 并且调用堆栈会在返回到用户模式之前展开到边缘:

WaitQueue::BlockEtcPreabmle()   ZX_ERR_INTERNAL_INTR_RETRY
WaitQueue::BlockEtc()                       |
PortDispatcher::Dequeue()                   |
sys_port_wait()                             |
syscall_dispatch()                          V
---- syscall ----
vdso
zx_port_wait()
user code

此时,线程会检查待处理信号并自行挂起。被动 恢复后,线程会返回用户模式(返回 vDSO),并显示状态结果 ZX_ERR_INTERNAL_INTR_RETRY。vDSO 具有用于处理操作的特殊逻辑 系统调用返回 ZX_ERR_INTERNAL_INTR_RETRY,它只会重新发出 带有原始参数的系统调用:

suspend_self()                  ZX_ERR_INTERNAL_INTR_RETRY
syscall_dispatch()                                   |
---- syscall ----                                    |      A
vdso                                                 |______|
zx_port_wait()
user code

场景 3:在内核中处于阻塞状态时挂起

在线程 B 进入内核并阻止后,线程 A 发出挂起 正在等待出现端口数据包线程 A 发现线程 B 被阻塞 使用值 ZX_ERR_INTERNAL_INTR_RETRY 取消屏蔽线程 B。从现在开始 与场景 2 中的行为一致。调用将返回到用户模式,在该模式下, 然后由 vDSO 重试:

blocked                           ZX_ERR_INTERNAL_INTR_RETRY
WaitQueue::BlockEtcPostamble()                         |
WaitQueue::BlockEtc()                                  |
PortDispatcher::Dequeue()                              |
sys_port_wait()                                        |
syscall_dispatch()                                     |
---- syscall ----                                      |      A
vdso                                                   |______|
zx_port_wait()
user code

场景 4:取消屏蔽后、从内核返回之前暂停

当线程 B 被阻塞时,正在等待端口数据包,但有数据包到达, 取消屏蔽(使用 ZX_OK):

blocked                            ZX_OK
WaitQueue::BlockEtcPostamble()       |
WaitQueue::BlockEtc()                |
PortDispatcher::Dequeue()            V
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

现在,当线程 A 发出挂起时,线程 B 将展开到用户模式。 线程 A 设置位,可以看到线程 B 标记为正在运行,因此它发送 IPI。类似于“在系统调用前暂停”在这种情况下,中断处理程序 执行:

interrupt_handler()
---- interrupt ----
PortDispatcher::Dequeue()
sys_port_wait()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

不过,这次它不会检查待处理信号,因为 中断的内核上下文,而不是用户上下文。处理程序完成 线程 B 继续展开。最后,线程 B 到达边缘 即将从系统调用返回到用户模式。在这里,它会检查待处理 信号,看到 THREAD_SIGNAL_SUSPEND 并挂起:

suspend_self()
syscall_dispatch()
---- syscall ----
vdso
zx_port_wait()
user code

恢复后,它将返回到用户模式,并显示 已取消屏蔽 (ZX_OK):

syscall_dispatch()    ZX_OK
---- syscall ----       |
vdso                    V
zx_port_wait()
user code

回顾

要点总结如下:

  1. 您无法强制挂起或终止线程。您只能要求其暂停 或终止自身。

  2. 线程信号是请求线程挂起或终止的机制。

  3. 线程应仅在特定时间点挂起或终止其执行 内核中。具体来说,线程只能挂起或终止 当它没有任何资源(例如锁)并且即将从 从内核模式切换到用户模式

  4. 为了保持快速响应,长时间运行的内核操作必须 定期检查待处理的信号,如有设置,则返回。