任务冻结¶
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 中定义),如果设置了 freezer_active,这些宏会将任务置于睡眠状态 (TASK_INTERRUPTIBLE) 或将其冻结 (TASK_FROZEN)。可冻结内核线程的主循环可能如下所示
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/[email protected])
“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,范围为无符号整数。