Linux电源管理7(基于Linux6.6)---Wakeup count介绍
一、概述
在 Linux 内核中,wakeup count(唤醒计数)是与进程调度、睡眠管理和 CPU 资源调度相关的一个概念。它用于跟踪和管理进程进入睡眠状态和唤醒状态之间的转换,确保在正确的时机唤醒进程并让系统资源得到合理的分配。具体来说,wakeup count 主要涉及以下几个方面:
1. 唤醒计数的作用
-
跟踪进程的睡眠状态:当进程因等待某些事件(如 I/O 操作完成、信号接收、定时器过期等)而进入睡眠状态时,内核需要跟踪该进程等待的唤醒事件。wakeup count 就是用来记录这些唤醒事件的数量。
-
唤醒机制的管理:每当一个唤醒事件发生时,系统会递减对应进程的 wakeup count,直到所有相关的事件都触发,从而唤醒该进程。wakeup count 为 0 时,表示该进程可以被唤醒并继续执行。
2. 内核中的应用场景
wakeup count 在以下场景中起到了重要作用:
-
睡眠与唤醒的同步:Linux 内核中有很多机制,如
wait queues
(等待队列),用来管理进程的睡眠与唤醒。当进程在某些条件下进入睡眠状态时,系统会记录与该进程相关的唤醒事件。这些事件触发时会依次减少唤醒计数,最终唤醒进程。 -
高效管理唤醒事件:在多核系统中,唤醒事件的处理必须高效。如果一个进程等待多个事件(比如 I/O 操作、定时器),那么它的 wakeup count 就是这些事件的总和。内核必须确保所有事件完成后,进程才能真正唤醒并恢复执行。
-
节能与调度优化:wakeup count 也有助于内核进行节能优化。进程在等待事件时可能处于低功耗模式,内核会根据唤醒事件的数量来决定什么时候唤醒进程,避免不必要的频繁唤醒和过度消耗系统资源。
3. 内核实现细节
在 Linux 内核中,wakeup count 主要通过 wait queues
和相关的唤醒机制来实现:
-
Wait Queue:当一个进程因等待某个事件而进入睡眠时,它会被放入一个等待队列中。每个等待队列的事件都会增加该队列中的进程的唤醒计数。当某个事件完成时,内核会从等待队列中唤醒相应的进程并减少唤醒计数。
-
wakeup_count 变量:每个进程可能会有一个关联的唤醒计数,表示它需要多少个唤醒事件才能恢复执行。在内核内部,wakeup count 的更新通常与事件发生时触发的回调函数相关。
4. 与 saved_count
的关系
在一些内核实现中,wakeup count 与 saved_count
(保存计数)共同作用:
-
saved_count:当进程进入睡眠状态时,它的
saved_count
用于记录它当前在等待的事件数量。此值可能是wakeup_count
的一个副本。当事件发生并且进程被唤醒时,saved_count
会被检查,确认进程是否需要等待更多的唤醒事件。 -
相互配合:当进程因为多个事件而进入睡眠时,内核需要确保所有事件完成后才唤醒进程。
wakeup_count
和saved_count
用于确保这一点,避免唤醒不完整或不必要的进程。
Wakeup count是Wakeup events framework的组成部分,用于解决“system suspend和system wakeup events之间的同步问题”。
二、wakeup count在电源管理中的位置
wakeup count的实现位于wakeup events framework中(drivers/base/power/wakeup.c),主要为两个模块提供接口:通过PM core向用户空间提供sysfs接口;直接向autosleep提供接口。
三、 wakeup count的功能
wakeup count的功能是suspend同步,实现思路是这样的:
下面是关于如何通过 wakeup count 实现 suspend 同步 的思路和实现细节:
3.1、问题背景:
在 Linux 内核中,当一个进程需要挂起(比如进入低功耗状态或者等待特定事件时),内核需要确保它没有任何未完成的任务或事件依赖。否则,如果在进程挂起后这些任务或事件仍然发生,可能会导致资源不一致或者在进程恢复时需要重新同步状态。
为了实现同步,内核利用 wakeup count 来追踪等待的事件。只有在所有相关的事件都已完成并触发了唤醒,内核才会允许进程挂起或进入低功耗状态。
3.2、实现思路:
a. 进程进入挂起状态时,设置 wakeup count:
当一个进程需要挂起时(比如进入睡眠状态或者等待某个事件),内核会设置 wakeup count,表示该进程依赖于多少个外部事件才能真正进入挂起状态。每个进程会记录其待处理的事件数量,这些事件可能包括:
- I/O 操作完成
- 超时事件(如定时器过期)
- 外部信号或条件(如某个标志被设定)
b. 事件发生时更新 wakeup count:
当某个事件发生(如 I/O 操作完成、定时器到期等),内核会减少进程的 wakeup count。每触发一次事件,wakeup count
减少一次。
c. 挂起条件的判断:
当进程处于等待挂起的状态时,内核会检查进程的 wakeup count 是否已经为 0。只有当所有相关的事件都发生,并且所有的 wakeup count 都被减少为 0 时,内核才会允许进程进入挂起状态或进行其他类似操作。
d. 进程恢复(唤醒)时,恢复 wakeup count:
如果进程从挂起状态恢复过来,内核会根据需要重新设置进程的 wakeup count,以便它可以继续等待新的事件或任务。
3.3、同步实现的细节:
Wait Queue 和 Wakeup Count 结合:
内核通常通过 wait queues 来管理进程的睡眠和唤醒。当进程因某个条件而需要进入挂起或等待状态时,它会被放入一个 wait queue,并且其 wakeup count 会被设置为该进程需要等待的事件数量。例如:
- 如果进程等待一个 I/O 操作的完成,它的 wakeup count 就是 1,表示等待一个事件。
- 如果进程等待多个事件(例如多个 I/O 操作完成),则 wakeup count 可能是多个。
触发事件时的操作:
当外部事件发生(如 I/O 操作完成、信号到达等),内核会通知等待的进程,并且减少该进程的 wakeup count。如果 wakeup count 减少到 0,进程就会被从等待队列中移除并唤醒。
在事件触发时,内核会按照以下步骤操作:
- 检查进程的 wakeup count,如果事件满足条件,减少相应的计数。
- 如果 wakeup count 已经为 0,唤醒该进程并恢复其执行。
高效的同步控制:
为了确保多事件的同步和高效的挂起控制,内核可能会使用以下技术:
- 原子操作:对 wakeup count 的更新通常使用原子操作(例如
atomic_add
和atomic_sub
),以确保在多核系统上同步操作的安全性。 - 延迟唤醒:如果某个事件尚未触发,内核可能会让进程在某个合理的时机恢复检查,以避免过度的 CPU 占用。
- 事件批量处理:当多个事件同时发生时,内核可以批量更新多个进程的 wakeup count,提高效率。
3.4、具体应用:
-
挂起/恢复机制:对于一些长时间处于等待状态的进程,如 I/O 等待进程,内核通过 wakeup count 来确保在所有依赖事件完成前,进程不会进入挂起状态。只有当进程的所有依赖事件都完成时,才能挂起或者进行电源管理。
-
节能和调度优化:利用 wakeup count,系统可以精确地调度进程的挂起和唤醒,以确保系统资源得到合理利用,尤其是在大规模并发环境和多核系统中。
通过 wakeup count,内核能够精确跟踪和管理进程等待的事件数量。只有当所有相关的事件都已完成时,内核才允许进程挂起或恢复。这种机制能够确保:
- 进程在所有依赖事件都处理完毕时才进入挂起状态。
- 系统在多任务和高并发环境下有效地进行进程同步和资源管理。
四、 wakeup count的实现逻辑
4.1、举例
在进行代码分析之前,先用伪代码的形式,写出一个利用wakeup count进行suspend操作的例子,然后基于该例子,分析相关的实现。
do {
ret = read(&cnt, "/sys/power/wakeup_count");
if (ret) {
ret = write(cnt, "/sys/power/wakeup_count");
} else {
countine;
}
} while (!ret);
write("mem", "/sys/power/state");
/* goto here after wakeup */
说明:
a)读取wakeup count值,如果成功,将读取的值回写。否则说明有正在处理的wakeup events,continue。
b)回写后,判断返回值是否成功,如果不成功(说明读、写的过程中产生了wakeup events),继续读、写,直到成功。成功后,可以触发电源状态切换。
4.2、 /sys/power/wakeup_count
wakeup_count文件是在kernel/power/main.c中,利用power_attr注册的,如下:
static ssize_t wakeup_count_show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
unsigned int val;
return pm_get_wakeup_count(&val, true) ?
sprintf(buf, "%u\n", val) : -EINTR;
}
static ssize_t wakeup_count_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t n)
{
unsigned int val;
int error;
error = pm_autosleep_lock();
if (error)
return error;
if (pm_autosleep_state() > PM_SUSPEND_ON) {
error = -EBUSY;
goto out;
}
error = -EINVAL;
if (sscanf(buf, "%u", &val) == 1) {
if (pm_save_wakeup_count(val))
error = n;
else
pm_print_active_wakeup_sources();
}
out:
pm_autosleep_unlock();
return error;
}
power_attr(wakeup_count);
实现很简单:read时,直接调用pm_get_wakeup_count(注意第2个参数);write时,直接调用pm_save_wakeup_count(注意用户空间的wakeup count功能和auto sleep互斥。这两个接口均是wakeup events framework提供的接口,跟着代码往下看吧。
4.3、pm_get_wakeup_count
pm_get_wakeup_count的实现如下:
drivers/base/power/wakeup.c
bool pm_get_wakeup_count(unsigned int *count, bool block)
{
unsigned int cnt, inpr;
if (block) {
DEFINE_WAIT(wait);
for (;;) {
prepare_to_wait(&wakeup_count_wait_queue, &wait,
TASK_INTERRUPTIBLE);
split_counters(&cnt, &inpr);
if (inpr == 0 || signal_pending(current))
break;
pm_print_active_wakeup_sources();
schedule();
}
finish_wait(&wakeup_count_wait_queue, &wait);
}
split_counters(&cnt, &inpr);
*count = cnt;
return !inpr;
}
该接口有两个参数,一个是保存返回的count值得指针,另一个指示是否block,具体请参考代码逻辑:
a)如果block为false,直接读取registered wakeup events和wakeup events in progress两个counter值,将registered wakeup events交给第一个参数,并返回wakeup events in progress的状态(若返回false,说明当前有wakeup events正在处理,不适合suspend)。
b)如果block为true,定义一个等待队列,等待wakeup events in progress为0,再返回counter。
4.4、pm_save_wakeup_count
pm_save_wakeup_count的实现如下:
drivers/base/power/wakeup.c
bool pm_save_wakeup_count(unsigned int count)
{
unsigned int cnt, inpr;
unsigned long flags;
events_check_enabled = false;
raw_spin_lock_irqsave(&events_lock, flags);
split_counters(&cnt, &inpr);
if (cnt == count && inpr == 0) {
saved_count = count;
events_check_enabled = true;
}
raw_spin_unlock_irqrestore(&events_lock, flags);
return events_check_enabled;
}
1)注意这个变量,events_check_enabled,如果它不为真,pm_wakeup_pending接口直接返回false,意味着如果不利用wakeup count功能,suspend过程中不会做任何wakeup events检查,也就不会进行任何的同步。
2)解除当前的registered wakeup events、wakeup events in progress,保存在变量cnt和inpr中。
3)如果写入的值和cnt不同(说明读、写的过程中产生events),或者inpr不为零(说明有events正在被处理),返回false(说明此时不宜suspend)。
4)否则,events_check_enabled置位(后续的pm_wakeup_pending才会干活),返回true(可以suspend),并将当前的wakeup count保存在saved count变量中。
4.5、/sys/power/state
再回忆一下中suspend的流程,在suspend_enter接口中,suspend前的最后一刻,会调用pm_wakeup_pending接口,代码如下:
kernel/power/suspend.c
static int suspend_enter(suspend_state_t state, bool *wakeup)
{
int error;
error = platform_suspend_prepare(state);
if (error)
goto Platform_finish;
error = dpm_suspend_late(PMSG_SUSPEND);
if (error) {
pr_err("late suspend of devices failed\n");
goto Platform_finish;
}
error = platform_suspend_prepare_late(state);
if (error)
goto Devices_early_resume;
error = dpm_suspend_noirq(PMSG_SUSPEND);
if (error) {
pr_err("noirq suspend of devices failed\n");
goto Platform_early_resume;
}
error = platform_suspend_prepare_noirq(state);
if (error)
goto Platform_wake;
if (suspend_test(TEST_PLATFORM))
goto Platform_wake;
if (state == PM_SUSPEND_TO_IDLE) {
s2idle_loop();
goto Platform_wake;
}
error = pm_sleep_disable_secondary_cpus();
if (error || suspend_test(TEST_CPUS))
goto Enable_cpus;
arch_suspend_disable_irqs();
BUG_ON(!irqs_disabled());
system_state = SYSTEM_SUSPEND;
error = syscore_suspend();
if (!error) {
*wakeup = pm_wakeup_pending();
if (!(suspend_test(TEST_CORE) || *wakeup)) {
trace_suspend_resume(TPS("machine_suspend"),
state, true);
error = suspend_ops->enter(state);
trace_suspend_resume(TPS("machine_suspend"),
state, false);
} else if (*wakeup) {
error = -EBUSY;
}
syscore_resume();
}
system_state = SYSTEM_RUNNING;
arch_suspend_enable_irqs();
BUG_ON(irqs_disabled());
Enable_cpus:
pm_sleep_enable_secondary_cpus();
Platform_wake:
platform_resume_noirq(state);
dpm_resume_noirq(PMSG_RESUME);
Platform_early_resume:
platform_resume_early(state);
Devices_early_resume:
dpm_resume_early(PMSG_RESUME);
Platform_finish:
platform_resume_finish(state);
return error;
}
在write wakeup_count到调用pm_wakeup_pending这一段时间内,wakeup events framework会照常产生wakeup events,因此如果pending返回true,则不能“enter”,终止suspend吧!等待wakeup。