Linux 内核入口/出口处理机制深度解析:syscall、中断、NMI 与 KVM 的状态转换体系

Linux 内核入口/出口处理机制深度解析:syscall、中断、NMI 与 KVM 的状态转换体系 Linux 内核入口/出口处理机制深度解析syscall、中断、NMI 与 KVM 的状态转换体系【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文围绕 Linux 内核文档 Documentation/core-api/entry.rst 展开系统剖析内核在 syscall、中断interrupt、NMI 及 KVM 虚拟化边界上必须执行的状态更新Lockdep、RCU/context tracking、抢占计数、tracing、时间统计及其严格顺序约束。读者读完后将能理解noinstr代码段的定位与instrumentation_begin()/end()的用法、四类转换路径的推荐实现模板含可直接对照的 C 代码以及irqentry_enter()/irqentry_nmi_enter()等核心接口在 kernel/entry/common.c 中的真实语义。为什么入口/出口代码是内核最敏感的地带内核代码会在多种执行域execution domain之间切换用户态 ↔ 内核态、普通内核上下文 ↔ 中断上下文、宿主机 ↔ KVM 客户机。每一次切换都需要对如下状态进行严格有序的更新Lockdep锁依赖与 IRQ 标志状态跟踪RCU / Context tracking读侧临界区与上下文追踪尤其在 NOHZ_FULL 下抢占计数器Preemption counter用于判定in_hardirq()、in_nmi()等上下文Tracingtrace_hardirqs_off/on等硬中断跟踪状态时间统计Time accounting中断/软中断的时间归属。这些更新的先后顺序因转换类型而异本仓库文档将其划分为四类Syscalls、KVM、Interrupts and regular exceptions、NMI and NMI-like exceptions。顺序一旦出错轻则 tracing/lockdep 状态失真重则导致 RCU 读侧临界区失效引发不可预期的崩溃。正因为顺序如此苛刻现代内核把这段代码收敛为统一的基础设施include/linux/entry-common.h、include/linux/irq-entry-common.h 与 kernel/entry/common.c。noinstr不可插桩的入口代码段大多数插桩instrumentation设施依赖 RCU 处于活动状态因此在 RCU 开始观察watching之前的入口代码、以及 RCU 停止观察之后的出口代码中插桩是被禁止的。此外很多架构在入口处需要保存/恢复寄存器状态——例如断点入口代码中的断点会覆写最初断点的调试寄存器。满足这类要求的代码必须用noinstr属性标记使其落入特殊的、插桩与调试设施不可达的 section。其定义位于 include/linux/compiler_types.h#define __noinstr_section(section) \ ... #define noinstr __noinstr_section(.noinstr.text)即所有noinstr函数被放置到.noinstr.textsection 中。文档给出了一部分可插桩、一部分不可插桩的典型函数结构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 }要点被noinstr函数调用的函数同样必须是noinstr或__always_inline内联后不产生独立的可插桩调用点instrumentation_begin()/instrumentation_end()之间的区间允许插桩反向的调用从可插桩上下文调用不可插桩函数没有限制常用于保护一旦被插桩就会出故障的状态切换在支持 objtool 的架构上这些约束可以被自动验证防止开发者误把可插桩代码混入入口路径所有位于 RCU 状态转换之前/之后的不可插桩入口/出口代码段必须在关中断interrupts disabled条件下运行。Syscalls推荐的两套入口模板与状态顺序最稳健的模板预置 -ENOSYSsyscall 入口代码起始于汇编在建立底层架构状态与栈帧后调用低级 C 代码该 C 代码必须不可插桩。文档推荐的 syscall 处理函数如下noinstr void syscall(struct pt_regs *regs, long nr) { arch_syscall_enter(regs); result_reg(regs) -ENOSYS; if (syscall_enter_from_user_mode_randomize_stack(regs, nr)) { instrumentation_begin(); if (valid(nr) result_reg(regs) invoke_syscall(regs, nr); instrumentation_end(); } syscall_exit_to_user_mode(regs); }该变体始终保证返回码合法先无条件写-ENOSYS再仅在系统调用号有效时覆盖结果因此最具韧性。替代模板及其缺陷noinstr void syscall(struct pt_regs *regs, long nr) { arch_syscall_enter(regs); if (syscall_enter_from_user_mode_randomize_stack(regs, nr)) { instrumentation_begin(); if (valid(nr) result_reg(regs) invoke_syscall(regs, nr); else result_reg(regs) -ENOSYS; instrumentation_end(); } syscall_exit_to_user_mode(regs); }此变体在大多数场景可用但存在一个已知陷阱如果挂在 syscall tracepoint 上的 probe/BPF 程序把系统调用号改成非法值如 -1并同时修改了结果寄存器那么else分支会用 -ENOSYS 覆写被修改过的结果。这正是第一套模板始终预置返回码的原因。状态建立与拆除的精确顺序syscall_enter_from_user_mode_randomize_stack()首先调用enter_from_user_mode_randomize_stack()按以下顺序建立状态对应 include/linux/entry-common.h 中的宏定义LockdepRCU / Context trackingTracing应用栈随机化stack randomization随后才调用 ptrace、seccomp、audit、syscall tracing 等各类 entry work 函数这些标志位集合见 include/linux/entry-common.h 的SYSCALL_WORK_ENTER。上述工作完成后才能调用可插桩的invoke_syscall可插桩区间结束后调用syscall_exit_to_user_mode()。syscall_exit_to_user_mode()负责返回用户态前的一切工作tracing、audit、信号、task work 等对应SYSCALL_WORK_EXIT标志集见 include/linux/entry-common.h最后调用exit_to_user_mode()以逆序拆除状态TracingRCU / Context trackingLockdepexit_to_user_mode()的完整实现位于 include/linux/irq-entry-common.h先 trace hardirqs on、lockdep prepare再user_enter_irqoff()通知 RCU最后arch_exit_to_user_mode()供架构做投机缓解等收尾与lockdep_hardirqs_on()。若架构需要在各步骤之间插入额外工作可以使用syscall_enter_from_user_mode_randomize_stack()/syscall_exit_to_user_mode()的细粒度子函数但必须保证入口先调enter_from_user_mode_randomize_stack()出口最后调exit_to_user_mode()。⚠️ 文档明确警告不要嵌套 syscall。嵌套会触发 RCU 和/或 context tracking 的警告输出。x86 上的落地实例x86-64 的do_syscall_64()arch/x86/entry/syscall_64.c正是上述模板的忠实实现/* Returns true to return using SYSRET, or false to use IRET */ __visible noinstr bool do_syscall_64(struct pt_regs *regs, long nr) { if (likely(syscall_enter_from_user_mode_randomize_stack(regs, nr))) { instrumentation_begin(); if (!do_syscall_x64(regs, nr)) do_syscall_x32(regs, nr); instrumentation_end(); } syscall_exit_to_user_mode(regs); ... }x86 32 位兼容入口do_int80_syscall_32()arch/x86/entry/syscall_32.c同样遵循此结构可见该模板已是各架构的通用范式。KVM客户机进出等价于用户态/内核态转换从宿主机内核视角看进入 guest 等价于 CPU跑到用户态退出 guest 则等价于回到内核。因此 KVM 的边界转换与 syscall 高度相似guest_state_enter_irqoff()是 KVM 版的exit_to_user_mode()guest_state_exit_irqoff()是 KVM 版的enter_from_user_mode()状态操作采用相同的顺序。其实现位于 include/linux/kvm_host.h。由于进入 guest 后宿主机 RCU、tracing、lockdep 等状态都必须被挂起并在退出时精确恢复这两组函数与 syscall 路径共用同一套状态机只是封装层面不同。Task work 的处理被单独放在vcpu_run()循环边界通过xfer_to_guest_mode_handle_work()完成它是返回用户态时所做 work 的一个子集。相关接口声明在 include/linux/entry-virt.hKVM 侧封装见 include/linux/kvm_host.h。⚠️ 文档强调不要嵌套 KVM 进出转换因为这样做毫无意义且会破坏状态一致性。中断与普通异常比 syscall 更复杂的双路径用户态被中断与 syscall 完全一致如果中断发生在用户态执行期间其进出处理与 syscall 完全一致。内核态被中断条件性 RCU 更新如果中断发生在内核态处理略有不同只有当中断落在 CPU 的 idle 任务上下文时才更新 RCU 状态否则 RCU 必然已在观察Lockdep 与 tracing 则无条件更新。这一逻辑由irqentry_enter()/irqentry_exit()实现其内核态分支的细节见 include/linux/irq-entry-common.h 的irqentry_enter_from_kernel_mode()若命中 idle 任务或arch_in_rcu_eqs()报告处于 RCU 扩展静止态无条件调用ct_irq_enter()并置ret.exit_rcu true——注释解释了为何不能用rcu_is_watching()判断嵌套中断如软中断重新使能中断后的再次中断中的 tick 会误判为第一个中断而过早结束宽限期否则只需rcu_irq_enter_check_tick()检查是否需要重启 NOHZ tick。架构相关的处理与 syscall 模板相似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() 与 irq_exit_rcu() 的分工irq_enter_rcu()更新抢占计数使in_hardirq()返回 true、处理 NOHZ tick 状态与中断时间统计。这意味着直到调用irq_enter_rcu()之前in_hardirq()都返回 falseirq_exit_rcu()处理中断时间统计、撤销抢占计数更新并最终处理软中断与 NOHZ tick 状态。理论上抢占计数可以在irqentry_enter()中就更新但实际推迟到irq_enter_rcu()有两个好处其一让抢占计数相关代码可以被 trace其二与irq_exit_rcu()、irqentry_exit()保持对称。代价是早期入口代码必须意识到抢占计数尚未叠加HARDIRQ_OFFSET。另外两个关键约束irq_exit_rcu()必须在处理软中断之前从抢占计数中移除HARDIRQ_OFFSET因为软中断处理函数运行在 BH 上下文而非关中断上下文irqentry_exit()可能会发生调度irqentry_exit_to_kernel_mode_preempt()在regs_irqs_disabled(regs)为 false 且开启抢占时会调用irqentry_exit_cond_resched()见 include/linux/irq-entry-common.h这同样要求HARDIRQ_OFFSET已被移除。嵌套与重入尽管中断处理函数期望在关中断下运行但从进出视角看中断嵌套很常见例如软中断处理发生在irqentry_{enter,exit}()块内且本地中断是使能的此外虽然罕见中断处理函数本身也可以重新使能中断。中断进出代码本身因运行在关中断环境而不必严格处理重入但NMI 随时可能发生且大量入口代码在两者之间共享——这正是下一节的主题。NMI 与 NMI 类异常可重入的状态机NMI 及 NMI 类异常machine check、double fault、debug 中断等可能命中任意上下文必须对状态格外小心。一个关键区分debug 异常与 machine-check 异常的状态变化取决于它们发生在用户态断点/观察点还是内核态代码补丁来自用户态 → 按普通中断处理来自内核态 → 按NMI处理而 NMI 本身及其他 NMI 类异常不做用户态/内核态来源的区分。irqentry_nmi_enter() 的进入顺序irqentry_nmi_enter()实现在 kernel/entry/common.c按如下顺序更新状态抢占计数器__nmi_enter()Lockdeplockdep_hardirqs_off()lockdep_hardirq_enter()RCU / Context trackingct_nmi_enter()Tracingtrace_hardirqs_off_finish()ftrace_nmi_enter()退出时irqentry_nmi_exit()kernel/entry/common.c做完全逆序的撤销ftrace_nmi_exit()→ 恢复 lockdep 使能态 →ct_nmi_exit()→lockdep_hardirq_exit()→__nmi_exit()。为什么抢占计数必须最先改、最后撤抢占计数的更新必须是进入时的第一个操作、退出时的最后一个操作因为 lockdep 和 RCU 都依赖in_nmi()在此场景下返回 true。因此 NMI 进出时的抢占计数修改绝不能被 trace这也是整个 NMI 进出代码都用noinstr标记、并把可 trace 部分包在instrumentation_begin()/end()内的原因。架构代码模板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); }debug 异常的模板则展示了按来源分流的完整写法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 类异常。因此NMI 入口代码必须是可重入的reentrant状态更新需要处理嵌套。这也是 NMI 路径相比普通中断路径多出大量保存/恢复现场工作的根本原因。x86 侧可对照 arch/x86/entry/entry_fred.c 中fred_entry_from_user()、fred_entry_from_kernel()等 FRED 新入口的实现观察它们如何对用户态/内核态来源分别调用irqentry_enter()/irqentry_nmi_enter()体系。总结一张顺序对照表转换类型入口状态顺序出口状态顺序逆序核心接口SyscallLockdep → RCU/CT → Tracing → 栈随机化Tracing → RCU/CT → Lockdepsyscall_enter_from_user_mode_randomize_stack()/syscall_exit_to_user_mode()KVM同 syscallguest 退出视角同 syscallguest 进入视角guest_state_exit_irqoff()/guest_state_enter_irqoff()task work 走xfer_to_guest_mode_handle_work()中断用户态来源同 syscall同 syscallirqentry_enter()/irqentry_exit()中断内核态来源Lockdep →条件性RCU/CT → Tracing抢占计数由irq_enter_rcu()延迟更新逆序irq_exit_rcu()先移除HARDIRQ_OFFSETirqentry_enter()/irqentry_exit()irq_enter_rcu()/irq_exit_rcu()NMI / NMI 类异常抢占计数 → Lockdep → RCU/CT → Tracing逆序抢占计数最后撤irqentry_nmi_enter()/irqentry_nmi_exit()这套机制的内核通用实现集中在三个文件可作为进一步深入阅读的入口include/linux/entry-common.hsyscall 进出、SYSCALL_WORK_*标志与 work 分发include/linux/irq-entry-common.henter_from_user_mode()、exit_to_user_mode()、irqentry_state_t及 irqentry 系列内联函数kernel/entry/common.cirqentry_enter()、irqentry_exit()、irqentry_nmi_enter()、irqentry_nmi_exit()与退出循环exit_to_user_mode_loop()的实际实现。理解这些入口路径的顺序约束是阅读任何架构x86、arm64 等入口汇编与低级 C 代码、排查 RCU/lockdep/tracing 异常告警、以及评估新异常类型接入方式的前提。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考