任务冻结¶
2007 Rafael J. Wysocki <rjw@sisk.pl>, GPL
I. 什么是任务冻结?¶
任务冻结是一种机制,用于在休眠或系统范围挂起(在某些架构上)期间控制用户空间进程和一些内核线程。
II. 它如何工作?¶
有一个每个任务的标志(PF_NOFREEZE)和三个每个任务的状态(TASK_FROZEN,TASK_FREEZABLE 和 __TASK_FREEZABLE_UNSAFE)用于此目的。未设置 PF_NOFREEZE 的任务(所有用户空间任务和一些内核线程)被视为“可冻结”,并在系统进入睡眠状态之前以及创建休眠镜像之前以特殊方式处理(休眠直接包含在以下内容中,但描述也适用于系统范围的挂起)。
也就是说,作为休眠过程的第一步,调用函数 freeze_processes()(在 kernel/power/process.c 中定义)。系统范围的静态键 freezer_active(与每个任务的标志或状态相反)用于指示系统是否将进行冻结操作。freeze_processes() 设置此静态键。之后,它执行 try_to_freeze_tasks(),向所有用户空间进程发送一个伪信号,并唤醒所有内核线程。所有可冻结任务必须通过调用 try_to_freeze() 来对此做出反应,这会导致调用 __refrigerator()(在 kernel/freezer.c 中定义),这会将任务的状态更改为 TASK_FROZEN,并使其循环直到它被显式的 TASK_FROZEN 唤醒。然后,该任务被视为“冻结”,因此处理此机制的函数集被称为“冷冻器”(这些函数在 kernel/power/process.c,kernel/freezer.c 和 include/linux/freezer.h 中定义)。用户空间任务通常在内核线程之前被冻结。
__refrigerator() 不得直接调用。而是使用 try_to_freeze() 函数(在 include/linux/freezer.h 中定义),该函数检查任务是否要被冻结,并使任务进入 __refrigerator()。
对于用户空间进程,try_to_freeze() 从信号处理代码自动调用,但可冻结的内核线程需要在适当的位置显式调用它,或者使用 wait_event_freezable()
或 wait_event_freezable_timeout() 宏(在 include/linux/wait.h 中定义),这些宏使任务进入睡眠状态(TASK_INTERRUPTIBLE)或冻结它(TASK_FROZEN)如果设置了 freezer_active。可冻结内核线程的主循环可能如下所示
set_freezable();
while (true) {
struct task_struct *tsk = NULL;
wait_event_freezable(oom_reaper_wait, oom_reaper_list != NULL);
spin_lock_irq(&oom_reaper_lock);
if (oom_reaper_list != NULL) {
tsk = oom_reaper_list;
oom_reaper_list = tsk->oom_reaper_list;
}
spin_unlock_irq(&oom_reaper_lock);
if (tsk)
oom_reap_task(tsk);
}
(来自 mm/oom_kill.c::oom_reaper())。
如果在冷冻器启动冻结操作后,可冻结内核线程未进入冻结状态,则任务冻结将失败,并且整个系统范围的转换将被取消。因此,可冻结内核线程必须在某处调用 try_to_freeze() 或使用 wait_event_freezable()
和 wait_event_freezable_timeout() 宏之一。
在系统内存状态从休眠镜像恢复并且设备重新初始化后,调用函数 thaw_processes() 以唤醒每个冻结的任务。然后,已被冻结的任务离开 __refrigerator() 并继续运行。
处理任务冻结和解冻的函数背后的原理¶
- freeze_processes()
仅冻结用户空间任务
- freeze_kernel_threads()
冻结所有任务(包括内核线程),因为我们无法在不冻结用户空间任务的情况下冻结内核线程
- thaw_kernel_threads()
仅解冻内核线程;如果我们需要在解冻内核线程和解冻用户空间任务之间做任何特殊的事情,或者如果我们想推迟解冻用户空间任务,这尤其有用
- thaw_processes()
解冻所有任务(包括内核线程),因为我们无法在不解冻内核线程的情况下解冻用户空间任务
III. 哪些内核线程是可冻结的?¶
默认情况下,内核线程不可冻结。但是,内核线程可以通过调用 set_freezable() 为自己清除 PF_NOFREEZE(不允许直接重置 PF_NOFREEZE)。从这一点开始,它被认为是可冻结的,必须在适当的位置调用 try_to_freeze() 或 wait_event_freezable()
的变体。
IV. 我们为什么要这样做?¶
一般来说,使用任务冻结有两个原因
主要原因是防止文件系统在休眠后损坏。目前,我们没有简单的方法来检查文件系统,因此如果对磁盘上的文件系统数据和/或元数据进行了任何修改,我们无法将它们恢复到修改前的状态。与此同时,每个休眠镜像都包含一些与文件系统相关的信息,这些信息必须与从镜像恢复系统内存状态后磁盘上数据和元数据的状态一致(否则文件系统将以一种糟糕的方式损坏,通常使它们几乎不可能修复)。因此,我们冻结可能导致磁盘上的文件系统数据和元数据在创建休眠镜像之后和系统最终关闭电源之前被修改的任务。这些任务中的大多数是用户空间进程,但如果任何内核线程可能导致类似情况发生,则它们必须是可冻结的。
接下来,要创建休眠镜像,我们需要释放足够的内存(大约 50% 的可用 RAM),并且我们需要在设备停用之前执行此操作,因为我们通常需要它们来进行交换。然后,在释放镜像的内存后,我们不希望任务分配额外的内存,我们通过更早地冻结它们来防止它们这样做。[当然,这也意味着设备驱动程序不应在休眠之前从其 .suspend() 回调中分配大量内存,但这是一个单独的问题。]
第三个原因是防止用户空间进程和一些内核线程干扰设备的挂起和恢复。例如,当我们在挂起设备时,在第二个 CPU 上运行的用户空间进程可能会很麻烦,如果没有任务冻结,我们需要采取一些保护措施来防止在这种情况下可能发生的竞争条件。
虽然 Linus Torvalds 不喜欢任务冻结,但他在 LKML 的一次讨论中说了以下内容(https://lore.kernel.org/r/alpine.LFD.0.98.0704271801020.9964@woody.linux-foundation.org)
“RJW:> 我们为什么要冻结任务或为什么要冻结内核线程?
Linus:在很多方面,“完全”。
我确实意识到 IO 请求队列问题,并且我们实际上无法在 DMA 中使用某些设备执行 s2ram。所以我们希望能够避免这种情况,这毫无疑问。我怀疑停止用户线程然后等待同步实际上是做到这一点更简单的方法之一。
所以在实践中,“完全”可能会变成“为什么要冻结内核线程?”,而我发现冻结用户线程真的没什么异议。”
尽管如此,仍然有一些内核线程可能希望是可冻结的。例如,如果属于设备驱动程序的内核线程直接访问设备,原则上它需要知道设备何时挂起,以便它不会尝试在该时间访问它。但是,如果内核线程是可冻结的,它将在驱动程序的 .suspend() 回调执行之前被冻结,并在驱动程序的 .resume() 回调运行后被解冻,因此在设备挂起时它不会访问该设备。
冻结任务的另一个原因是防止用户空间进程意识到休眠(或挂起)操作正在发生。理想情况下,用户空间进程不应注意到发生了这种系统范围的操作,并且应在恢复(或从挂起恢复)后继续运行而没有任何问题。不幸的是,在大多数情况下,如果没有任务冻结,这很难实现。例如,考虑一个进程,它依赖于在运行时所有 CPU 都在线。由于我们需要在休眠期间禁用非启动 CPU,如果此进程未被冻结,它可能会注意到 CPU 的数量已更改,并且可能会因此开始错误地工作。
VI. 是否应采取任何预防措施以防止冻结失败?¶
是的,有。
首先,不鼓励获取“system_transition_mutex”锁以使一段代码与系统范围的睡眠(如挂起/休眠)互斥。如果可能,该代码段必须挂接到挂起/休眠通知程序以实现互斥。有关示例,请参阅 CPU 热插拔代码 (kernel/cpu.c)。
但是,如果这不可行,并且认为获取“system_transition_mutex”是必要的,则强烈建议不要直接调用 mutex_[un]lock(&system_transition_mutex),因为这可能导致冻结失败,因为如果挂起/休眠代码成功获取了“system_transition_mutex”锁,因此另一个实体未能获取该锁,那么该任务将被阻塞在 TASK_UNINTERRUPTIBLE 状态。因此,冷冻器将无法冻结该任务,从而导致冻结失败。
但是,[un]lock_system_sleep() API 在此场景中可以安全使用,因为它们会要求冷冻器跳过冻结此任务,因为它无论如何都“足够冻结”,因为它被阻塞在“system_transition_mutex”上,只有在整个挂起/休眠序列完成后才会释放。因此,总而言之,请使用 [un]lock_system_sleep() 而不是直接使用 mutex_[un]lock(&system_transition_mutex)。这将防止冻结失败。
V. 杂项¶
/sys/power/pm_freeze_timeout 控制冻结所有用户空间进程或所有可冻结内核线程最多需要多长时间,单位为毫秒。默认值为 20000,范围为无符号整数。