任务冻结
什么是任务冻结?
任务冻结是一种机制,通过该机制可以在休眠或系统范围挂起(在某些架构上)期间控制用户空间进程和一些内核线程。
它是如何工作的?
任务冻结使用三个任务级标志,即PF_NOFREEZE、PF_FROZEN和PF_FREEZER_SKIP(最后一个是辅助标志)。具有未设置PF_NOFREEZE(所有用户空间进程和一些内核线程)的任务被视为“可冻结”,并在系统进入挂起状态之前以及在创建休眠映像之前(以下我们只考虑休眠,但描述也适用于挂起)以特殊方式处理。
具体来说,在休眠过程的第一步中,将调用函数freeze_processes()(在kernel/power/process.c中定义)。系统范围变量system_freezing_cnt(与每个任务标志相对应)用于指示系统是否要进行冻结操作。freeze_processes()设置此变量。然后,它执行try_to_freeze_tasks(),向所有用户空间进程发送一个虚假信号,并唤醒所有内核线程。所有可冻结任务必须通过调用try_to_freeze()做出反应,这将导致调用__refrigerator()(在kernel/freezer.c中定义),它设置任务的PF_FROZEN标志,将其状态更改为TASK_UNINTERRUPTIBLE,并使其循环,直到为止。PF_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/freezer.h中定义),它们将可中断的睡眠与检查任务是否要被冻结和调用try_to_freeze()结合在一起。可冻结内核线程的主循环可能如下所示:
set_freezable();
do {
hub_events();
wait_event_freezable(khubd_wait,
!list_empty(&hub_event_list) ||
kthread_should_stop());
} while (!kthread_should_stop() || !list_empty(&hub_event_list));
(来自drivers/usb/core/hub.c::hub_thread())。
如果可冻结的内核线程在冷冻器启动冻结操作后未能调用try_to_freeze(),则任务冻结将失败,并且整个休眠操作将被取消。因此,可冻结的内核线程必须在某个地方调用try_to_freeze(),或者使用wait_event_freezable()和wait_event_freezable_timeout()宏之一。
在系统内存状态从休眠映像中恢复并重新初始化设备后,将调用函数thaw_processes()以清除每个冻结任务的PF_FROZEN标志。然后,已被冻结的任务离开__refrigerator()并继续运行。
处理冻结和解冻任务的函数背后的原理
- freeze_processes():仅冻结用户空间任务
- freeze_kernel_threads():冻结所有任务(包括内核线程),因为我们不能在不冻结用户空间任务的情况下冻结内核线程
- thaw_kernel_threads():解冻只内核线程;如果需要在解冻内核线程和解冻用户空间任务之间执行任何特殊操作,或者如果要推迟解冻用户空间任务,则这是特别有用的
- thaw_processes():解冻所有任务(包括内核线程),因为我们不能在不解冻内核线程的情况下解冻用户空间任务
哪些内核线程是可冻结的?
内核线程默认情况下是不可冻结的。但是,内核线程可以通过调用set_freezable()为自身清除PF_NOFREEZE。从这一点开始,它被视为可冻结,并且必须在适当的位置调用try_to_freeze()。
为什么要这样做?
一般来说,使用任务冻结有几个原因:
- 主要原因是防止在休眠后损坏文件系统。目前,我们没有简单的手段来对文件系统进行检查点,因此,如果对磁盘上的文件系统数据和/或元数据进行了任何修改,我们无法将其恢复到修改之前的状态。同时,每个休眠映像都包含一些与文件系统相关的信息,这些信息必须与从映像中恢复系统内存状态后的磁盘上的数据和元数据状态一致(否则文件系统将以一种讨厌的方式损坏,通常使其几乎不可能修复)。因此,我们冻结可能在创建休眠映像后并在系统最终关闭之前导致磁盘文件系统数据和元数据被修改的任务。其中大部分是用户空间进程,但如果任何内核线程可能导致这种情况发生,它们也必须是可冻结的。
- 其次,为了创建休眠映像,我们需要释放足够的内存(大约为可用RAM的50%),并且我们需要在设备被停用之前这样做,因为我们通常需要它们进行交换。然后,在释放映像的内存之后,我们不希望任务分配额外的内存,因此我们通过更早地冻结它们来阻止它们这样做。[当然,这也意味着设备驱动程序在休眠之前不应该从其.suspend()回调中分配大量内存,但这是另一个问题。]
- 第三个原因是防止用户空间进程和一些内核线程干扰设备的挂起和恢复。例如,在我们挂起设备时运行在第二个CPU上的用户空间进程可能会引起麻烦,如果没有任务冻结,我们需要一些防护措施来防止在这种情况下可能发生的竞争条件。
尽管Linus Torvalds不喜欢任务冻结,但他在LKML上的讨论中说过:
"RJW:> 为什么我们要冻结任务,或者为什么我们要冻结内核线程?
Linus: 在很多方面,“为什么要冻结任务”。
我确实意识到IO请求队列的问题,以及我们实际上不能在DMA进行中进行s2ram。因此,我们希望能够避免这种情况,这是毫无疑问的。我怀疑停止用户线程,然后等待同步实际上是实现这一点的较容易的方法之一。
因此,在实践中,“为什么要冻结内核线程?”可能会变成“为什么要冻结内核线程?”而我并不认为冻结用户线程真的令人反感。"
仍然有一些内核线程可能希望是可冻结的。例如,如果属于设备驱动程序的内核线程直接访问设备,则原则上需要知道何时暂停设备,以便在此时不尝试访问它。但是,如果内核线程是可冻结的,它将在执行驱动程序的.suspend()回调之前被冻结,并且在驱动程序的.resume()回调运行后被解冻,因此在设备暂停时不会访问设备。
另一个冻结任务的原因是防止用户空间进程意识到休眠(或挂起)操作正在进行。理想情况下,用户空间进程不应该注意到发生了这样的系统范围操作,并且在恢复(或从挂起中恢复)后应该继续运行而没有任何问题。不幸的是,在最一般的情况下,这是相当难以在没有任务冻结的情况下实现的。例如,考虑一个依赖所有CPU在线时运行的进程。由于我们需要在休眠期间禁用非引导CPU,如果此进程未被冻结,它可能会注意到CPU数量已更改,并且可能开始因此而工作不正确。
与任务冻结相关的问题有哪些?
是的,有。
首先,如果内核线程相互依赖,冻结内核线程可能会棘手。例如,如果内核线程A等待由可冻结内核线程B完成(处于TASK_UNINTERRUPTIBLE状态),而B在此期间被冻结,则A将被阻塞,直到B被解冻,这可能是不希望的。这就是为什么内核线程默认情况下不可冻结的原因。
其次,与冻结用户空间进程相关的问题有以下两个:
- 将进程置于不可中断的睡眠状态会扭曲负载平均值。
- 现在我们有了FUSE,以及在用户空间进行设备驱动程序的框架,情况变得更加复杂,因为一些用户空间进程现在正在执行内核线程的工作。
问题1似乎是可以解决的,尽管到目前为止还没有解决。另一个问题更为严重,但似乎我们可以通过使用休眠(和挂起)通知器来解决(在这种情况下,尽管如此,我们将无法避免用户空间进程意识到正在进行休眠)。
任务冻结往往暴露出一些问题,尽管它们与任务冻结并不直接相关。例如,如果从设备驱动程序的.resume()例程中调用request_firmware(),它将超时并最终失败,因为应该响应请求的用户空间进程此时被冻结。因此,看起来失败是由于任务冻结。然而,假设固件文件位于仅通过另一个尚未恢复的设备访问的文件系统上。在这种情况下,request_firmware()将失败,无论是否使用任务冻结。因此,问题实际上与任务冻结无关,因为通常情况下它确实存在。
在调用suspend()之前,驱动程序必须在RAM中具有所有可能需要的固件。如果保留它们不切实际,例如由于其大小,必须尽早使用挂起通知器API请求它们。
有什么预防措施可以防止冻结失败?
是的,有。
首先,不鼓励使用“system_transition_mutex”锁来相互排除系统范围睡眠(如挂起/休眠)的代码片段。如果可能,该代码片段必须代替挂接到挂起/休眠通知器上以实现相互排除。可以查看CPU-Hotplug代码(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)。这将防止冻结失败。
其他
/sys/power/pm_freeze_timeout
控制冻结所有用户空间进程或所有可冻结内核线程所需的最长时间,单位为毫秒。默认值为20000,范围为无符号整数。