电源管理的调试对于开发需要Suspend to Disk(STD)或者Suspend to Ram(STR)的系统来说,非常必要和重要
由于系统在suspend/resume过程会进行非常复杂的一系列操作,如禁用console、冻结进程等,会导致常规的调试方法难以排查定位问题和跟踪流程
Linux的电源管理框架提供了专门的调试方法,用于方便开发者调试不同类型、不同深度的suspend/resume,本文介绍一些常用的工具和使用方法,并在实际环境中验证
参考文档
- kernel document : /Document/power/basic-pm-debugging.txt
- kernel document : /Document/power/drivers-testing.txt
- kernel document : /Document/power/s2ram.txt
- ubuntu wiki - DebuggingKernelSuspend
- stackexchange - How to debug a suspend problem?
- inter open source blog - BEST PRACTICE TO DEBUG LINUX* SUSPEND/HIBERNATE ISSUES
测试环境
本测试在ubuntu上使用qemu创建arm虚拟机环境,linux源码版本4.0,编写内核模块qksleep_test用于调试,关于qemu调试arm kernel参见文章Qemu+gdb调试内核
- 宿主机 : 18.04.1-Ubuntu x86_64
- 虚拟机 : qemu-system-arm vexpress-a9
- kernel : linux_4.0
qksleep_test
源码下载
pm-debugging.rar
qksleep_test以platform_driver方式向系统注册驱动,在其pm
操作域上挂接私有的suspend/resume函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static int qksleep_suspend(struct device *dev)
{
qksleep_dev_t *qkdev = get_qkdev();
qksleep_dev_priv *priv = &qkdev->priv_data;
qksleep_debug("qksleep suspend in");
if (priv->suspend_wakeup_timeout > 0)
schedule_delayed_work(&priv->suspend_wakeup, msecs_to_jiffies(priv->suspend_wakeup_timeout));
if (priv->suspend_errlock)
qksleep_lockerr(priv);
if (priv->suspend_timeout)
qksleep_vsleep(priv, priv->suspend_timeout);
return priv->suspend_ret;
}
static int qksleep_resume(struct device dev)
{
qksleep_dev_t qkdev = get_qkdev();
qksleep_dev_priv *priv = &qkdev->priv_data;
qksleep_debug("qksleep resume in");
if (priv->resume_errlock)
qksleep_lockerr(priv);
if (priv->resume_timeout)
qksleep_vsleep(priv, priv->resume_timeout);
return priv->resume_ret;
}
static struct dev_pm_ops qk_sleep_pm = {
.suspend = qksleep_suspend,
.resume = qksleep_resume,
};
static struct platform_driver qksleep_driver = {
.driver = {
.name = QK_SLEEP_DRV_NAME,
.pm = &qk_sleep_pm,
.owner = THIS_MODULE,
},
.probe = qksleep_probe,
.remove = qksleep_remove,
};
1 | 在platform_driver的probe函数中,向系统注册了7个sysfs节点 |
- suspend_wakeup_timeout:suspend后多久唤醒系统
- suspend_ret:suspend函数返回值
- suspend_timeout:suspend函数中模拟一个超时时间
- suspend_errlock:suspend函数中模拟一个错误的锁操作
- resume_xxx:同上
编译kernel,并用quem启动后,在/sys/devices/platform/qksleep路径下生成了该设备的所有sysfs节点
1 | / # cd /sys/devices/platform/qksleep/ |
查看suspend/resume操作默认值
1 | /sys/devices/platform/qksleep # cat suspend_* |
执行以下命令可控制qksleep设备suspend 2秒后唤醒
1 | echo 20000 > suspend_wakeup_timeout |
执行以下命令可控制qksleep设备suspend函数返回错误值-1
1 | echo -1 > suspend_ret |
执行以下命令可控制qksleep设备在suspend函数中模拟一个死锁
1 | echo 1 > suspend_errlock |
执行以下命令可控制qksleep设备在suspend函数中模拟一个2秒的超时
1 | echo 2000 > suspend_timeout |
resume同suspend
操作/sys/power/state节点,手动进入休眠状态,qksleep默认会在10秒后唤醒系统
1 | /sys/devices/platform/qksleep # echo mem > /sys/power/state |
后续的调试验证将在qksleep设备节点的基础上来做
调试工具
kernel的电源管理框架在“/sys/power”目录下创建了一系列供用户空间操作的sysfs节点,kernel文档“/Document/power/basic-pm-debugging.txt”中详细说明了如何将pm_test
节点用于调试suspend/resume过程。“/Document/power/s2ram.txt”文档介绍了使用“s2ram”工具来调试和排查suspend/resume问题。stackexchange问题“How to debug a suspend problem?”的回答中介绍了“pm_utils”工具。英特尔开源社区的文章“BEST PRACTICE TO DEBUG LINUX* SUSPEND/HIBERNATE ISSUES”中系统的介绍了系统级的调试方法和遇到问题的排查步骤
总的来说,Linux电源管理的调试工具主要分为3大类:系统级调试工具(system debug tools)、PM专用调试方法(pm debug tools)和应用层开发的工具(application tools)
- system debug tools
主要是一些kernel启动参数的控制,用于增加更多打印信息- initcall_debug
- no_console_suspend
- ignore_loglevel
- pm debug tools
PM创建的sysfs节点- pm_test
- pm_trace
- pm_async
- applaction tools
结合PM sysfs编写的PM调试app- pm_utils
- s2ram
- analyze_suspend.py
initcall_debug
通过将initcall_debug
作为启动参数传入kernel,可以跟踪kernel的initcalls和驱动在boot、suspend和resume时的调用情况。通过这种方式可以追踪由于特定组件或驱动引起的suspend/resume问题
验证
在qksleep的resume过程设置2秒的超时
1 | /sys/devices/platform/qksleep # echo 2000 > resume_timeout |
手动进入休眠,系统唤醒后可看到suspend过程耗时0.010秒,resume操作耗时2.020秒
1 | /sys/devices/platform/qksleep # echo mem > /sys/power/state |
虽然能看出来系统resume过程耗时明显过长,但是无法知道是在哪里耗时过长,尝试用initcall_debug
来查看。系统启动时传入参数initcall_debug
,开启该参数后,启动虚拟机,会打印所有initcall信息
1 | ... |
设置resume超时2秒,手动进入休眠
1 | /sys/devices/platform/qksleep # echo 2000 > resume_timeout |
明显可以看出来在qksleep设备的resume过程耗时1950845微秒
1 | [ 42.269209] call qksleep+ returned 0 after 1950845 usecs |
no_console_suspend
默认的kernel休眠过程中会禁用console,因此suspend的任何打印只有在系统唤醒以后才能看到,对于suspend这部分的执行过程,kernel是不会输出的。而开启no_console_suspend
选项,可以让kernel进入suspend的过程,仍然输出打印
no_console_suspend的控制位于/kernel/power/suspend.c和/kernel/printk.c文件中,函数console_suspend_disable
用于处理kernel参数no_console_suspend,当设置了该参数后,全局变量console_suspend_enabled
的值为false
1 | static int __init console_suspend_disable(char *str) |
kernel休眠时会调用suspend_devices_and_enter
来执行设备的休眠流程,在其中会调用suspend_console
来执行console的suspend
1 | /** |
如果console_suspend_enabled
为真,则会执行下面的锁定console操作,此后所有printk
的打印信息不会立刻打印到终端;如果为否,直接退出,此后printk
函数的打印信息不受影响
no_console_suspend
参数比较适用于要跟踪调试kernel代码的情况,且适用于suspend后唤醒不正常的情况
验证
qksleep的resume设置模拟死锁,并分别在带no_console_suspend
参数和不带参数情况下调试
qksleep的resume设置死锁
1 | /sys/devices/platform/qksleep # echo 1 > resume_errlock |
不带no_console_suspend
情况下,手动进入休眠,打印在“Suspending console(s) (use no_console_suspend to debug)”处停止
1 | /sys/devices/platform/qksleep # echo mem > /sys/power/state |
带no_console_suspend
的情况下,手动进入休眠,死锁前的打印都能看到
1 | /sys/devices/platform/qksleep # echo mem > /sys/power/state |
ignore_loglevel
开启ignore_loglevel
参数后,kernel的打印会无视log级别限制,所有打印都能看到,适用于代码中有很多log级别区分的调试
pm_test
该功能依赖kernel配置宏CONFIG_PM_DEBUG
,在make menuconfig中配置路径为
1 | make menuconfig |
开启此配置后,可通过写入/sys/power/pm_test节点让PM core以测试模式运行,测试模式有5个级别,对应于不同深度的休眠
- freezer:测试进程冻结
- devices:测试进程冻结和设备suspend
- platform:测试进程冻结、设备suspend、平台架构相关suspend
- processors:测试进程冻结、设备suspend、平台架构相关suspend、禁用非引导CPU
- core:测试进程冻结、设备suspend、平台架构相关suspend、禁用非引导CPU、系统suspend
这5个级别由浅入深,正好对应kernel的休眠流程,kernel在不同地方都设置了测试点suspend_test
,当设置了测试模式后,对应级别的测试点会delay 5秒钟然后唤醒系统
1 | static int suspend_test(int level) |
使用这种方法能够在不添加额外唤醒源的情况下测试suspend/resume的完整流程,且能够分阶段进行调试
验证
禁用qksleep原本的suspend唤醒
1 | /sys/devices/platform/qksleep # echo 0 > suspend_wakeup_timeout |
设置PM core调试模式为devices
1 | /sys/devices/platform/qksleep # echo devices > /sys/power/pm_test |
设置qksleep设备resume返回错误值-1
1 | /sys/devices/platform/qksleep # echo -1 > resume_ret |
手动进入休眠,可看到kernel在5秒后自动唤醒了
1 | /sys/devices/platform/qksleep # echo mem > /sys/power/state |
/sys/kernel/debug/wakeup_source
该节点列举了当前系统中所有唤醒源以及他们的状况
1 | /sys/kernel/debug # cat wakeup_sources |
- name:唤醒源的驱动名称
- active_count:wake lock 活跃次数
- event_count:唤醒源唤醒事件次数
- wakeup_count:唤醒源强制设备唤醒的次数
- expire_count:唤醒源已到期次数
- active_since:唤醒源处于活跃状态的时间(以jiffies时间为单位)
- total_time:唤醒源活跃的总时间(以jiffies时间为单位)
- max_time:唤醒源持续活跃的最长时间
- last_change:上次更改唤醒源为活跃的时间戳
- prevent_suspend_time:如果没有这个唤醒源,系统进入suspend可以节省多少时间。这对于计算对电池寿命的影响特别有用
验证
首次启动系统,查看该节点,所有唤醒源均未唤醒过系统
1 | / # cat /sys/kernel/debug/wakeup_sources |
手动进入休眠,待系统唤醒后,查看唤醒源
1 | / # echo mem > /sys/power/state |
可看到是qksleep驱动唤醒了系统