Skip to main content
 首页 » 操作系统

Linux 进程冻结学习笔记

2022年07月19日177Renyi-Fan

一、内核进程冻结文档Documentation\power\freezing-of-tasks.txt翻译

任务冻结
(C)2007拉斐尔·J·怀索克<rjw@sisk.pl>,GPL

I.什么是任务冻结?

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

二.  它是如何工作的?

每个任务有三个标志 PF_NOFREEZEPF_FROZENPF_FREEZER_SKIP(最后一个是辅助标志)。 未设置 PF_NOFREEZE 的任务(所有用户空间进程和某些内核线程)被视为“可冻结的”,并在系统进入挂起状态之前以及在创建休眠映像之前以特殊方式进行处理(在此之后,我们仅考虑休眠,但说明也适用于暂停)。即,作为休眠过程的第一步,将调用函数 freeze_processes()(在kernel/power/process.c中定义)。

系统范围的变量 system_freezing_cnt(与每个任务的标志相反)用于指示系统是否在进行冻结操作。然后 freeze_processes()设置此变量。此后,它将执行try_to_freeze_tasks(),该函数会向所有用户空间进程发送虚假信号(fake signal),并唤醒所有内核线程。所有可冻结的任务必须通过调用 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()).

如果在冻结器启动冻结操作后,可冻结的内核线程未能调用 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(不允许直接重置PF_NOFREEZE)。 从这一点来看,它被视为可冻结的,必须在适当的位置调用 try_to_freezee()。

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

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

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在运行时处于联机状态的进程。 由于我们需要在休眠期间禁用非引导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(),则它将超时并最终失败,因为此时应响应请求的用户空间进程是被冻结状态的。 因此,表面上的失败似乎是由于任务冻结。但是,假设固件文件位于只能通过尚未resume的设备访问的文件系统上。 在这种情况下,无论是否使用任务冻结,request_firmware()都会失败。 因此,该问题与冻结任务并没有真正的关系,因为无论如何它通常都存在。

在调用suspend()之前,驱动程序必须在RAM中拥有其可能需要的所有固件。 如果保留它们不可行(例如由于它们的大小),则必须使用 Documentation/driver-api/pm/notifiers.rst 中描述的suspend通知链API提早请求它们。

六.有什么预防措施可以防止冻结失败?

是的,有。

首先,不鼓励持有 'system_transition_mutex' 锁来从系统范围的睡眠中互斥一段代码,例如暂停/休眠。 如果可能的话,那段代码必须改为挂接到挂起/休眠通知程序上,以实现互斥。 请查看CPU热插拔代码(kernel/cpu.c)作为示例。

但是,如果这样做不可行,并且认为必须使用 “system_transition_mutex”,则强烈不建议您直接调用mutex_[un]lock(&system_transition_mutex),因为这可能会导致冻结失败,因为如果suspend/hibernate代码成功获取了“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,取值范围为无符号整数。

补充总结:

1. 用户进程默认是可以被冻结的,借用信号处理机制实现;内核线程和work_queue默认是不能被冻结的,少数内核线程和work_queue在创建时指定了freezable标志,这些任务需要对freeze状态进行判断,当系统进入freezing时,主动暂停运行。

2. kernel threads可以通过调用 kthread_freezable_should_stop() 来判断freezing状态,并主动调用 __refrigerator()进入冻结;work_queue通过判断 max_active 属性,如果 max_active=0,则不能入队新的work,所有work延后执行。

3. 标记系统freeze状态的有三个重要的全局变量:pm_freezing、system_freezing_cnt和pm_nosig_freezing,如果全为0,表示系统未进入冻结;

system_freezing_cnt > 0  //表示系统进入冻结, 
pm_freezing = true  //表示冻结用户进程, 
pm_nosig_freezing = true  //表示冻结内核线程和workqueue。

它们会在 freeze_processes 和 freeze_kernel_threads 中置位,在 thaw_processes 和 thaw_kernel_threads 中清零。

4. fake_signal_wake_up() 函数巧妙的利用了信号处理机制,只设置任务的 TIF_SIGPENDING 位,但不传递任何信号,然后唤醒任务;这样任务在返回用户态
时会进入信号处理流程,检查系统的freeze状态,并做相应处理。

TODO: 确认一下 lock_system_sleep() 在哪里有额外调用吗?

二、代码分析

三、Debug总结

1. 进程冻结debug log打印

freeze_processes/freeze_kernel_threads //kernel/power/process.c 
    try_to_freeze_tasks(bool user_only) 
        //若有唤醒源持锁导致休眠失败打印: 
        pr_err("Freezing of tasks aborted after %d.%03d seconds", elapsed_msecs / 1000, elapsed_msecs % 1000); 
        //若有冻结失败的会打印 
        pr_err("Freezing of tasks failed after %d.%03d seconds (%d tasks refusing to freeze, wq_busy=%d):\n", elapsed_msecs/1000, elapsed_msecs%1000, todo-wq_busy, wq_busy); 
        //若冻结失败还会打印冻结失败的进程 
        for_each_process_thread(g, p) { 
            if (p != current && !freezer_should_skip(p) && freezing(p) && !frozen(p)) 
                sched_show_task(p); /*打印格式eg: BuglyThread-1   R  running task        0 14555    789 0x00400809*/ 
            }

有个超时时间为20s的冻结死循环,不断等待进程冻结完毕,会打印出冻结失败进程的信息,也会打印出整个冻结过程持续的时间。

四、相关测试

1. 通过信号冻结解冻测试

(1) 测试代码

#include <stdio.h> 
#include <unistd.h> 
 
void main() 
{ 
    int count = 0; 
    while(1) { 
        printf("count=%d\n", count++); 
        sleep(1); 
    } 
}

(2) 进行冻结解冻

root@ubuntu:/work/freeze# ps -A | grep pp 
  2784 ?        00:00:00 indicator-appli 
  2852 ?        00:00:19 nm-applet 
  4365 pts/6    00:00:00 pp 
root@ubuntu:/work/freeze#  
root@ubuntu:/work/freeze# kill -19 4365 // 19) SIGSTOP 
root@ubuntu:/work/freeze#  
root@ubuntu:/work/freeze# kill -18 4365 // 18) SIGCONT

(3) 结果打印

root@ubuntu:/work/freeze# ./pp 
count=0 
count=1 
...... 
count=11 
count=12 
 
[1]+  Stopped                 ./pp 
root@ubuntu:/work/freeze# count=13  //解冻后接着执行 
count=14 
count=15

注意:这里只是模拟进程被冻结的状态,实际进程冻结并不是给进程发任何信号,而是只设置任务的 TIF_SIGPENDING 位,让进程返回用户控件之前检查是否要冻结。


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