如果在为用户进程提供服务(如系统调用或页面调入)的上下文中或在进程中发生内核中的 CPU 异常(如错误的内存访问或无效的操作码),则在设置kthread
之前,panic_on_oops
有用的信息将被转储,任务将终止。无需惊慌。有时系统仍然完全可用。足以让用户尝试同步磁盘、正常终止程序,并以其他方式为紧急重启做准备。
但不幸的是,如果异常发生在原子上下文中(如中断或软中断),则采取的操作始终是内核恐慌(带有描述"Fatal exception in interrupt"
)——无论任何设置或构建时配置如何。这很可悲。为什么不能模拟中断返回,并保持系统运行,希望某些部分仍能正常工作?谢谢。
我知道我可以mdelay()
在“中断异常”的代码路径中放置一个无限,而不是panic()
,这样只会让本地 CPU 停滞。但通常在发生这种情况后就没有什么可以做的事情了……即使机器中有数百个 CPU,它们很快就会全部锁定。所以不是很有用。
我将以通用方式描述 CPU 行为。这解释了一般原则,但细节和术语在不同的 CPU 架构之间可能会有很大差异。
CPU 运行代码时,可能会发生某些不好的事情,导致代码无法正常执行,比如尝试执行无效的指令操作码或尝试访问未映射的内存地址。我将这些称为故障。发生故障时,CPU 会调用故障处理程序。这意味着 CPU 会保存某些寄存器的内容并将这些寄存器设置为新值。具体而言,CPU 会将程序计数器 (PC) 保存在某处,然后跳转(即设置 PC)到某个固定地址,即配置的故障处理程序。在开始执行故障处理程序之前,CPU 还会将 CPU 模式更改为故障处理模式(具有内核级权限,实际上可能只是内核模式,具体取决于 CPU 架构)。
如果故障发生在 CPU 处于非特权模式时,那么(假设操作系统设计正确)系统仍处于已知状态,除了当前正在执行的用户上下文(进程)。故障处理程序可以检查它所拥有的有关进程的信息并决定要做什么。这包括根本不是错误的事件,例如将内存映射文件的页面加载到 RAM 中。即使该事件不是内核可以处理的事件,错误的后果也仅限于进程,因此允许进程为此类事件设置信号处理程序。信号处理程序意味着内核将把控制权转交给进程,但会跳转到进程配置的某个固定地址。在进程中处理此类事件非常棘手,而且大多数情况下都不会这样做,但进程可以承担责任并按照其想要的方式处理事件。
如果故障发生在 CPU 处于内核模式时,故障处理程序会做什么?一些可能破坏内核内存的代码刚刚试图做一些不可能的事情。无法知道后果有多严重。也许一切都很好,然后代码试图运行被禁用的可选部分,因此试图跳转到存储在空指针中的地址。或者代码可能一直在运行一个破坏内存的循环,并且它刚刚到达映射块的末尾。此时,最安全的做法是尽可能少做,停止做任何事情(特别是不要写入磁盘,以避免破坏存储的风险)。这在操作系统设计中称为恐慌:发生了一些意外的事情,无法安全地恢复,因此操作系统试图通过尽可能少地做事来尽可能少地造成伤害,就是这样。
Linux 的设计基于这样的假设:内核接受的任何代码都是经过非常非常谨慎编写的,因此内核模式中的故障通常可能是相对良性的。除非
kernel.panic_on_oops
启用 sysctl 设置,否则内核将通过仅终止当前任务来响应故障,希望其他任务不受影响。这假设该任务与其他任务基本保持独立,特别是没有破坏其他任务使用的任何内存,并且不持有其他任务也使用的任何锁。在这种情况下,“任务”大致是指内核线程。该内核线程可能与用户线程相关联,在这种情况下,用户线程也会被终止,因为内核无法再处理它。但是,如果故障发生在没有任务上下文的情况下怎么办?那么就没有希望限制故障的影响了。您可以
kernel_should_crash
在发生故障时调用的函数中看到 Linux 内核对“无任务上下文”的定义:p->pid == 0
,则意味着内核在调度程序中,并且无法从调度程序故障中恢复——调度程序负责决定下一步做什么,但它无法决定下一步做什么,所以无法知道下一步该做什么。现在,我敢肯定,您仍然觉得这并没有真正回答问题。为什么内核不让您假设一切都很好,并继续处理它仍然具有的任何损坏的数据结构?在任何给定情况下,熟悉代码并可以访问调试器的熟练程序员都可以决定,是的,只要我们不触及这部分内存,系统仍然很好……但是哪一部分呢?哦,“当然”系统需要释放这个锁(否则下次同一外设触发中断时,它将进入无限循环)。不,那个锁:那是一个指向刚刚被释放的内存的指针(可能已被另一个内核上运行的另一个线程重用)。现在有一些内核内存无法访问,但太糟糕了,这只是在用户决定重新启动之前的一小段时间。哦,“当然”我们需要在做其他任何事情之前取消屏蔽中断。哦,但在此之前,我们需要确保一旦中断被解除屏蔽,另一个 CPU 不会立即从同一外设获取已经挂起的中断。哦,还有……
重点是,在中断期间处理故障的有效方法在很大程度上取决于中断处理程序正在做什么以及故障的性质。如果您可以编写足够强大的代码来可靠地处理故障,那么您就可以编写不会触发故障的代码 - 这要容易得多!
处理嵌套异常(中断或故障)(例如中断处理程序期间的故障)的另一个问题是,根据发生故障时的 CPU 架构和系统状态,有关第一个异常的某些信息可能会丢失。嵌套异常处理需要某种堆栈来存储有关每个连续异常的信息。在某些架构上,这完全取决于异常处理程序:它需要检测嵌套异常并将信息保存在任何需要的地方。有些架构中 CPU 本身会操作堆栈,但即使在这些架构上,堆栈也可能已满(尤其是如果异常处理程序中的错误导致它无法按预期返回)。
在触发故障的相同权限级别上处理故障极其棘手。Linux 已经站在了 YOLO 的一边。更强大的系统在内核模式下执行的操作更少。即使在 Linux 中(实际上在所有内核中),中断处理程序也应该尽可能少地执行操作,原因有很多。(驱动原因通常是并发性:在中断处理程序运行时,有很多事情不能发生。)中断处理程序应该很小,而且众所周知,它是特别精致的代码,因此应该特别小心地编写。如果连它都有缺陷,那就没有希望了。
执行上下文不同。
“用户模式”是程序运行的地方,它有各种限制,将其与现实世界隔离开来。系统仍然拥有它知道未被用户弄乱的代码,并且可以依靠它来处理用户级故障。
“内核模式”是中断处理程序和其他系统任务运行的模式,没有任何限制。内核模式下的所有硬件/软件都容易受到有缺陷的程序的攻击。
当内核模式发生故障或中断处理程序发生中断时,系统其余部分都不再可靠。系统将重新启动以刷新可能已损坏的软件。