异常、中断、系统调用和 KVM 的入口/退出处理

执行域之间的所有转换都需要状态更新,这些更新受到严格的排序约束。以下情况需要状态更新

  • 锁依赖 (Lockdep)

  • RCU / 上下文跟踪

  • 抢占计数器

  • 跟踪

  • 时间统计

更新顺序取决于转换类型,并在以下转换类型部分中进行解释:系统调用KVM中断和常规异常NMI 和类似 NMI 的异常

不可检测的代码 - noinstr

大多数检测工具依赖于 RCU,因此在 RCU 开始监视之前的入口代码和 RCU 停止监视之后的退出代码禁止进行检测。此外,许多架构必须保存和恢复寄存器状态,这意味着(例如)在断点入口代码中的断点将覆盖初始断点的调试寄存器。

此类代码必须使用“noinstr”属性进行标记,将该代码放置在特殊的部分中,该部分不可用于检测和调试工具。某些函数是部分可检测的,这通过将其标记为 noinstr 并使用 instrumentation_begin() 和 instrumentation_end() 来标记代码的可检测范围来处理

noinstr void entry(void)
{
      handle_entry();     // <-- must be 'noinstr' or '__always_inline'
      ...

      instrumentation_begin();
      handle_context();   // <-- instrumentable code
      instrumentation_end();

      ...
      handle_exit();      // <-- must be 'noinstr' or '__always_inline'
}

这允许在受支持的架构上通过 objtool 验证“noinstr”限制。

从可检测的上下文调用不可检测的函数没有限制,并且对于保护例如状态切换非常有用,如果进行检测则会导致故障。

在 RCU 状态转换之前和之后的所有不可检测的入口/退出代码部分都必须在禁用中断的情况下运行。

系统调用

系统调用入口代码从汇编代码开始,在建立底层特定于架构的状态和堆栈帧后,调用到底层 C 代码。此底层 C 代码不得进行检测。从底层汇编代码调用的典型系统调用处理函数如下所示

noinstr void syscall(struct pt_regs *regs, int nr)
{
      arch_syscall_enter(regs);
      nr = syscall_enter_from_user_mode(regs, nr);

      instrumentation_begin();
      if (!invoke_syscall(regs, nr) && nr != -1)
              result_reg(regs) = __sys_ni_syscall(regs);
      instrumentation_end();

      syscall_exit_to_user_mode(regs);
}

syscall_enter_from_user_mode() 首先调用 enter_from_user_mode(),后者按以下顺序建立状态

  • 锁依赖 (Lockdep)

  • RCU / 上下文跟踪

  • 跟踪

然后调用各种入口工作函数,如 ptrace、seccomp、audit、系统调用跟踪等。完成所有这些操作后,可以调用可检测的 invoke_syscall 函数。可检测的代码段随后结束,之后调用 syscall_exit_to_user_mode()。

syscall_exit_to_user_mode() 处理返回到用户空间之前需要完成的所有工作,如跟踪、审计、信号、任务工作等。之后,它调用 exit_to_user_mode(),后者再次按相反顺序处理状态转换

  • 跟踪

  • RCU / 上下文跟踪

  • 锁依赖 (Lockdep)

syscall_enter_from_user_mode() 和 syscall_exit_to_user_mode() 也可用作细粒度子函数,以便架构代码在各个步骤之间进行额外工作。在这种情况下,它必须确保在入口时首先调用 enter_from_user_mode(),并在退出时最后调用 exit_to_user_mode()。

不要嵌套系统调用。嵌套的系统调用将导致 RCU 和/或上下文跟踪打印警告。

KVM

进入或退出访客模式与系统调用非常相似。从宿主内核的角度来看,CPU 在进入访客时进入用户空间,并在退出时返回到内核。

kvm_guest_enter_irqoff() 是 exit_to_user_mode() 的 KVM 特定变体,kvm_guest_exit_irqoff() 是 enter_from_user_mode() 的 KVM 变体。状态操作具有相同的顺序。

任务工作处理在 vcpu_run() 循环的边界处通过 xfer_to_guest_mode_handle_work() 为访客单独完成,这是返回用户空间时处理的工作的子集。

不要嵌套 KVM 进入/退出转换,因为这样做是没有意义的。

中断和常规异常

中断进入和退出处理比系统调用和 KVM 转换稍微复杂一些。

如果 CPU 在用户空间中执行时引发中断,则进入和退出处理与系统调用完全相同。

如果 CPU 在内核空间中执行时引发中断,则进入和退出处理略有不同。仅当在 CPU 空闲任务的上下文中引发中断时,才会更新 RCU 状态。否则,RCU 将已经处于监视状态。锁依赖和跟踪必须无条件更新。

irqentry_enter() 和 irqentry_exit() 提供此实现。

特定于架构的部分看起来类似于系统调用处理

noinstr void interrupt(struct pt_regs *regs, int nr)
{
      arch_interrupt_enter(regs);
      state = irqentry_enter(regs);

      instrumentation_begin();

      irq_enter_rcu();
      invoke_irq_handler(regs, nr);
      irq_exit_rcu();

      instrumentation_end();

      irqentry_exit(regs, state);
}

请注意,实际中断处理程序的调用是在 irq_enter_rcu() 和 irq_exit_rcu() 对中进行的。

irq_enter_rcu() 更新抢占计数,使 in_hardirq() 返回 true,处理 NOHZ 时钟状态和中断时间统计。这意味着直到调用 irq_enter_rcu() 的点,in_hardirq() 都返回 false。

irq_exit_rcu() 处理中断时间统计,撤消抢占计数更新,并最终处理软中断和 NOHZ 时钟状态。

理论上,可以在 irqentry_enter() 中更新抢占计数。实际上,将此更新推迟到 irq_enter_rcu() 允许跟踪抢占计数代码,同时保持与 irq_exit_rcu() 和 irqentry_exit() 的对称性,这将在下一段中进行描述。唯一的缺点是,早期的入口代码直到 irq_enter_rcu() 必须知道尚未使用 HARDIRQ_OFFSET 状态更新抢占计数。

请注意,irq_exit_rcu() 必须在处理软中断之前从抢占计数中删除 HARDIRQ_OFFSET,软中断的处理程序必须在 BH 上下文而不是禁用中断的上下文中运行。此外,irqentry_exit() 可能会进行调度,这也要求从抢占计数中删除 HARDIRQ_OFFSET。

尽管中断处理程序应该在禁用本地中断的情况下运行,但从入口/退出的角度来看,中断嵌套很常见。例如,软中断处理发生在启用本地中断的 irqentry_{enter,exit}() 块内。此外,尽管不常见,但没有什么能阻止中断处理程序重新启用中断。

中断入口/退出代码并不严格需要处理重入,因为它是在禁用本地中断的情况下运行的。但是 NMI 可以随时发生,并且很多入口代码在两者之间共享。

NMI 和类似 NMI 的异常

NMI 和类似 NMI 的异常(机器检查、双重故障、调试中断等)可以命中任何上下文,并且必须格外小心状态。

调试异常和机器检查异常的状态更改取决于这些异常发生在用户空间(断点或监视点)还是在内核模式(代码修补)。从用户空间,它们被视为中断,而从内核模式,它们被视为 NMI。

NMI 和其他类似 NMI 的异常处理状态转换,而不区分用户模式和内核模式的来源。

入口时的状态更新在 irqentry_nmi_enter() 中处理,它按以下顺序更新状态

  • 抢占计数器

  • 锁依赖 (Lockdep)

  • RCU / 上下文跟踪

  • 跟踪

退出时的对应项 irqentry_nmi_exit() 以相反的顺序执行相反的操作。

请注意,抢占计数器的更新必须是入口时的第一个操作和退出时的最后一个操作。原因是 lockdep 和 RCU 都依赖于 in_nmi() 在此情况下返回 true。NMI 入口/退出情况下的抢占计数修改不得进行跟踪。

特定于架构的代码如下所示

noinstr void nmi(struct pt_regs *regs)
{
      arch_nmi_enter(regs);
      state = irqentry_nmi_enter(regs);

      instrumentation_begin();
      nmi_handler(regs);
      instrumentation_end();

      irqentry_nmi_exit(regs);
}

例如,调试异常可以如下所示

noinstr void debug(struct pt_regs *regs)
{
      arch_nmi_enter(regs);

      debug_regs = save_debug_regs();

      if (user_mode(regs)) {
              state = irqentry_enter(regs);

              instrumentation_begin();
              user_mode_debug_handler(regs, debug_regs);
              instrumentation_end();

              irqentry_exit(regs, state);
      } else {
              state = irqentry_nmi_enter(regs);

              instrumentation_begin();
              kernel_mode_debug_handler(regs, debug_regs);
              instrumentation_end();

              irqentry_nmi_exit(regs, state);
      }
}

没有可用的组合 irqentry_nmi_if_kernel() 函数,因为上述情况无法以不区分异常的方式处理。

NMI 可以发生在任何上下文中。例如,在处理 NMI 时触发的类似 NMI 的异常。因此,NMI 入口代码必须是可重入的,并且状态更新需要处理嵌套。