CPU就像皮鞋厂里的工人,没办法只能躺着。历代CPU的架构师都有一颗仁慈的心——给自己的产品留下扁平化的功能,一代比一代强。相关的指令有HLT/PAUSE/MWAIT/UMWAT/TPAUSE,其中我最喜欢的是UMWAIT,后面再说它的美。当然,manager不会轻易让CPUworker去钓鱼,需要满足一定的条件。严格来说,在Linux内核中,Scheduler是判断当前工作负载是否满了。如果工作量实在不够用,还得任由CPU划桨,视而不见,闭目养神,至少能省点电费。虽然CPU层面的钓鱼方式有很多种,但是OS层面的抽象概念只有一个,就是IDLE。这时经理表面上是让CPU工们划水,实际上是打出了省电的99,也算是Win-Win的帕累托改进。那么问题来了,Scheduler如何判断当前工作负载未满呢?scheduler中的空闲触发条件,LinuxScheduler为每个CPUworker维护了一个RunQueue,可以认为是一个任务列表,当且仅当这个列表中的所有任务都不可运行时,Scheduler才会切换到空闲进程.也就是说此时的CPUworker是完全闲置的,必须休息才能节省能源和电费!同时需要注意的是nohz/nohz_full启动参数可以减少tick中断对正在休息的CPUworker的干扰!那么我们首先要考虑的是空闲进程从何而来?在描述细节之前,让我先剧透一下。idleprocess这个名字虽然听起来不太好听,但是却有着显赫的背景。第一个空闲进程实际上是由0号进程改造而来的!内核已经成为生活的真理,平躺不是每个人的选择!具体过程说来话长。在鸿蒙开发之初,所有内核进程的祖先都是一个静态结构体:structtask_structinit_taskinit_task不是由任何内核API创建的,它是所有进程的祖先,真正的一个。它肩负着非常重要的职责,比如创建第一个内核线程kernel_init。从而达到“一命二,二命三,三万物”的效果。不过这些都不是今天本文的重点。重点是,当你完成后,init_task并没有消失,而是悄无声息地变成了一个空闲进程。它继续默默守护着整个系统,低着头欺负人。看到代码init/main.cline451here,这里是函数rest_init的最后阶段,可以发现函数cpu_startup_entry被调用了。我们可以跳转到kernel/sched/idle.c查看详细信息。这里的第一个arch_cpu_idle_entry是可选接口,x86没有实现。核心中的核心还是do_idle()这个函数——平躺就好。如果不是在idle模式启动时强制poll,那么我们这里就进入真正的平面函数cpuidle_idle_call。在smp系统中,除core0以外的其他core最终会通过start_secondary函数生成process0,并调用cpu_startup_entry进入idleloop。CPU的各种“平躺”姿势各个工厂生产的CPU工人的闲置姿势也在慢慢演变,由简单到复杂,花样繁多。让我们一起来快速了解一下这些“超能力”。X86HLT是第一条空闲指令,是在486DX时代引入的。首先,HLT指令只能在ring0的特权级执行,执行的效果是CPU进入C1/C1E状态(参考ACPI标准)。严格来说只能算是钓0.1v。APIC/BUS/CACHE都照常运行,一旦出现中断,CPU工人会立即返回生产线继续搬砖。C1E稍微偏向CPU点数,停止内部时钟,降低电压,比较贴心。PAUSE也是很早的指令(Pentium4),可以让CPUworker小憩一下,大概几个到几十个cycle(不同代的CPU)。你为什么需要小睡?其实主要是为了减轻CPUworker在某些情况下(自旋锁)对内存控制器的压力。与其让CPUworker阻塞内存控制器,还不如让他小睡一下。在最后几代Xeons之上有一个功率降低增益。MWAIT/MONITOR新一代的CPU架构师回顾了前人的设计,觉得CPU工作者的力量没有得到充分的照顾,应该给予进一步的休息甚至平躺的机会!而且还多了一种唤醒条件,除了中断这种强唤醒方式外,还增加了内存CacheLineInvalidate唤醒。除了敲门,你邻居的CPU还有一种方法,就是用橡皮筋弹窗玻璃。首先,这两条指令只能在ring0级别执行。一是调用MONITOR地址范围,二是进入MWAIT休眠。一旦这个地址的内存被任何其他主体修改,CPUworker就会被唤醒,继续搬砖。同时,这次最大的改进是可以通过MWAIT进入各种Cstates。其中C6才是我心目中真正的平板CPU。可以将电压重置为0,同时停止缓存。它名副其实。CState状态最常见的详细描述,引用自[2]Cstate名称描述C0OperatingStateCPU完全开启C1EEnhancedHalt通过软件停止CPU主要内部时钟并降低CPU电压;总线接口单元和APIC保持全速运行C3DeepSleep停止所有CPU内部和外部时钟C6DeepPowerDown将CPU内部电压降低到任何值,包括0伏UMWAIT/UMONITORMWAIT很好,但必须在ring0权限级别,如果是一些特定的用户级应用比如DPDK,Linuxidledriver很难得到执行的机会,所以CPU架构师有一颗慈悲的心,允许CPU进入平躺模式甚至在用户层面,但作为妥协,连C1状态都不允许,只能进入C0.1/C0.2等神秘模式。效果还有待观察,不过话说回来,至强的SPR一代才刚刚开始支持……至少要一年时间才能上市。TPAUSEUMWAIT命令的升级和增强版本,附加了一个计时器。TPAUSE可以让CPUworker按照指定的时间休息一下,时间一到,马上继续搬砖。当然,这也是全新的指令,大家还得等SPR。ARMARM的Idle-statelevel比较复杂,和具体的芯片实现比较相关。但总的来说,它也掌握了几个大类:只是停止CPU内部时钟,CPU降频,停止对Cache供电,停止对CPU供电以及与Arm的唤醒机制相比,X86没有与MESI协议,比较遗憾(就是没有通过MEM地址监听实现唤醒)。YEILD与PAUSE非常相似。基本功能接近,使用场景也接近(自旋锁)。WFE/WFI这两条指令,顾名思义就是waitforevent/waitforinterrupt,中断大家可以理解为类似于HLT,所以事件值得一看。ARM体系结构可以将事件(sev指令)从一个CPU发送到所有其他CPU。我的理解类似于IPIbroadcast。如果接收到该事件的CPU处于空闲状态,则需要立即唤醒。(注:和宋老师讨论后,发现event和IPI的区别是不需要ISR响应,同时event不能因为WFI命令空闲唤醒,这个有点尴尬。在turn,interruptcanwakeidleduetoWFE.这两种卧姿水很深)除了硬件的各种花哨的卧法外,软件实现的“假卧”技术还有两类。Idlepolling通过启动参数,我们可以指定CPU的idle进程不调用硬件提供的idle函数,只调用polling。这种情况主要用于要求CPU从空闲状态返回延迟极低的场景。所以如果完全不进入实际的idle状态,当然延迟极低,也可以融入到idle的整体框架中,以免破坏规则,做特例。随着暂停轮询开启虚拟化,事情变得更加有趣。大多数情况下,qemu默认只会提供HLT命令作为guest的唯一空闲机制,但是HLT命令会毫无悬念的触发VMEXIT。虽然kvm大部分情况下看到退出原因是HLT,只是执行了poll,但是VMEXIT/VM_RESUME还是这么蛋疼。毕竟,已经毫无意义地过去了数千个周期。怎能为了追求极致而放纵资源浪费。所以Redhat在Guest端引入了haltpoll机制,也就是说如果matrix中的CPUworker先开始fakefishing(poll),如果fakefishing的时间超过了阈值,那么HLT命令才真正被触发。如果迅速从幻鱼状态拉回来搬砖,就省去了进出矩阵的成本(经理得意地笑了笑)。相关细节参考内核文档Documentation/admin-guide/pm/cpuidle.rst:和:Documentation/virt/guest-halt-polling.rst:CPUidledriver/governor最后还有很多方法可以平躺软件和硬件,内核不得不采用抽象的方法,将idleduration和returndelay的选择从idle的具体实现机制中分离出来。idlegovernor负责duration和delay的选择,也可以叫做idle-select。idledriver负责通过我们上面描述的各种硬件和软件机制来实现governor指定的目标。同时向governor菜单管理器提供各种机制的性能参数,供菜单管理器选择,即所谓的idle-enter。图片引自[6]闲置调速器。默认算法只有一个菜单,备选的ladder/TEO/haltpoll算法有3种,但一般需要重新编译内核才能激活。顾名思义,阶梯算法从高能耗/返回延迟小的状态开始,在系统闲置超过阈值时进入更深层次的节能状态,从而逐步升级节能状态。俗称加油战术,也可以美其名曰“快速迭代”。菜单算法光看名字就有点迷糊,其内部机制确实相当复杂。菜单算法的主要目的是在节能状态下的停留时间和系统可以容忍的返回延迟之间进行权衡,以达到最佳性能。效果好。请原谅我在描述菜单时非常不准确。菜单似乎是一位非常敬业的经理,他必须在精算上做出最佳选择。一旦CPU工作者休息一下,想振作起来工作,这种转换是有代价的,往往需要口头鼓励(画饼)+物质鼓励(夹肉夹馍)。那么manager就得考虑如果worker的休息时间太短,休息的收益远低于恢复CPUworker的成本,那么休息就是不合理(无情)。而且休息的方式有很多种,从假休息到完全躺着,哪种休息状态的好处最好?菜单会无情地选择带来收益大于复兴成本的计划。同时,菜单管理员也会承受来自顾客的压力,延迟也必须得到满足。顾客的耐心一般都不好,菜单经理会疯狂试探顾客的底线。它选择的方案是在满足客户端耐心上限的情况下,CPUworker消耗的能量最少的方案。菜单经理只有同时做好以上两点,才有希望完成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]IntelSDM最新版本[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代码阅读领域公众号。
