Freezing of tasks [Kernel Documents]

Freezing of tasks

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

翻译: Arethe Qin

        

I.                    什么是任务冷冻?

任务冷冻是一种在系统休眠或者系统挂起期间控制用户空间进程和一些内核线程的机制。

II.                  它是如何工作的?

每个任务都有4个标志用于任务冷冻机制,PF_NOFREEZE, PF_FROZEN, TIF_FREEZEPF_FREEZER_SKIP(最后一个是辅助的[auxiliary])。没有设置PF_NOFREEZE标志的任务(包括所有的用户空间进程和部分内核线程)被认为是“可冷冻的”。在系统进入挂起状态前,或创建休眠映像[hibernation image](在下文中仅讨论休眠的情况,但是以下的论述同样适用于挂起),对此类任务要进行特殊处理。

就是说,在休眠过程的第一步,需要调用函数freeze_processes()(定义在kernel/power/process.c)。此函数通过try_to_freeze_tasks()函数将所有“可冷冻”任务的TIF_FREEZE标志置位。如果这些任务是内核线程,则唤醒它们。如果是用户空间进程,则向它们发送一个欺骗信号[fake signals]。如果一个任务设置了TIF_FREEZE标志,就必须通过函数refrigerator()(定义在kernel/power/process.c)对其进行响应,这个函数设置任务的PF_FROZEN标志,将任务的状态切换至TASK_UNINTERRUPTIBLE,并且使得该任务不断的循环直到PF_FROZEN标准被清除。随后,我们就可以说这个任务被“冷冻”了,实现这个机制的那些函数则被称作“冰箱”(这些函数定义在kernel/power/process.c以及头文件include/linux/freezer.h)。用户空间进程需在内核线程之前被逐个地冷冻。

       最好不要直接调用refrigerator()函数。如果有类似需要,可以使用try_to_freeze()函数(定义在include/linux/freezer.h),该函数会检查任务的TIF_FREEZE标志,在该标志被置位的时候,才会调用refrigerator()

         对用户空间进程而言,信号处理程序会自动调用try_to_freeze(),但是可冷冻的内核线程则需要在合适的地方显示地调用它,或者使用wait_event_freezable()wait_event_freezable_timeout()(定义在include/linux/freezer.h),这些宏在TIF_FREEZE标志被置位时将内核线程转入可中断睡眠态,并且调用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()).

         如果一个内核线程在“冰箱”为其设置TIF_FREEZE后调用try_to_freeze()失败,便意味着任务冷冻的失败,整个休眠操作都应该被取消。出于此原因,可冷冻的内核线程必须在某处调用try_to_freeze()函数,或者使用宏wait_event_freezable()wait_event_freezable_timeout()中的一个。

         在系统内存状态从一个休眠映像中恢复,并且设备被重新初始化之后,需要调用解冻函数thaw_processes()清除所有冷冻任务的PF_FROZEN标志。此后,被解冻的任务离开refrigerator(),继续运行。

 

III.                哪些内核线程是可冷冻的?

内核线程在默认情况下是不可冷冻的。不过,内核线程可以通过调用set_freezable()以清除自己的PF_NOFREEZE标志(强烈建议不要直接重置PF_NOFREEZE标志)。如果一个线程被认为是可冷冻的,最好还是在合适的地方调用try_to_freeze()函数。

IV.                我们为什么要冷冻任务?

一般而言,下面两点原因可以解释为什么要冷冻任务:

1.       最主要的原因是防止文件系统在休眠之后被破坏。在没有简单意义上的检查点文件系统的情况下,对磁盘上的数据和(或)元数据的任何修改,都将使文件系统无法还原。于此同时,每个休眠映像都包含一些文件系统相关的信息,这些信息必须与系统内存状态存储在休眠映像中时的盘上数据及元数据保持一致(否则文件系统将被以一种可怕的方式破坏掉,而这种破坏通常是无法修复的)。因而,我们将那些可能会引起盘上文件系统的数据及元数据在创建休眠映像以后到系统真正被断电以前被修改的任务冷冻起来(好长的句子)。这些任务中的大多数是用户空间进程,但是,如果一个内核线程可能引起上述状况,也应该被冷冻起来。

2.       其次,为了创建休眠映像,我们需要释放足够的内存空间(大概是整个可用RAM50%),而且需要在设备被重新激活以前完成这个工作,因为我们通常需要这些设备来支持“换出”。在释放了足够的内存之后,我们不期望任务再来申请内存,因而还是在此之前将它们冷冻了比较好。(当然,这也意味着设备驱动将在休眠之前不能通过它们的.suspend()回调操作申请大块的内存,但是这是一个e separate issue.)

3.       第三个原因是为了避免用户空间进程及一些内核线程干扰设备的挂起和恢复操作。举个例子,在我们正在挂起某个设备时,某个运行在另一个CPU上的用户空间进程可能会造成麻烦。如果不冷冻这些任务,我们就需要一些防止竞态的保护措施,因为在这种情况下,可能会出现竞态。

Linus不喜欢任务冷冻,他在LKML(http://lkml.org/lkml/2007/4/27/608)中说过下面这段话:

RJW:从本质上,为什么我们要冷冻任务?问什么我们要冷冻内核线程?

Linus:有很多原因,在本质上[‘at all’]

我终于理解了IO请求队列的问题,我们不能真的在DMA中间对某些设备进行s2ram。因而,如果能够避免它,那么就不存在问题了。而且,我猜想终止[stopping]用户线程,并等待一个同步是比较容易实现的。

所以,在实现上,’at all’[不知道如何翻译]可能变成为什么冷冻内核线程?’,对用户线程的冷冻并没有引起我的不快。

当然,有一些内核线程主动的想要被冷冻,举个例子,对与某个隶属于设备驱动的内核线程,该线程可以直接访问该设备,它在原则上需要知道该设备什么时候被挂起,那么在挂起期间,它就不会去尝试访问此设备。不过,如果这个内核线程是可冷冻的,它在设备的回调操作.suspend()被调用以前就会被冷冻起来了,直到此设备的.resume回调函数被调用后才会解冻。因而,它就不会在设备挂起期间尝试访问设备了。

4.       另外一个冷冻任务的原因是,阻止用户空间进程意识到是否发生了休眠(或挂起)操作。在理想的状态下,用户空间进程不应受到此类系统级操作的影响,而且在系统恢复时,能够完美的继续执行。不幸的是,如果离开了任务冷冻机制的帮助,在大多数普通情况下是很难实现这种期望的。让我们来考虑一个例子,一个进程在执行时,需要所有的CPU都处于在线[online]状态。因为我们在休眠时需要禁用所有的非启动CPU,那么在任务没有被冷冻的情况下,它可能会感知到CPU数目的变化,并且因而无法正常工作。

V.                  还有什么与任务冷冻相关的问题吗?

是的,还有一些。

首先,如果内核线程彼此依赖,那么对它们的冷冻会有一些麻烦。比如说,如果内核线程A正在等待(处于TASK_UNINTERRUPTIBLE状态)一个可冷冻任务B完成某个操作,而与此同时,B被冷冻了。那么A必须被阻塞直到B解冻为止,这个时间是无法预期的。这就是内核线程为什么在默认条件下是不可冷冻的原因。

第二,用户空间进程的冷冻也存在2个问题:

1.       将进程转入不可唤醒的睡眠会引起负载失衡。

2.       现在我们使用了FUSE框架,可以在用户空间中执行设备驱动。那么用户空间进程便需要执行内核线程常做的排序操作,这便使问题变得更加复杂。(https://lists.linux-foundation.org/pipermail/linux-pm/2007-May/012309.html)

问题1似乎是可以被解决的,虽然现在还没有解决。另外一个问题则更严重一点,不过貌似可以使用休眠(挂起)通知链的方法加以解决。(在这种情况下,我们无需避免用户空间进程对休眠操作的感知。)

还存在一些由任务冷冻引起的问题,虽然这些问题并不和任务冷冻直接相关。比如,如果函数request_firmware()在一个设备驱动的.resume()例程中被调用,它将会超时,并最终失败,因为需要响应此操作的用户空间进程可能被冷冻了。因此,这个问题看起来是由任务冷冻引起的。假设相关的firmware文件存放于需要通过其它设备进行访问的文件系统中,而该设备尚未恢复[resume]。在此情况下,无论相关的任务是否被冷冻,request_firmware()都会失败。所以,该问题并非真的与任务冷冻相关,其它原因也会引起此问题的出现。

一个驱动必须在suspend()被调用之前拥有它所需的所有的firmwares。如果这不太可能实现,比如这些firmwares过大,就必须使用suspend通知链尽早的申请它们。相关的APInotifiers.txt中有介绍。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页