作者简介:
程磊,某手机大厂系统开发工程师,阅码场荣誉总编辑,最大的爱好是钻研Linux内核基本原理。
目录:
一、电源管理框架
1.1 电源状态管理
1.2 省电管理
1.3 电源管理质量
二、睡眠与休眠
2.1 冻结进程
2.2 睡眠流程
2.3 休眠流程
2.4 自动睡眠
三、关机与重启
3.1 用户空间处理
3.2 内核处理
四、CPU动态调频
4.1 CPUFreq Core
4.2 Govener介绍
4.3 Driver介绍
五、CPU休闲
5.1 CPUIdle Core
5.2 决策者介绍
5.3 Driver介绍
六、电源管理质量
6.1 系统级约束
6.2 设备级约束
七、总结回顾
计算机运行在物理世界中,物理世界中的一切活动都需要消耗能量。能量的形式有很多种,如热能、核能、化学能等。计算机消耗的是电能,其来源是电池或者外电源。计算机内部有一个部件叫做电源管理芯片(PMIC),它接收外部的电能,然后转化为不同电压的电流,向系统的各个硬件供电。什么硬件需要多少伏的电压,都是由相应的电气标准规定好了的,各个硬件厂商按照标准生成硬件就可以了。上电的过程是由硬件自动完成的,不需要软件的参与。因为硬件不上电的话,软件也没法运行啊。但是当硬件运行起来之后,软件就可以对硬件的电源状态进行管理了。电源管理的内容包括电源状态管理和省电管理。电源状态管理是对整个系统的供电状态进行管理,内容包括睡眠、休眠、关机、重启等操作。省电管理是因为电能不是免费的,我们应该尽量地节省能源,尤其是对于一些手持设备来说,电能虽然并不昂贵但是却非常珍贵,因为电池的容量非常有限。不过省电管理也不能一味地省电,还要考虑性能问题,在性能与功耗之间达到平衡。
计算机只有开机之后才能使用,但是我们并不是一直都在使用计算机。当我们短时间不使用计算机时,可以把它置入睡眠或者休眠状态,这样可以省电,而且当我们想使用时还可以快速地恢复到可用状态。当我们长时间不使用计算机时,就可以把它关机,这样更省电,当然再使用它时还需要重新开机。有时候我们觉得系统太卡或者系统状态不对的时候,还可以对计算机进行重启,让系统重新恢复到一个干净稳定的状态。
睡眠(Sleep)也叫做Suspend to RAM(STR),挂起到内存。休眠(Hibernate)也叫做Suspend to Disk(STD)。有时候我们会把睡眠叫做挂起(Suspend),但是有时候我们也会把睡眠和休眠统称为挂起(Suspend)。系统睡眠的时候会把系统的状态信息保存到内存,然后内存要保持供电,其它设备都可以断电。系统休眠的时候会把系统的状态信息保存到磁盘,此时整个系统都可以断电,就和关机一样。系统无论睡眠还是休眠,都可以被唤醒。对于睡眠来说很多外设都可以唤醒整个系统,比如键盘。对于休眠来说,就只有电源按钮能唤醒系统了。休眠一方面和睡眠比较像,都保存了系统的状态信息,一方面又和关机比较像,整个系统都断电了。
重启和关机的关系比较密切,重启相当于是关机再开机。二者都是用reboot系统调用来实现的,其参数cmd用来指定是关机还是重启。关机和重启是需要init进程来处理的,无论我们是使用命令还是使用系统的关机按钮还是直接按电源键,事件最终都会被传递给init进程。Init接收到关机或重启命令后,会进行一些保存处理,然后停止所有的服务进程、杀死所有的普通进程,最后调用系统调用reboot进行关机或者重启。
我们不使用电脑时可以进行睡眠、休眠甚至关机来进行省电,但是我们使用电脑时也可以有很多办法来省电。这些省电方法又可以分为两类,使用省电和闲暇省电。闲暇省电是指计算机在宏观上整体上还在使用,但是在微观上局部上有的设备暂时不在使用。使用省电的方法就是动态调频,包括CPU动态调频(CPUFreq)和设备动态调频(DevFreq)。你正在使用着还想要省电,那唯一的方法就是降低频率了。降低频率就会降低性能,所以还要考虑性能,结合当时的负载进行动态调频。闲暇省电的方法就比较多了,包括CPU休闲(CPUIdle)、CPU热插拔(CPU Hotplug)、CPU隔离(Core Isolate)和动态PM(Runtime PM)。CPUIdle指的是当某个CPU上没有进程可调度的时候可以暂时局部关掉这个CPU的电源,从而达到省电的目的,当再有进程需要执行的时候再恢复电源。CPU Hotplug指的是我们可以把某个CPU热移除,然后系统就不会再往这个CPU上派任务了,这个CPU就可以放心地完全关闭电源了,当把这个CPU再热插入之后,就对这个CPU恢复供电,这个CPU就可以正常执行任务了。CPU隔离指的是我们把某个CPU隔离开来,系统不再把它作为进程调度的目标,这样这个CPU就可以长久地进入Idle状态了,达到省电的目的。不过CPU隔离并不是专门的省电机制,我们把CPU隔离之后还可以通过set_affinity把进程专门迁移到这个CPU上,这个CPU还会继续运行。CPU隔离能达到一种介于CPUIdle和CPU热插拔之间的效果。Runtime PM指的是设备的动态电源管理,系统中存在很多设备,但是并不是每种设备都在一直使用,比如相机可能在大部分时间都不会使用,所以我们可以在大部分时间把相机的电源关闭,在需用相机的时候,再给相机供电。
省电管理可以达到省电的目的,但是也会降低系统的性能,包括响应延迟、带宽、吞吐量等。所以内核又提供了一个PM QOS框架,QoS是Quality Of Service(服务质量)。PM QoS框架一面向顾客提供接口,顾客可以通过这些接口对系统的性能提出要求,一面向各种省电机制下发要求,省电机制在省电的同时也要满足这些性能要求。PM QoS的顾客包括内核和进程:对于内核,PM QoS提供了接口函数可以直接调用;对于进程,PM QoS提供了一些设备文件可以让用户空间进行读写。PM QoS对某一项性能指标的要求叫做一个约束,约束分为系统级约束和设备级约束。系统级约束针对的是整个系统的性能要求,设备级约束针对的是某个设备的性能要求。
下面我们画个图总结一下电源管理:
睡眠和休眠的整体过程是相似的,都是暂停系统的运行、保存系统信息、关闭全部或大部分硬件的供电,当被唤醒时的过程正好相反,先恢复供电,然后恢复系统的运行,再恢复之前保存的信息,然后就可以正常使用了。暂停系统运行包括以下操作:同步文件数据到磁盘、冻结几乎所有进程、暂停devfreq和cpufreq、挂起所有设备(调用所有设备的suspend函数)、禁用大部分外设的中断、下线所有非当前CPU。对于睡眠来说,内存是不断电的,所以不用保存信息。对于休眠来说整个系统是要断电的,所以要把很多系统关键信息都保存到swap中。然后系统就可以断电进入睡眠或者休眠状态了。对于睡眠来说有很多外设都可以唤醒系统,对于休眠来说只有电源键能唤醒系统。当系统被唤醒时就开始了恢复操作,睡眠的恢复和休眠的恢复操作是不太一样的。睡眠基本上是上面操作的反操作,休眠是先正常启动,然后在启动的末尾从swap区恢复状态信息。
睡眠和休眠都有冻结进程的流程,我们就先来看一看冻结进程的过程。冻结进程是先冻结普通进程,再冻结内核进程,其中有些特殊进程不冻结,当前进程不冻结。冻结的方法是先把一个全局变量pm_freezing设置为true,然后给每个进程都发送一个伪信号,也就是把所有进程都唤醒。进程唤醒之后会运行,在其即将返回用户空间时会进行信号处理,在信号处理的流程中,会先进行冻结检测,如果发现pm_freezing为true而且当前进程也不是免冻进程,那么就会冻结该进程。冻结方法也很简单,就是把进程的运行状态设置为不可运行,然后调度其它进程。
下面我们看一下冻结的流程,代码进行了极度删减,只保留最关键的部分。
linux-src/kernel/power/process.c
int freeze_processes(void) { pm_freezing = true; try_to_freeze_tasks(true); } static int try_to_freeze_tasks(bool user_only) { for_each_process_thread(g, p) { freeze_task(p) } }
linux-src/kernel/freezer.c
bool freeze_task(struct task_struct *p) { fake_signal_wake_up(p); } 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); } }
linux-src/arch/x86/kernel/signal.c
void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal) { struct ksignal ksig; if (has_signal && get_signal(&ksig)) { /* Whee! Actually deliver the signal. */ handle_signal(&ksig, regs); return; } }
linux-src/kernel/signal.c
bool get_signal(struct ksignal *ksig) { try_to_freeze(); }
linux-src/include/linux/freezer.h
static inline bool try_to_freeze(void) { return try_to_freeze_unsafe(); } static inline bool try_to_freeze_unsafe(void) { if (likely(!freezing(current))) return false; return __refrigerator(false); } static inline bool freezing(struct task_struct *p) { if (likely(!atomic_read(&system_freezing_cnt))) return false; return freezing_slow_path(p); }
linux-src/kernel/freezer.c
bool freezing_slow_path(struct task_struct *p) { if (p->flags & (PF_NOFREEZE | PF_SUSPEND_TASK)) return false; if (test_tsk_thread_flag(p, TIF_MEMDIE)) return false; if (pm_nosig_freezing || cgroup_freezing(p)) return true; if (pm_freezing && !(p->flags & PF_KTHREAD)) return true; return false; } bool __refrigerator(bool check_kthr_stop) { unsigned int save = get_current_state(); for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); was_frozen = true; schedule(); } set_current_state(save); return was_frozen; }
冻结流程并不是一条线执行完成的,分为发送冻结信号把每个进程都唤醒,然后每个进程自己在运行的时候自己把自己冻结了。
下面我们来看一下睡眠流程的代码: linux-src/kernel/power/suspend.c
int pm_suspend(suspend_state_t state) { int error; if (state <= PM_SUSPEND_ON || state >= PM_SUSPEND_MAX) return -EINVAL; pr_info("suspend entry (%s) ", mem_sleep_labels[state]); error = enter_state(state); if (error) { suspend_stats.fail++; dpm_save_failed_errno(error); } else { suspend_stats.success++; } pr_info("suspend exit "); return error; } static int enter_state(suspend_state_t state) { int error; if (sync_on_suspend_enabled) { ksys_sync_helper(); } error = suspend_prepare(state); error = suspend_devices_and_enter(state); return error; } static int suspend_prepare(suspend_state_t state) { int error; trace_suspend_resume(TPS("freeze_processes"), 0, true); error = suspend_freeze_processes(); trace_suspend_resume(TPS("freeze_processes"), 0, false); return error; } int suspend_devices_and_enter(suspend_state_t state) { int error; error = platform_suspend_begin(state); suspend_console(); suspend_test_start(); error = dpm_suspend_start(PMSG_SUSPEND); do { error = suspend_enter(state, &wakeup); } while (!error && !wakeup && platform_suspend_again(state)); Resume_devices: dpm_resume_end(PMSG_RESUME); suspend_test_finish("resume devices"); resume_console(); Close: platform_resume_end(state); pm_suspend_target_state = PM_SUSPEND_ON; return error; }
linux-src/drivers/base/power/main.c
int dpm_suspend_start(pm_message_t state) { ktime_t starttime = ktime_get(); int error; error = dpm_prepare(state); if (error) { suspend_stats.failed_prepare++; dpm_save_failed_step(SUSPEND_PREPARE); } else error = dpm_suspend(state); dpm_show_time(starttime, state, error, "start"); return error; } int dpm_suspend(pm_message_t state) { int error = 0; devfreq_suspend(); cpufreq_suspend(); while (!list_empty(&dpm_prepared_list)) { struct device *dev = to_device(dpm_prepared_list.prev); get_device(dev); error = device_suspend(dev); } return error; }
linux-src/kernel/power/suspend.c
static int suspend_enter(suspend_state_t state, bool *wakeup) { int error; error = platform_suspend_prepare(state); error = dpm_suspend_late(PMSG_SUSPEND); error = platform_suspend_prepare_late(state); error = dpm_suspend_noirq(PMSG_SUSPEND); error = platform_suspend_prepare_noirq(state); error = suspend_disable_secondary_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)) { error = suspend_ops->enter(state); } else if (*wakeup) { error = -EBUSY; } syscore_resume(); } system_state = SYSTEM_RUNNING; arch_suspend_enable_irqs(); BUG_ON(irqs_disabled()); Enable_cpus: suspend_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; }
下面我们来看一下休眠流程的代码:
linux-src/kernel/power/hibernate.c
int hibernate(void) { int error; lock_system_sleep(); pm_prepare_console(); ksys_sync_helper(); error = freeze_processes(); lock_device_hotplug(); error = create_basic_memory_bitmaps(); error = hibernation_snapshot(hibernation_mode == HIBERNATION_PLATFORM); if (in_suspend) { pm_pr_dbg("Writing hibernation image. "); error = swsusp_write(flags); swsusp_free(); if (!error) { power_down(); } } return error; }
linux-src/kernel/power/snapshot.c
int create_basic_memory_bitmaps(void) { struct memory_bitmap *bm1, *bm2; int error = 0; bm1 = kzalloc(sizeof(struct memory_bitmap), GFP_KERNEL); error = memory_bm_create(bm1, GFP_KERNEL, PG_ANY); bm2 = kzalloc(sizeof(struct memory_bitmap), GFP_KERNEL); error = memory_bm_create(bm2, GFP_KERNEL, PG_ANY); forbidden_pages_map = bm1; free_pages_map = bm2; mark_nosave_pages(forbidden_pages_map); return 0; }
linux-src/kernel/power/hibernate.c
int hibernation_snapshot(int platform_mode) { int error; error = platform_begin(platform_mode); error = hibernate_preallocate_memory(); error = freeze_kernel_threads(); error = dpm_prepare(PMSG_FREEZE); suspend_console(); pm_restrict_gfp_mask(); error = dpm_suspend(PMSG_FREEZE); error = create_image(platform_mode); msg = in_suspend ? (error ? PMSG_RECOVER : PMSG_THAW) : PMSG_RESTORE; dpm_resume(msg); resume_console(); dpm_complete(msg); Close: platform_end(platform_mode); return error; } static void power_down(void) { switch (hibernation_mode) { case HIBERNATION_REBOOT: kernel_restart(NULL); break; case HIBERNATION_PLATFORM: hibernation_platform_enter(); fallthrough; case HIBERNATION_SHUTDOWN: if (pm_power_off) kernel_power_off(); break; } kernel_halt(); /* * Valid image is on the disk, if we continue we risk serious data * corruption after resume. */ pr_crit("Power down manually "); while (1) cpu_relax(); }
上面是休眠的过程,下面我们来看一下休眠恢复的过程,休眠恢复是先正常开机,然后从swap分区中加载之前保存的数据。
linux-src/kernel/power/hibernate.c
late_initcall_sync(software_resume); static int software_resume(void) { int error; if (swsusp_resume_device) goto Check_image; if (resume_delay) { pr_info("Waiting %dsec before reading resume device ... ", resume_delay); ssleep(resume_delay); } /* Check if the device is there */ swsusp_resume_device = name_to_dev_t(resume_file); if (!swsusp_resume_device) { wait_for_device_probe(); if (resume_wait) { while ((swsusp_resume_device = name_to_dev_t(resume_file)) == 0) msleep(10); async_synchronize_full(); } swsusp_resume_device = name_to_dev_t(resume_file); if (!swsusp_resume_device) { error = -EnodeV; goto Unlock; } } Check_image: pm_pr_dbg("Hibernation image partition %d:%d present ", MAJOR(swsusp_resume_device), MINOR(swsusp_resume_device)); pm_pr_dbg("Looking for hibernation image. "); error = swsusp_check(); if (error) goto Unlock; /* The snapshot device should not be opened while we're running */ if (!hibernate_acquire()) { error = -EBUSY; swsusp_close(FMODE_READ | FMODE_EXCL); goto Unlock; } error = freeze_processes(); error = freeze_kernel_threads(); error = load_image_and_restore(); thaw_processes(); Finish: pm_notifier_call_chain(PM_POST_RESTORE); Restore: pm_restore_console(); pr_info("resume failed (%d) ", error); hibernate_release(); /* For success case, the suspend path will release the lock */ Unlock: mutex_unlock(&system_transition_mutex); pm_pr_dbg("Hibernation image not present or could not be loaded. "); return error; Close_Finish: swsusp_close(FMODE_READ | FMODE_EXCL); goto Finish; } static int load_image_and_restore(void) { int error; lock_device_hotplug(); error = create_basic_memory_bitmaps(); error = swsusp_read(&flags); swsusp_close(FMODE_READ | FMODE_EXCL); error = hibernation_restore(flags & SF_PLATFORM_MODE); swsusp_free(); free_basic_memory_bitmaps(); Unlock: unlock_device_hotplug(); return error; } int hibernation_restore(int platform_mode) { int error; pm_prepare_console(); suspend_console(); pm_restrict_gfp_mask(); error = dpm_suspend_start(PMSG_QUIESCE); if (!error) { error = resume_target_kernel(platform_mode); /* * The above should either succeed and jump to the new kernel, * or return with an error. Otherwise things are just * undefined, so let's be paranoid. */ BUG_ON(!error); } dpm_resume_end(PMSG_RECOVER); pm_restore_gfp_mask(); resume_console(); pm_restore_console(); return error; }
随着智能手机的普及,手机的电量问题也越来越严重。之前的手机都是充一次能用三到五天甚至七天以上,但是对于智能手机来说,充一次只能用一天或者半天。手机电池技术迟迟没有大的突破,为此也只能从软件上下手解决了。安卓系统为此采取的办法是投机性睡眠,也就是说对于手机来说,睡眠是常态,运行不是常态,这也符合手机的使用习惯,一天24小时大部分时间是不用手机的。安卓在内核中添加了wakelock模块,内核默认情况下总是尝试去睡眠,除非受到了wakelock的阻止。用户空间的各个模块都可以向内核添加wakelock,以表明自己需要运行,系统不能去睡眠。当用户空间都把自己的wakelock移除之后,内核没了wakelock就会去睡眠了。Wakelock推出之后,受到了很多内核核心维护者的强烈批评,wakelock的源码也一直没有合入标准内核。后来内核又重新实现了wakelock的逻辑,叫做自动睡眠。
其代码如下: linux-src/kernel/power/autosleep.c
int __init pm_autosleep_init(void) { autosleep_ws = wakeup_source_register(NULL, "autosleep"); if (!autosleep_ws) return -ENOMEM; autosleep_wq = alloc_ordered_workqueue("autosleep", 0); if (autosleep_wq) return 0; wakeup_source_unregister(autosleep_ws); return -ENOMEM; } static void try_to_suspend(struct work_struct *work) { unsigned int initial_count, final_count; if (!pm_get_wakeup_count(&initial_count, true)) goto out; mutex_lock(&autosleep_lock); if (!pm_save_wakeup_count(initial_count) || system_state != SYSTEM_RUNNING) { mutex_unlock(&autosleep_lock); goto out; } if (autosleep_state == PM_SUSPEND_ON) { mutex_unlock(&autosleep_lock); return; } if (autosleep_state >= PM_SUSPEND_MAX) hibernate(); else pm_suspend(autosleep_state); mutex_unlock(&autosleep_lock); if (!pm_get_wakeup_count(&final_count, false)) goto out; /* * If the wakeup occurred for an unknown reason, wait to prevent the * system from trying to suspend and waking up in a tight loop. */ if (final_count == initial_count) schedule_timeout_uninterruptible(HZ / 2); out: queue_up_suspend_work(); }
关机和重启是我们平时使用电脑时用的最多的操作了。重启也是一种关机,只不是关机之后再开机,所以把它们放在一起讲,实际上它们的代码也是在一起实现的。后文中我们用关机来同时指代关机和重启。关机的过程分为两个部分,用户空间处理和内核处理。正常的关机的话,我们肯定不能直接拔电源,也不能让内核直接去关机,因为用户空间也运行着大量的进程,也要对它们进行妥善的处理。由于init进程是所有用户空间进程的祖先,所以由init进程处理关机命令是最合适不过的。实际上无论你是用命令行关机还是图形界面按钮关机还是长按电源键关机,最终的关机命令都会发给init进程来处理。Init进程首先会stop各个服务进程,然后杀死其它用户空间进程,最后使用reboot系统调用请求内核进行最后的关机操作。
我们使用命令reboot或者图形界面关机时,最终都会把命令发给init进程来处理。Init进程会首先关闭各个服务进程(deamon),然后发送信号SIGTERM给所有其他进程,给其一次优雅地退出的机会,并sleep一段时间(一般是3s)来等待其退出,接着再发送信号SIGKILL给那么还是没有退出的进程,强制其退出。最后Init进程会调用sync把内存中的文件数据同步到磁盘,最终通过reboot系统调用请求内核来关机。
我们来看一下内核总reboot系统调用的实现:
linux-src/kernel/reboot.c
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg) { struct pid_namespace *pid_ns = task_active_pid_ns(current); char buffer[256]; int ret = 0; /* We only trust the superuser with rebooting the system. */ if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT)) return -EPERM; /* For safety, we require "magic" arguments. */ if (magic1 != LINUX_REBOOT_MAGIC1 || (magic2 != LINUX_REBOOT_MAGIC2 && magic2 != LINUX_REBOOT_MAGIC2A && magic2 != LINUX_REBOOT_MAGIC2B && magic2 != LINUX_REBOOT_MAGIC2C)) return -EINVAL; /* * If pid namespaces are enabled and the current task is in a child * pid_namespace, the command is handled by reboot_pid_ns() which will * call do_exit(). */ ret = reboot_pid_ns(pid_ns, cmd); if (ret) return ret; /* Instead of trying to make the power_off code look like * halt when pm_power_off is not set do it the easy way. */ if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off) cmd = LINUX_REBOOT_CMD_HALT; mutex_lock(&system_transition_mutex); switch (cmd) { case LINUX_REBOOT_CMD_RESTART: kernel_restart(NULL); break; case LINUX_REBOOT_CMD_CAD_ON: C_A_D = 1; break; case LINUX_REBOOT_CMD_CAD_OFF: C_A_D = 0; break; case LINUX_REBOOT_CMD_HALT: kernel_halt(); do_exit(0); panic("cannot halt"); case LINUX_REBOOT_CMD_POWER_OFF: kernel_power_off(); do_exit(0); break; case LINUX_REBOOT_CMD_RESTART2: ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1); if (ret < 0) { ret = -EFAULT; break; } buffer[sizeof(buffer) - 1] = '