进程冻结(freezing of task)
Posted 程序猿Ricky的日常干货
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程冻结(freezing of task)相关的知识,希望对你有一定的参考价值。
进程冻结(freezing of tasks)
进程冻结是当系统hibernate或者suspend时,对进程进行暂停挂起的一种机制,后面主要以hibernate为例进行介绍。那么为什么要在hibernate或者suspend时需要把进程冻结呢?主要是出于如下的原因:
- 第1点,防止文件系统被修改后无法恢复。假设没有进程冻结操作,那么在hibernate时,进程可能会在hibernation image镜像生成后依然修改文件系统,这就导致当系统从hibernate镜像resume时这部分的修改丢失,更严重的可能导致无法从hibernate image恢复文件系统数据。
- 第2点,hibernation image生成需要足够的内存空间,为了保证内存回收后不被其他进程再申请走,因此需要先对进程进行冻结,然后在生成hibernation image。
- 第3点,防止进程在系统suspend或者hibernate之后继续访问已经休眠的设备
- 第4点,防止用户空间进程需要针对suspend或者hibernate状态做相应的处理,有了进程冻结之后,suspend和hibernate对于用户空间进程来说完全是透明的,不用特殊做处理。
实现
有3个per-task的flag用于描述进程冻结状态:
PF_NOFREEZE:如果置位表示该进程不会被冻结,为0表示进程需要在suspend或者hibernate时被冻结
PF_FROZEN:表示进程已经处于冻结状态
PF_FREEZER_SKIP:附加备用状态
3个重要的全局变量:
system_freezing_cnt:大于0表示系统进入了冻结状态
pm_freezing: true表示用户进程被冻结
pm_nosig_freezing: true表示内核进程和workqueue被冻结
重要的函数API:
freeze_processes():
- 冻结用户态进程,内部会调用 `try_to_freeze_tasks(true)`。
freeze_kernel_threads():
- 冻结内核线程,内核会调用 `try_to_freeze_tasks(false)` (实际上是冻结所有进程,因为也会扫描用户态进程)
thaw_kernel_threads():
- 解冻内核线程
thaw_processes():
- 解冻所有进程(包括内核线程和用户态进程)
如何请求冻结一个进程
freeze_processes 和 freeze_kernel_threads 最终都会调用到一个关键函数 try_to_freeze_tasks:
static int try_to_freeze_tasks(bool user_only)
struct task_struct *g, *p;
unsigned long end_time;
unsigned int todo;
bool wq_busy = false;
ktime_t start, end, elapsed;
unsigned int elapsed_msecs;
bool wakeup = false;
int sleep_usecs = USEC_PER_MSEC;
#ifdef CONFIG_PM_SLEEP
char suspend_abort[MAX_SUSPEND_ABORT_LEN];
#endif
start = ktime_get_boottime();
end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs);
if (!user_only) //根据传入的参数判断是否只冻结用户态进程
freeze_workqueues_begin(); //如果为false,则冻结WQ_FREEZABLE类型的workqueue
while (true)
todo = 0;
read_lock(&tasklist_lock);
for_each_process_thread(g, p) //遍历系统中所有进程
if (p == current || !freeze_task(p)) //对非本进程的进程尝试冻结,冻结成功执行continue下一个
continue;
if (!freezer_should_skip(p)) //运行到这里说明尝试冻结失败了,如果该进程不能跳过冻结,todo需要+1
todo++;
read_unlock(&tasklist_lock);
if (!user_only) //尝试对内核workqueue进行冻结
wq_busy = freeze_workqueues_busy();
todo += wq_busy;
if (!todo || time_after(jiffies, end_time)) //判断冻结操作是否完成,完成了就break退出循环,如果超时也会break,当做冻结失败
break;
if (pm_wakeup_pending())
#ifdef CONFIG_PM_SLEEP
pm_get_active_wakeup_sources(suspend_abort,
MAX_SUSPEND_ABORT_LEN);
log_suspend_abort_reason(suspend_abort);
#endif
wakeup = true;
break;
/*
* We need to retry, but first give the freezing tasks some
* time to enter the refrigerator. Start with an initial
* 1 ms sleep followed by exponential backoff until 8 ms.
*/
usleep_range(sleep_usecs / 2, sleep_usecs); //等待一段时间后重新尝试冻结操作
if (sleep_usecs < 8 * USEC_PER_MSEC)
sleep_usecs *= 2;
下面分析该函数的后半部分,也就是退出该循环后的操作:
end = ktime_get_boottime();
elapsed = ktime_sub(end, start);
elapsed_msecs = ktime_to_ms(elapsed);
if (wakeup) //是否是被打断
pr_cont("\\n");
pr_err("Freezing of tasks aborted after %d.%03d seconds",
elapsed_msecs / 1000, elapsed_msecs % 1000);
else if (todo) //超时退出时会到这里,此时todo大于0,还有进程未冻结,冻结失败
pr_cont("\\n");
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);
if (wq_busy)
show_workqueue_state();
read_lock(&tasklist_lock);
for_each_process_thread(g, p)
if (p != current && !freezer_should_skip(p)
&& freezing(p) && !frozen(p))
sched_show_task(p);
read_unlock(&tasklist_lock);
else //冻结成功
pr_cont("(elapsed %d.%03d seconds) ", elapsed_msecs / 1000,
elapsed_msecs % 1000);
return todo ? -EBUSY : 0;
冻结信号
freeze_task 是用来对一个进程进行freeze操作的函数,冻结进程的行为实际上只能由被冻结进程本身进行处理,而此函数只是向被冻结进程发送一个信号。
bool freeze_task(struct task_struct *p)
unsigned long flags;
/*
* This check can race with freezer_do_not_count, but worst case that
* will result in an extra wakeup being sent to the task. It does not
* race with freezer_count(), the barriers in freezer_count() and
* freezer_should_skip() ensure that either freezer_count() sees
* freezing == true in try_to_freeze() and freezes, or
* freezer_should_skip() sees !PF_FREEZE_SKIP and freezes the task
* normally.
*/
if (freezer_should_skip(p)) // ----------- step 1
return false;
spin_lock_irqsave(&freezer_lock, flags);
if (!freezing(p) || frozen(p)) // --------step 2
spin_unlock_irqrestore(&freezer_lock, flags);
return false;
if (!(p->flags & PF_KTHREAD))
fake_signal_wake_up(p); // --------- step 3
else
wake_up_state(p, TASK_INTERRUPTIBLE); // -------- step 4
spin_unlock_irqrestore(&freezer_lock, flags);
return true;
- 1.使用freezer_should_skip函数来判断该进程是否属于PF_FREEZER_SKIP类型,是否需要跳过冻结操作
- 2.使用freezing来判断是否要对该进程进行冻结,其中会判断pm_freezing和pm_nosig_freezing全局变量来判断对于内核进程是否也要执行冻结操作,frozen函数用来判断该进程是否是已经处于冻结状态了,如果已经冻结自然不需要重复发送冻结信号。
- 3.运行到这里,说明经过前面的条件判断,确定需要对该进程发送冻结请求,PF_KTHREAD用来区分是否是内核线程,如果不是内核线程,那么表示该进程为用户态进程,此时发送一个虚假的信号去唤醒该进程,借助于进程的信号处理机制来处理进程冻结操作。
- 4.如果当前进程是内核线程,此时需要使用TASK_INTERRUPTIBLE唤醒该内核线程,在需要冻结的内核线程中,需要调用try_to_freeze、wait_event_freezable、wait_event_freezable_timeout来处理进程冻结。
用户态进程的冻结
前面介绍到当对一个用户态的进程进行冻结请求时,会发送一个虚假的信号fake_signal_wake_up来唤醒用户态进程处理信号。那么这个流程是怎样的呢?
static void fake_signal_wake_up(struct task_struct *p)
unsigned long flags;
if (lock_task_sighand(p, &flags))
signal_wake_up(p, 0);
unlock_task_sighand(p, &flags);
进一步追溯signal_wake_up函数:
static inline void signal_wake_up(struct task_struct *t, bool resume)
signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
void signal_wake_up_state(struct task_struct *t, unsigned int state)
set_tsk_thread_flag(t, TIF_SIGPENDING); //设置SIGPENDING标记,这样当进程返回用户空间时会先处理信号
/*
* TASK_WAKEKILL also means wake it up in the stopped/traced/killable
* case. We don't check t->state here because there is a race with it
* executing another processor and just now entering stopped state.
* By using wake_up_state, we ensure the process will wake up and
* handle its death signal.
*/
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) //设置进程为TASK_INTERRUPTIBLE状态,并唤醒进程
kick_process(t); //让进程陷入内核态处理信号
/***
* kick_process - kick a running thread to enter/exit the kernel
* @p: the to-be-kicked thread
* Cause a process which is running on another CPU to enter
* kernel-mode, without any delay. (to get signals handled.)
*/
void kick_process(struct task_struct *p)
int cpu;
preempt_disable();
cpu = task_cpu(p); //找到该进程要被运行的cpu
if ((cpu != smp_processor_id()) && task_curr(p))
smp_send_reschedule(cpu);// 发送CPU间的IPI中断请求
preempt_enable();
void smp_send_reschedule(int cpu)
BUG_ON(cpu_is_offline(cpu));
smp_cross_call_common(cpumask_of(cpu), IPI_RESCHEDULE);
这里需要注意为什么最后要发送一个IPI中断给到需要运行该进程的CPU,因为需要冻结用户态进程,因为该中断会使得用户进程陷入到内核态,处理中断,中断处理完成后都会调用ret_to_user返回用户态继续运行,而在ret_to_user中,也就会返回用户态前会判断是否有pending signal要被处理,这时就可以处理进程冻结了。
->ret_to_user
-->do_notify_resume
--->do_signal
---->get_signal
------>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));
set_freezable会清除本进程的PF_NOFREEZE标志,也就意味着该内核进程可以被冻结。或者:
set_freezable();
while (!kthread_should_stop())
try_to_freeze();
......
while循环在每次运行到try_to_freeze时都会检测调用freezing函数检测一下本进程是否可以被冻结,如果可以就直接进行冻结操作。
static inline bool try_to_freeze_unsafe(void)
might_sleep();
if (likely(!freezing(current)))
return false;
return __refrigerator(false);
static inline bool try_to_freeze(void)
if (!(current->flags & PF_NOFREEZE))
debug_check_no_locks_held();
return try_to_freeze_unsafe();
如果我们想要创建一个需要冻结的内核线程,就需要遵守上面的要求来实现它,否则会导致系统freeze失败从而无法休眠。
参考:
kernel-4.19/Documentation/power/freezing-of-tasks.txt
以上是关于进程冻结(freezing of task)的主要内容,如果未能解决你的问题,请参考以下文章
没有 ui 冻结的 Task.Factory.StartNew 延迟
Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)
在 Task ContinueWith TaskScheduler.FromCurrentSynchronizationContext 的 ShowDialog 中打开表单时,应用程序会冻结