異常、中斷、系統呼叫和 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 入口程式碼必須是可重入的,並且狀態更新需要處理巢狀。