Skip to main content
 首页 » 操作系统

Linux Documentation/power/freezing-of-tasks.rst 翻译

2022年07月19日184kevingrace

来自:kernel-5.10

==================
冻结任务
==================

(C) 2007 Rafael J. Wysocki <rjw@sisk.pl>,GPL

一、什么是任务冻结?

==================================

任务的冻结是一种机制,在休眠或系统范围的挂起(在某些体系结构上)期间,用户空间进程和某些内核线程可以通过该机制进行控制。


二、 它是如何工作的?

======================

为此使用了三个per-task的标志,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)); 
 
//(from drivers/usb/core/hub.c::hub_thread()).

如果一个可冻结的内核线程在freezer启动冻结操作后调用 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(): 
- 解冻所有任务(包括内核线程),因为我们无法在不解冻内核线程的情况下解冻用户空间任务

四. 我们为什么要这样做?

=======================

一般来说,使用冻结任务有几个原因:

1. 主要原因是为了防止文件系统在休眠后被损坏。目前我们没有简单的检查点文件系统的方法,所以如果对磁盘上的文件系统数据和/或元数据进行了任何修改,我们无法将它们恢复到修改之前的状态。同时每个休眠镜像都包含一些文件系统相关的信息,这些信息必须与从镜像恢复系统内存状态后的磁盘数据和元数据的状态一致(否则文件系统将被严重损坏,通常使它们几乎无法修复)。因此,我们冻结了可能导致在创建休眠映像之后和系统最终关闭之前修改磁盘文件系统的数据和元数据的任务。其中大部分是用户空间进程,但如果任何内核线程可能导致类似的事情发生,它们必须是可冻结的。

2. 接下来,要创建休眠映像,我们需要释放足够的内存(大约 50% 的可用 RAM),并且我们需要在设备停用之前这样做,因为我们通常需要它们来换出。 然后,在镜像的内存被释放后,我们不希望任务分配额外的内存,我们通过提前冻结它们来防止它们这样做。 [当然,这也意味着设备驱动程序在休眠之前不应从其 .suspend() 回调中分配大量内存,但这是一个单独的问题。]

3、第三个原因是防止用户空间进程和一些内核线程干扰设备的挂起和恢复。 例如,当我们暂停设备时,在第二个 CPU 上运行的用户空间进程可能会访问设备,如果没有冻结任务,我们需要一些保护措施来防止在这种情况下可能发生的竞争情况。

虽然 Linus Torvalds 不喜欢冻结任务,但他在一次关于 LKML 的讨论中这样说(http://lkml.org/lkml/2007/4/27/608):

"RJW:> 为什么我们冻结任务或者为什么我们冻结内核线程?

Linus:在很多方面,“完全”。

我**确实**意识到 IO 请求队列问题,并且我们实际上无法对 DMA 中间的某些设备执行 s2ram。 所以我们希望能够避免*that*,这是毫无疑问的。 而且我怀疑停止用户线程然后等待同步实际上是更简单的方法之一。

所以在实践中,“根本”可能会变成“为什么要冻结内核线程?” 和冻结用户线程我并不觉得真的令人反感。”

尽管如此,还是有一些内核线程可能想要被冻结。 例如,如果属于设备驱动程序的内核线程直接访问设备,原则上需要知道设备何时挂起,这样它就不会在那个时候尝试访问它。 但是,如果内核线程是可冻结的,它将在驱动程序的 .suspend() 回调执行之前被冻结,并在驱动程序的 .resume() 回调运行后解冻,因此它不会在设备被挂起时访问设备了

4. 冻结任务的另一个原因是防止用户空间进程意识到发生了休眠(或挂起)操作。 理想情况下,用户空间进程不应注意到已发生此类系统范围的操作,并且应在恢复(或从挂起状态恢复)后继续运行而不会出现任何问题。 不幸的是,在最一般的情况下,如果不冻结任务,这很难实现。 例如,考虑一个依赖于所有 CPU online的进程。 由于我们需要在休眠期间禁用非引导 CPU,如果这个进程没有被冻结,它可能会注意到 CPU 的数量已经挂起,并且可能因此开始无法正常工作。

五、是否存在与任务冻结相关的问题?

==================================================== =========

是的,有。

首先,如果内核线程相互依赖,冻结内核线程可能会很棘手。 例如,如果内核线程 A 等待需要由可冻结内核线程 B 完成的完成(处于 TASK_UNINTERRUPTIBLE 状态),同时 B 被冻结,那么 A 将被阻塞,直到 B 解冻,这可能是不希望的。 这就是默认情况下内核线程不可冻结的原因。

其次,与冻结用户空间进程有关的问题有以下两个:

1. 将进程置于不可中断的睡眠状态会扭曲平均负载。
2. 现在我们有了 FUSE,加上在用户空间做设备驱动程序的框架,它变得更加复杂,因为一些用户空间进程现在正在做内核线程所做的事情(https://lists.linux-foundation.org/pipermail/linux-pm/2007-May/012309.html)。

问题 1. 似乎是可以解决的,尽管到目前为止还没有解决。 另一个更严重,但似乎我们可以通过使用休眠(和挂起)通知程序来解决它(但是,在这种情况下,我们将无法避免用户空间进程意识到休眠正在发生)。

任务的冻结也容易暴露出一些问题,尽管它们与它没有直接关系。 例如,如果从设备驱动程序的 .resume() 例程中调用 request_firmware(),它将超时并最终失败,因为此时应该响应请求的用户空间进程还处于被冻结状态。 因此,看起来,失败是由于任务冻结造成的。 然而,假设固件文件位于一个
文件系统上,该文件系统只能通过另一个尚未恢复的设备访问。 在这种情况下,无论是否使用冻结任务,request_firmware() 都会失败。 因此,该问题与冻结任务并不真正相关,因为它通常无论如何都存在。

在调用 suspend() 之前,驱动程序必须在 RAM 中拥有它可能需要的所有固件。 如果保留它们不切实际,例如由于它们的大小,必须使用 Documentation/driver-api/pm/notifiers.rst 中描述的挂起通知器 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,范围为无符号整数。

其它冻结相关博文:

Cgroup内核文档翻译(5)——Documentation/cgroup-v1/freezer-subsystem.txt


本文参考链接:https://www.cnblogs.com/hellokitty2/p/15779814.html