CPU 就和皮鞋厂的工人一样, 无可奈何之时也得躺平。历代CPU的架构师都有一颗仁慈的心——给自己的产品留下了躺平的功能,而且是一代更比一代强。相关的指令有HLT/PAUSE/MWAIT/UMWAT/TPAUSE,其中我最喜爱的是UMWAIT,它的妙处我们后面再说。
当然经理并不会轻易让CPU工人去摸鱼, 必须满足一定的条件。Linux 内核中那么严格说来是由Scheduler 来判断目前的工作量是否饱满。如果工作量实在不够,也只好让CPU划划水, 睁只眼闭只眼, 好歹省点电费不是。虽然在CPU层面摸鱼的手段花样百出, 但是在OS层面只有一个抽象的概念就是IDLE。在这个时候经理表面上放任CPU工人划水, 其实打的是省电的小九九,这也算是Win-Win的帕累托改进。那么问题就来了, Scheduler 到底是如何判断当前工作量不饱满呢?
调度器中idle 触发条件
Linux Scheduler 为每一个CPU工人都维护了一个RunQueue 可以认为是一个任务列表, 当且仅当这个列表里所有的任务都不是runnable的状态时, Scheduler 才会切换到idle process。也就是说这个时候CPU工人完全无所事事, 必须休息节省体力以及节省电费! 同时还要注意可以通过nohz/nohz_full启动参数减少tick 中断对正在休息的CPU工人的干扰!
身世显赫
那么首先我们要考虑得是idle进程从何而来,在描述细节之前我先剧透一下, idle进程虽然名字听起来不怎么样, 但是出身显赫。首个idle进程实际上转化自0号进程!内核成为了生活的真相帝, 躺平并不是人人都能拥有的选项!
具体的过程则是说来话长. 在开辟鸿蒙之初, kernel 所有的进程之始祖是一个静态的结构体:
- struct
- task_struct init_task
init_task 并非由任何kernel API所创建, 是所有进程的祖先, 名副其实的the one。
它肩负了非常重要的职责, 例如创建首个内核线程kernel_init。从而达到 一生二,二生三,三生万物 的效果。不过今天这些不是本文的重点, 重点在于大功告成之际, init_task 并未事了拂衣去而是默默的转化成了idle 进程。 它继续无言的守护着整个系统, 俯首甘当孺子牛啊。
这里参见代码 init/main.c line 451,这里是函数rest_init 的最后阶段大家可以发现调用了 cpu_startup_entry 这个函数。
我们可以跳转到 kernel/sched/idle.c 来一窥细节. 这里第一个arch_cpu_idle_entry 是可选接口,x86并没有实现,核心中的核心还是函数 do_idle()-- just 躺平.
如果没有在启动时强制idle 模式为poll, 则我们将在这里进入真正的躺平函数 cpuidle_idle_call.
在smp系统中 core0 以外的其它core 也会通过 start_secondary 函数最终产生0号进程且调用cpu_startup_entry 来进入idle loop之中。
CPU各种“躺平”的姿势
各个厂子出产的CPU工人的idle 姿势也是慢慢演化的, 从简入繁, 花样百出。下面我们一起来浮光掠影的看一下这些"超能力"。
X86
HLT
这是初代的idle 指令, 于486DX时代引入. 首先只有在ring0的特权级别才能执行HLT指令, 同时执 行的效果是 CPU 进入了C1/C1E state(参考ACPI标准)。严格说起来只能算是摸鱼0.1v。APIC/BUS /CACHE 都是照常运转, 只要中断发生, CPU工人立即就要回到产线继续搬砖。C1E 稍微又优待了CPU点, 停止内部时钟又降了压, 比较体贴。
PAUSE
这个也是非常早期的指令(Pentium 4)许可CPU 工人打个盹,大概从几个到几十个cycles吧(各代CPU有差异)。为什么要打盹呢?其实主要是要降低CPU工人在特定情况下(spin-lock)给内存控制器带来的压力,与其让CPU工人阻塞了内存控器, 不如让他打个盹吧。在最近的几代Xeon之上还附带了降低功耗的buff。
MWAIT/MONITOR
新一代CPU架构师回顾了前辈的设计, 觉得CPU工人的权力完全没有得到充分的照顾, 应该给予更进一步的休息机会乃至真正的躺平!而且唤醒的条件又多了一个, 除了中断这种强唤醒模式以外, 又加了内存的CacheLine Invalidate唤醒。你的邻居CPU 除了敲门以外还多了拿橡皮筋弹窗户玻璃的渠道。首先这两条指令也只能在ring0 级别执行, 首先是调用MONITOR 地址范围, 其次是MWAIT 进入休眠,一旦该地址的内存被任何其它的主体修改, 则唤醒CPU工人起来继续搬砖。同时这次最大的改进是可以通过MWAIT 进入各种不同的Cstate。其中C6 是我心目中真正的躺平 CPU 电压可以归0同时cache 也停, 实至名归啊。
最常见的C State 状态详细描述,引自[2]
Cstate | Name | Description |
---|---|---|
C0 | Operating State | CPU fully turned on |
C1E | Enhanced Halt | Stops CPU main internal clocks via software and reduces CPU voltage; bus interface unit and APIC are kept running at full speed |
C3 | Deep Sleep | Stops all CPU internal and external clocks |
C6 | Deep Power Down | Reduces the CPU internal voltage to any value, including 0 Volts |
UMWAIT/UMONITOR
MWAIT虽好, 但是奈何必须在ring0特权级下执行, 如果是一些特定的用户级应用例如DPDK, Linux的 idle driver 是很难得到执行的机会,所以CPU架构师又生怜悯之心, 允许CPU在用户级也能进入躺平的模式, 不过作为妥协连C1 state都不行,只能进入 C0.1/C0.2 等神秘模式。效果还有待观察,不过话说回来SPR这代Xeon才开始支持....距离上市少说还得1年之久。
TPAUSE
UMWAIT 指令的升级加强版, 附带了一个timer。TPAUSE 可以让CPU工人根据规定好的时间进行休息, 时间一到, 立刻继续搬砖。当然这也是一个簇新簇新的指令,大家还要等待SPR。
ARM
ARM的Idle-state 级别情况比较复杂一些, 更多的是和具体的芯片实现相关. 但是总体上也是把握几个大的类别:
- 只是停止CPU内部时钟
- CPU降频
- 停止给Cache供电
- 停止给CPU供电
和X86 相比 Arm的唤醒机制没有和MESI协议连接有些遗憾(也就是没有实现通过MEM 地址监控的方式达成唤醒).
YEILD
这条颇为类似 PAUSE基本功能接近,使用场景也接近(spin lock).
WFE/WFI
这两条指令顾名思义 wait for event/ wait for interrupt,中断这条大家都可以理解类似HLT,那么event这条就值得看看了。ARM架构可以从一个CPU向所有其它CPU 发送event(sev 指令),我的理解类似IPI广播,收到了此event的CPU如果处于idle状态, 则需要立即唤醒。(注:和宋老师讨论以后发现 event 和IPI的一个区别是不需要ISR来响应,同时event并不能唤醒由于WFI指令进入的idle,这个有点囧,反过来中断可以唤醒由于WFE进入的idle。这两个躺平姿势水很深啊)
软件实现
除了硬件的各种花式躺平技术之外还有两类“伪躺平”技术。
idle polling
通过启动参数, 我们可以指定cpu的idle 进程并不调用硬件提供的idle功能而仅仅是polling, 这种情况主要用于需要极低的CPU从idle状态返回时延的场景。那么如果压根没有进入实际的idle状态,当然时延是极低的,同时也能融入到idle整体的框架,不至于破坏规矩开特例。
halt-polling
在打开虚拟化的场景下, 事情就变得更加有趣了。大多数情况下, qemu 会缺省的只对guest 提供HLT指令作为idle的唯一机制,但是 HLT 指令毫无悬念的会触发VMEXIT。虽然说大多数情况下kvm看到exit reason 是HLT 也只是执行poll而已, 但是VMEXIT/VM_RESUME 还是如此的痛,毕竟几千个cycles已经无谓流逝, 追求极致的我们怎么能放任资源浪费。于是Redhat在Guest端引入了halt poll 机制, 也就是说如果matrix中的CPU工人首先开始假摸鱼(poll), 如果假摸鱼时间超过了阈值才真的去触发HLT指令。如果很快就被从假模鱼状态拉回去搬砖, 则省去了出入matrix的费用(经理得意的笑了)。
相关细节参考内核文档Documentation/admin-guide/pm/cpuidle.rst:
以及:
Documentation/virt/guest-halt-polling.rst:
CPU idle driver/governor
最后软件硬件各路躺平姿势花样繁多, 内核无奈又祭出了抽象大法把idle的时长与返回时延的选择与具体执行idle的机制分离开来。
- idle governor 就负责做时长与时延的选择,也可以称为 idle -select。
- idle driver 则是负责通过我们上面描述的各种软硬件机制来实现governor指定的目标。同时向governor menu 经理提供各种不同机制的性能参数,以供menu经理选择, 就是所谓的idle-enter。
图片引自[6]
idle governor 缺省的算法只有一个就是menu, 还有3个候选的ladder/TEO/haltpoll 算法但是一般需要重新编译内核来激活。
- ladder 算法故名意思, 是首先从能耗较高/返回时延较小的状态开始,当系统idle超过了阈值再进入更深的节能状态,从而逐步升级节能状态。俗称添油战术也可以美其名曰“快速迭代”。
- menu 算法单从名字看则有点让人摸不到头脑,其内部机制也确实颇为复杂,menu算法主要是要在节能状态的停留时间与系统能容忍的返回时延之间做权衡以达到最佳效果。
请原谅我非常不精确地描述一下menu。menu仿佛一个非常敬业的经理凡事都要精算做出最优选择,CPU工人一旦休息再想打起精神干活这个转换是有一个代价的, 往往需要口头鼓励(画饼)+物质鼓励(肉夹馍)。那么经理就要考虑如果工人休息时间太短,休息的好处远低于让CPU工人重新振作的代价,那么这个休息就是不合理的(无情啊)。而且休息也有好些种类, 从假休息到完全躺平, 到底哪一种休息状态才是收益比最佳的? menu会无情的选择那个休息带来好处大于重新振作代价的方案。同时menu经理还会受到来自客户的压力, 时延也是要满足的。客户的耐心大抵上都是不好的, menu经理会疯狂试探客户的底线。它选择的方案是满足客户耐心上限的情况下CPU工人消耗能耗最少的方案。同时做到以上两点 menu经理大约才能有希望完成OKR/KPI。
结语
今天我们一起浮光掠影的讨论了一下Linux的各种躺平姿势,从中能领略到一代代CPU架构师对CPU打工人的关爱。最后我衷心的祝愿CPU打工人在层出不穷的各类躺平技术加持下,最终能同各位经理一起实现碳中和的OKR/KPI。
参考文献
[1]https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/idle-states.txt
[2]https://www.dell.com/support/kbdoc/en-ie/000060621/what-is-the-c-state
[3] Intel SDM latest version
[4] https://www.kernel.org/doc/html/latest/virt/guest-halt-polling.html
[5]https://www.kernel.org/doc/html/latest/admin-guide/pm/cpuidle.html
[6] https://www.programmersought.com/article/13982556297/
作者简介
作者Liam,海外老码农,对Linux、应用密码学、CPU微架构、高速网络通信等领域都有所涉猎。
本文转载自微信公众号「Linux阅码场」,可以通过以下二维码关注。转载本文请联系Linux阅码场公众号。