芯片(SoC)上系统的电源状态有:on、idle和suspend。On表示SoC正在运行。Idle是一种中等功率模式,在这种模式下,SoC是通电的,但不执行任何任务。Suspend是一种低功耗模式,其中SoC没有供电。在此模式下,设备的功耗通常比“On” 模式低100倍。
SystemSuspend 服务
在 Android 9 及更低版本中,libsuspend 中有一个负责发起系统挂起的线程。Android 10 在 SystemSuspend HIDL 服务中引入了等效功能。此服务位于系统映像中,由 Android 平台提供。 libsuspend 的逻辑基本保持不变,以下情况除外:阻止系统挂起的每个用户空间进程都需要与 SystemSuspend 进行通信。
libsuspend 和 libpower
在 Android 10 中,SystemSuspend 服务取代了 libsuspend。libpower 经过重新实现,目前依赖于 SystemSuspend 服务(而不是 /sys/power/wake[un]lock),且不必更改 C API。
下面的伪代码显示了如何实现 acquire_wake_lock 和 release_wake_lock。
// hardware/libhardware_legacy/power.cpp
static std::unordered_map<std::string, sp<IWakeLock>> gWakeLockMap;
int acquire_wake_lock(int, const char* id) {
...
if (!gWakeLockMap[id]) {
gWakeLockMap[id] = suspendService->acquireWakeLock(WakeLockType::PARTIAL, id);
}
...
return 0;
}
int release_wake_lock(const char* id) {
...
if (gWakeLockMap[id]) {
auto ret = gWakeLockMap[id]->release();
gWakeLockMap[id].clear();
return 0;
}
...
return -1;
}
执行线程
system/hardware/interfaces/suspend/1.0/default/SystemSuspend.cpp
SystemSuspend 服务使用挂起计数器跟踪发出的唤醒锁数量。它有两个执行线程:
- 主线程响应 binder 调用。
- 挂起线程控制系统挂起。
主线程
主线程响应来自客户端的请求以分配新的唤醒锁,从而递增/递减挂起计数器。
挂起线程
挂起线程循环执行以下操作:
- 从 /sys/power/wakeup_count 读取
- 获取互斥量。这可确保挂起线程在主线程尝试递增或递减挂起计数器时不会触发挂起计数器。如果挂起计数器达到零,并且挂起线程尝试运行,则系统会在发出或移除唤醒锁定时阻止主线程。
- 等到计数器等于零。
- 将从 /sys/power /wakeup_count(第 1 步中)读取的值写入此文件。 如果写入失败,则返回到循环的开头。(这一步很重要相当于events_check_enabled,这也是android suspend和直接echo mem > sys/power/state的不同之处,events_check_enabled之后在suspend的过程中只要有wakeup_source_report_event,就会导致wakeup_count++,就会导致cnt != saved_count,进而导致"Wakeup pending, aborting suspend")。
// 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;
}
static void wakeup_source_report_event(struct wakeup_source *ws, bool hard)
{
ws->event_count++;
/* This is racy, but the counter is approximate anyway. */
if (events_check_enabled)
ws->wakeup_count++;
if (!ws->active)
wakeup_source_activate(ws);
if (hard)
pm_system_wakeup();
}
bool pm_wakeup_pending(void)
{
unsigned long flags;
bool ret = false;
char suspend_abort[MAX_SUSPEND_ABORT_LEN];
raw_spin_lock_irqsave(&events_lock, flags);
if (events_check_enabled) {
unsigned int cnt, inpr;
split_counters(&cnt, &inpr);
ret = (cnt != saved_count || inpr > 0);
events_check_enabled = !ret;
}
raw_spin_unlock_irqrestore(&events_lock, flags);
if (ret) {
pm_pr_dbg("Wakeup pending, aborting suspend\n");
pm_print_active_wakeup_sources();
pm_get_active_wakeup_sources(suspend_abort,
MAX_SUSPEND_ABORT_LEN);
log_suspend_abort_reason(suspend_abort);
pr_info("PM: %s\n", suspend_abort);
}
return ret || atomic_read(&pm_abort_suspend) > 0;
}
如果(cnt != saved_count || inpr > 0) == true, 你想知道哪里触发的。你需要combined_event_count怎么改变的,因为cnt和inpr的值来自combined_event_count。总之你如下增加log
diff --git a/drivers/base/power/wakeup.c b/drivers/base/power/wakeup.c
index addbd42930bf8..6f089700a6087 100644
--- a/drivers/base/power/wakeup.c
+++ b/drivers/base/power/wakeup.c
@@ -591,7 +596,11 @@ static void wakeup_source_report_event(struct wakeup_source *ws, bool hard)
ws->wakeup_count++;
if (!ws->active)
+ {
+ pr_info("Wakeup source: %s\n", ws->name);
+ dump_stack();
wakeup_source_activate(ws);
+ }
if (hard)
pm_system_wakeup();
你得到如下的log,因此知道cnt和inpr的值的更改和cdns_hdmi_bridge_disable有关。
[42667.761490][ T2207] PM: Wakeup source: event2
[42667.761496][ T2207] CPU: 2 PID: 2207 Comm: binder:218_4 Tainted: G W OE 6.6.0-gb2ac2263d6bd-dirty #6
[42667.761504][ T2207] Hardware name: NXP i.MX8MQ EVK (DT)
[42667.761508][ T2207] Call trace:
[42667.761510][ T2207] dump_backtrace+0xe8/0x10c
[42667.761528][ T2207] show_stack+0x18/0x28
[42667.761536][ T2207] dump_stack_lvl+0x50/0x6c
[42667.761545][ T2207] wakeup_source_report_event+0x94/0x1e8
[42667.761555][ T2207] __pm_stay_awake+0x38/0x68
[42667.761562][ T2207] ep_poll_callback+0x17c/0x34c
[42667.761573][ T2207] __wake_up_common+0xe8/0x188
[42667.761584][ T2207] __wake_up+0x7c/0xd0
[42667.761592][ T2207] evdev_pass_values+0x208/0x220
[42667.761603][ T2207] evdev_events+0x90/0xb0
[42667.761610][ T2207] input_pass_values+0x1f8/0x3a4
[42667.761618][ T2207] input_handle_event+0x498/0x4a8
[42667.761624][ T2207] input_event+0x60/0x88
[42667.761630][ T2207] snd_jack_report+0x24c/0x2ac
[42667.761640][ T2207] snd_soc_jack_report+0xe8/0x210
[42667.761650][ T2207] plugged_cb+0x98/0xd0
[42667.761657][ T2207] cdns_hdmi_bridge_disable+0x4c/0x60
[42667.761666][ T2207] drm_atomic_bridge_chain_disable+0xb0/0xe8
[42667.761676][ T2207] drm_atomic_helper_commit_modeset_disables+0x18c/0x66c
[42667.761684][ T2207] drm_atomic_helper_commit_tail_rpm+0x30/0x1f0
[42667.761691][ T2207] commit_tail+0xbc/0x170
[42667.761697][ T2207] drm_atomic_helper_commit+0x2cc/0x2f0
[42667.761704][ T2207] drm_atomic_commit+0xbc/0xec
[42667.761710][ T2207] drm_atomic_helper_disable_all+0x160/0x1e0
[42667.761717][ T2207] drm_atomic_helper_suspend+0xb8/0x1b8
[42667.761724][ T2207] drm_mode_config_helper_suspend+0x30/0x74
[42667.761735][ T2207] dcss_dev_suspend+0x2c/0x78
[42667.761744][ T2207] platform_pm_suspend+0x40/0x88
[42667.761754][ T2207] dpm_run_callback+0x64/0x258
[42667.761761][ T2207] __device_suspend+0x284/0x464
[42667.761768][ T2207] dpm_suspend+0x100/0x508
[42667.761775][ T2207] dpm_suspend_start+0x84/0xc0
[42667.761783][ T2207] suspend_devices_and_enter+0x128/0xadc
[42667.761792][ T2207] pm_suspend+0x2ec/0x6a8
[42667.761798][ T2207] state_store+0x104/0x140
[42667.761804][ T2207] kobj_attr_store+0x30/0x48
[42667.761813][ T2207] sysfs_kf_write+0x54/0x6c
[42667.761820][ T2207] kernfs_fop_write_iter+0x104/0x1a4
[42667.761832][ T2207] vfs_write+0x1f0/0x2e4
[42667.761842][ T2207] ksys_write+0x78/0xe8
[42667.761850][ T2207] __arm64_sys_write+0x1c/0x2c
[42667.761858][ T2207] invoke_syscall+0x58/0x114
[42667.761867][ T2207] el0_svc_common+0xac/0xe8
[42667.761877][ T2207] do_el0_svc+0x1c/0x28
[42667.761885][ T2207] el0_svc+0x38/0xb0
[42667.761892][ T2207] el0t_64_sync_handler+0x68/0xbc
[42667.761899][ T2207] el0t_64_sync+0x1a8/0x1ac
[42667.763990][ T2207] [drm] Reg val is 0x0080
[42667.868807][ T2207] [drm] hdmi phy shutdown complete
- 通过将 mem 写入 /sys/power/state 来发起系统挂起。
- 释放互斥量。
成功返回唤醒锁定的请求后,挂起线程会被阻止。
注意:互斥量始终在循环迭代结束时释放。
SystemSuspend API
SystemSuspend API 包含两个接口。HIDL 接口由native进程用于获取唤醒锁,而 AIDL 接口则用于在 SystemServer 和 SystemSuspend 之间通信。
ISystemSuspend HIDL 接口
enum WakeLockType : uint32_t {
PARTIAL,
FULL
};
interface IWakeLock {
oneway release();
};
interface ISystemSuspend {
acquireWakeLock(WakeLockType type, string debugName)
generates (IWakeLock lock);
};
请求唤醒锁定的每个客户端都会收到唯一的 IWakeLock 实例。这与 /sys/power/wake_lock 不同,后者允许多个客户端使用相同名称的唤醒锁定。如果拥有 IWakeLock 实例的客户端终止,则 binder 驱动程序和 SystemSuspend 服务会将其清除。
ISuspendControlService AIDL 接口
ISuspendControlService 仅供 SystemServer 使用。
interface ISuspendCallback {
void notifyWakeup(boolean success);
}
interface ISuspendControlService {
boolean enableAutosuspend();
boolean registerCallback(ISuspendCallback callback);
boolean forceSuspend();
}
利用 Android HIDL 可带来以下好处:
- 如果挂起阻止进程终止,则可以通知 SystemSuspend。
- 可以为负责系统挂起的线程提供回调。
查看wakelock状态
dumpsys suspend_control_internal
dumpsys 是一种在 Android 设备上运行的工具,可提供有关系统服务的信息。您可以使用 Android 调试桥 (ADB) 从命令行调用 dumpsys,获取在连接的设备上运行的所有系统服务的诊断输出。
suspend_control_internal这个service的code:
system/hardware/interfaces/suspend/1.0/default/SuspendControlService.cpp
最终可以通过这个节点/sys/class/wakeup来查看wakelock的状态。最终调用在如下code:
system/hardware/interfaces/suspend/1.0/default/main.cpp
申请wakelock
从上图的结果来看,wakelock的type有native或者kernel,native类型的wakelock通过上面的libpower进行申请和释放。kernel类型的wakelock分两种情况:
3. 具体驱动注册wakelock是通过wakeup_source_register注册的。
// drivers/base/power/wakeup.c
struct wakeup_source *wakeup_source_register(struct device *dev,
const char *name)
{
struct wakeup_source *ws;
int ret;
ws = wakeup_source_create(name);
if (ws) {
if (!dev || device_is_registered(dev)) {
ret = wakeup_source_sysfs_add(dev, ws);
if (ret) {
wakeup_source_free(ws);
return NULL;
}
}
wakeup_source_add(ws);
}
return ws;
}
EXPORT_SYMBOL_GPL(wakeup_source_register);
- 第二种方式通过epoll机制,userspace的binary 注册一个kernel type的wakelock。例如:
hardware/interfaces/health/aidl/default/android.hardware.health-service.example.rc
// hardware/interfaces/health/aidl/default/android.hardware.health-service.example.rc
service vendor.health-default /vendor/bin/hw/android.hardware.health-service.example
class hal
user system
group system
capabilities WAKE_ALARM BLOCK_SUSPEND
file /dev/kmsg w
health HAL service 有 CAP_BLOCK_SUSPEND的能力,并且使用epoll机制监听可读事件。
// hardware/interfaces/health/utils/libhealthloop/HealthLoop.cpp
int HealthLoop::RegisterEvent(int fd, BoundFunction func, EventWakeup wakeup) {
CHECK(!reject_event_register_);
auto* event_handler =
event_handlers_
.emplace_back(std::make_unique<EventHandler>(EventHandler{this, fd, func}))
.get();
struct epoll_event ev;
ev.events = EPOLLIN;
if (wakeup == EVENT_WAKEUP_FD) ev.events |= EPOLLWAKEUP;
ev.data.ptr = reinterpret_cast<void*>(event_handler);
if (epoll_ctl(epollfd_, EPOLL_CTL_ADD, fd, &ev) == -1) {
KLOG_ERROR(LOG_TAG, "epoll_ctl failed; errno=%d\n", errno);
return -1;
}
return 0;
}
epoll_ctl是syscall。
// fs/eventpoll.c
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
struct epoll_event epds;
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
return -EFAULT;
return do_epoll_ctl(epfd, op, fd, &epds, false);
}
static int ep_create_wakeup_source(struct epitem *epi)
{
struct name_snapshot n;
struct wakeup_source *ws;
if (!epi->ep->ws) {
epi->ep->ws = wakeup_source_register(NULL, "eventpoll");
if (!epi->ep->ws)
return -ENOMEM;
}
take_dentry_name_snapshot(&n, epi->ffd.file->f_path.dentry);
ws = wakeup_source_register(NULL, n.name.name);
release_dentry_name_snapshot(&n);
if (!ws)
return -ENOMEM;
rcu_assign_pointer(epi->ws, ws);
return 0;
}
最终调用到上面的wakeup_source_register,从而申请一个wakelock。同理也可以调用wakeup_source_unregister来removes a wakeup source。
如果你已知一个wakelock的名字但是不清楚是usersapce那个地方设置的wakelock,你可以如下加log:
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 1d9a71a0c4c16..e00e12972479c 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -1413,6 +1413,10 @@ static int ep_create_wakeup_source(struct epitem *epi)
take_dentry_name_snapshot(&n, epi->ffd.file->f_path.dentry);
ws = wakeup_source_register(NULL, n.name.name);
+ if (strcmp(n.name.name, "event2") == 0) {
+ pr_info("6666666666666666666666666666666Name is event2\n");
+ dump_stack();
+ }
release_dentry_name_snapshot(&n);
if (!ws)
log如图下所示:
[ 39.905161][ T733] CPU: 2 PID: 733 Comm: InputReader Tainted: G OE 6.6.0-g09a4dc860532 #15
[ 39.914845][ T733] Hardware name: NXP i.MX8MQ EVK (DT)
[ 39.920100][ T733] Call trace:
[ 39.923253][ T733] dump_backtrace+0xe8/0x10c
[ 39.927723][ T733] show_stack+0x18/0x28
[ 39.931751][ T733] dump_stack_lvl+0x50/0x6c
[ 39.936125][ T733] wakeup_source_register+0x128/0x138
[ 39.941371][ T733] ep_create_wakeup_source+0x7c/0x100
[ 39.946617][ T733] ep_insert+0x2cc/0x5c4
[ 39.950731][ T733] do_epoll_ctl+0x328/0x3dc
[ 39.955105][ T733] __arm64_sys_epoll_ctl+0xfc/0x19c
[ 39.960175][ T733] invoke_syscall+0x58/0x114
[ 39.964638][ T733] el0_svc_common+0x80/0xe8
[ 39.969012][ T733] do_el0_svc+0x1c/0x28
[ 39.973042][ T733] el0_svc+0x38/0xb0
[ 39.976810][ T733] el0t_64_sync_handler+0x68/0xbc
[ 39.981705][ T733] el0t_64_sync+0x1a8/0x1ac
[ 39.988915][ T733] 6666666666666666666666666666666Name is event2
[ 39.995097][ T733] CPU: 2 PID: 733 Comm: InputReader Tainted: G OE 6.6.0-g09a4dc860532 #15
[ 40.004767][ T733] Hardware name: NXP i.MX8MQ EVK (DT)
[ 40.010004][ T733] Call trace:
[ 40.013154][ T733] dump_backtrace+0xe8/0x10c
[ 40.017626][ T733] show_stack+0x18/0x28
[ 40.021657][ T733] dump_stack_lvl+0x50/0x6c
[ 40.026034][ T733] ep_create_wakeup_source+0xf4/0x100
[ 40.031281][ T733] ep_insert+0x2cc/0x5c4
[ 40.035396][ T733] do_epoll_ctl+0x328/0x3dc
[ 40.039770][ T733] __arm64_sys_epoll_ctl+0xfc/0x19c
[ 40.044840][ T733] invoke_syscall+0x58/0x114
[ 40.049303][ T733] el0_svc_common+0x80/0xe8
[ 40.053679][ T733] do_el0_svc+0x1c/0x28
[ 40.057705][ T733] el0_svc+0x38/0xb0
[ 40.061469][ T733] el0t_64_sync_handler+0x68/0xbc
[ 40.066364][ T733] el0t_64_sync+0x1a8/0x1ac
[ 40.179525][ T1] init: processing action (sys.sysctl.extra_free_kbytes=*) from
可以看出进程/线程为T733,然后
$ ps -AT | grep 733
USER PID TID PPID VSZ RSS WCHAN ADDR S CMD
system 617 733 385 19806716 328088 do_epoll_wait 0 S InputReader
可以得出进程/线程的名字为InputReader然后在code中搜索InputReader,找到对应的目录,然后搜索epoll_ctl,你可以看到如下的代码:
frameworks/native/services/inputflinger/reader/EventHub.cpp
struct epoll_event eventItem = {};
eventItem.events = EPOLLIN | EPOLLWAKEUP;
eventItem.data.fd = mINotifyFd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance. errno=%d", errno);
这就找到了在哪注册的wakelock。
suspend/resume kernel的流程
linux提供了这几种suspend: “freeze”, “standby”, “mem” and
“disk”。
- “freeze”: Suspend-To-Idle(ACPI state:S0), 这种状态是通用的、纯软件的、轻量级的、系统休眠状态。它允许通过冻结用户相对于运行时空闲节省更多能量空间并将所有 I/O 设备置于低功耗状态(可能比运行时可用的功率低),这样处理器就可以花更多的时间在闲置状态。
- “standby”: Standby / Power-On Suspend(ACPI state:S1), 这种状态(如果支持)提供适度但真实的节电,同时提供相对低延迟的返回工作系统的转换。 没有操作状态丢失(CPU 保留电源),因此系统可以轻松地从中断处重新启动。
- “mem”:Suspend-to-RAM(ACPI state:S3),此状态(如果支持)可显着节电,因为系统中的所有内容都处于低功耗状态,但内存除外,它应置于自刷新模式以保留其内容。进入上电暂停时执行的所有步骤也在转换到 STR 期间执行。
根据平台功能,可能会发生其他操作。特别是,在 ACPI 系统上,内核将控制权传递给 BIOS(平台固件)作为 STR 转换期间的最后一步,这通常会导致关闭一些不受内核直接控制的低级组件。
系统和设备状态被保存并保存在内存中。所有设备都被挂起
并进入低功耗状态。在许多情况下,所有外设总线在进入STR 时都会断电,因此设备必须能够处理返回“开启”状态的转换。
至少对于 ACPI,STR 需要一些最小的引导代码来恢复从它的系统。在其他平台上也可能是这种情况。
4.“disk”: Suspend-to-disk(ACPI state:S4),此状态提供最大的节能效果,即使在没有低级平台支持电源管理的情况下也可以使用。此
状态的操作类似于 Suspend-to-RAM,但包括将内存内容写入磁盘的最后一步。在恢复时,它被读取并且内存恢复到其挂起前的状态。
一旦内存状态被写入磁盘,系统可能会进入低功耗状态(如 ACPI S4),或者它可能只是断电。供电down 提供更大的节省,并允许此机制在任何系统上工作。然而,进入真正的低功耗状态允许用户触发唤醒事件(例如按下一个键或打开笔记本电脑的盖子)。
内核一般支持最多四种系统睡眠状态,尽管其中三种依赖于平台支持代码来实现每个状态的底层细节。通过以下命令查看你的板子支持哪几种睡眠状态:
# cat sys/power/state
freeze mem disk
这些字符串可以是"mem", “standby”, “freeze"和"disk”,其中后三个总是分别表示上电挂起(如果支持),挂起到空闲和休眠(挂起到磁盘)。
“ mem”字符串的含义由/sys/power/mem_sleep文件控制。它包含代表可用系统suspend模式的字符串,通过将“ mem”写入/sys/power/state触发。这些模式是“s2idle”(Suspend- to - idle)、“shallow”(上电挂起)和“deep”(Suspend- to - ram)。
# cat sys/power/mem_sleep
s2idle [deep]
“s2idle”模式始终可用,而其他模式只有在平台支持的情况下才可用(如果不支持,则代表它们的字符串不存在/sys/power/mem_sleep中)。表示随后使用的suspend模式的字符串用方括号括起来(也就是上面显示的[deep])。写一个/sys/power/mem_sleep中存在的其他字符串导致suspend模式随后使用将其更改为由该字符串表示的一个。
# echo s2idle > sys/power/mem_sleep
# cat sys/power/mem_sleep
[s2idle] deep
也就是说你此时echo mem > /sys/power/state代表进入s2idle模式。
因此,有两种方法可以使系统进入Suspend-To-Idle 睡眠状态。第一个是直接将“freeze”写入/sys/power/state。第二种是将“s2idle”写入/sys/power/mem_sleep,然后将“mem”写入/sys/power/state。类似地,有两种方法可以使系统进入 Power-On Suspend 睡眠状态(在这种情况下写入控制文件的字符串分别是“standby”或“shallow”和“mem” )如果该状态平台支持。反过来,只有一种方法可以使系统进入 Suspend-To-RAM 状态(将“deep”写入/sys/power/mem_sleep,将“mem”写入/sys/power/state)。
默认挂起模式(即不向 /sys/power/mem_sleep 写入任何内容而使用的挂起模式)是"deep"(如果支持挂起到 RAM)或"s2idle",但它可以被kernel command line中"mem_sleep_default" parameter覆盖。
参考文档 |
SystemSuspend service |
dumpsys |
System Power Management Sleep States |