异常、中断、系统调用和 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 在内核空间执行时发生中断,则进入和退出处理略有不同。RCU 状态仅在中断在 CPU 空闲任务的上下文发生时更新。否则,RCU 将已在监视。Lockdep 和跟踪必须无条件更新。

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。

尽管中断处理程序预期在本地中断禁用状态下运行,但从进入/退出角度来看,中断嵌套很常见。例如,softirq 处理发生在 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 入口代码必须是可重入的,并且状态更新需要处理嵌套。