我们知道操作系统的特性是:在任何时间内,CPU有且只有一个任务在运行。如果没有一个任务在运行,CPU在做什么的?
事实证明,这种情况非常普遍,对于大多数个人计算机来说,它实际上是常态:睡眠过程的进程,都在等待某些条件唤醒,而近100%的CPU时间正在这个神秘的“空闲任务”中。事实上,如果CPU一直忙于普通用户,那通常是有问题的,或者是恶意软件在霸占CPU。
为了保持设计的一致性,OS开发人员创建了一个空闲任务,当没有其他工作时,该任务将被安排运行。我们在Linux 启动过程中已经看到,空闲任务是进程0,它是计算机首次打开时运行的第一条指令的直接后代。它在rest_init中 初始化,其中init_idle_bootup_task初始化空闲调度类。
简而言之,Linux支持不同的调度类,例如实时进程,常规用户进程等。当选择进程成为活动任务时,将按优先级顺序查询这些类。这样,“核反应堆控制代码”总是在Web浏览器之前运行。但是,这些类通常会返回NULL,这意味着它们没有合适的运行过程 - 它们都在睡觉。但是最后运行的空闲调度类永远不会失败:它总是返回空闲任务。
这一切都很好,但让我们来看看这个空闲任务究竟在做什么。cpu_idle_loop,如下:
cpu_idle_loop
- while(1){ while(!need_resched()){ cpuidle_idle_call(); }
- / *
- [注意:切换到其他任务。当
- 再次选择空闲任务运行时,我们将返回此循环。]
- * /
- schedule_preempt_disabled();
- }
我已经省略了很多细节,我们稍后会仔细研究任务切换,但是如果你阅读了代码,你会得到它的重要信息:只要不需要重新安排,即改变活动任务,CPU就会一直空闲。以经过的时间来衡量,这个循环及其在其他操作系统中的表兄弟可能是计算历史中执行最多的代码片段。对于英特尔处理器,传统上保持空闲意味着运行暂停指令:
native_halt
- static inline void native_halt(void)
- {
- asm volatile("hlt": : :"memory");
- }
hlt停止处理器中的代码执行并将其置于暂停状态。奇怪的是,全世界数以百万计的类似英特尔的CPU正在花费大部分时间停止工作,即使在他们通电的情况下也是如此。它也不是非常有效的节约能源,这导致芯片制造商为处理器开发更深层次的睡眠状态,从而在更长的唤醒延迟中消耗更少的功耗。内核的cpuidle子系统负责利用这些节能模式。
现在,一旦我们告诉CPU停止或睡眠,我们需要以某种方式让它恢复活力。如果您已阅读过我以前的文章,您可能会怀疑涉及到中断,实际上它们也是如此。中断会刺激CPU退出暂停状态并恢复运行。所以把这些放在一起,这是你的电脑在阅读这篇文章时所做的大部分工作
除定时器中断外的其他中断也会使处理器再次运行。如果你点击一个网页就会发生这种情况,例如:你的鼠标发出一个中断,它的驱动程序处理它,突然一个进程可以运行,因为它有新的输入。此时need_resched()返回true,并且启动空闲任务以支持您的浏览器任务。
这是随时间变化的空闲循环:
在这个例子中,内核将定时器中断编程为每4毫秒(ms)发生一次。这是滴答期。这意味着我们每秒获得250个滴答,因此滴答率或滴答频率为250 Hz。这是在英特尔处理器上运行的Linux的典型值,100赫兹是另一个人群的最爱。这在CONFIG_HZ构建内核时在选项中定义。
现在看起来对于空闲CPU来说看起来像是一项非常多的毫无意义的工作,而且确实如此。如果没有来自外界的新鲜输入,CPU将继续陷入这种地狱般的小睡状态,在您的笔记本电脑电池耗尽时,每秒钟会被唤醒250次。如果这是在虚拟机中运行,我们将从主机CPU烧掉电源和时钟周期。
这里的解决方案是动态勾选,以便当CPU空闲时,定时器中断被取消激活或重新编程,以便在内核知道将要工作的地方发生(例如,进程可能有一个定时器)在5秒内到期,所以我们不能睡过去)。这也称为无滴答模式。
最后,假设您在系统中有一个活动进程,例如长时间运行的CPU密集型任务。这几乎与空闲系统完全相同:上述图保持大致相同,只需将一个进程替换为空闲任务,图景就是准确的。在那种情况下,每隔4毫秒中断任务是没有意义的:它只是操作系统抖动,从而减慢了你的工作量。Linux还可以在这个单进程场景中停止固定速率滴答,即所谓的自适应滴答模式。最终,固定速率蜱可能会消失完全。
这对于一个文章来说已经足够发散了。内核的空闲行为是操作系统难题的重要组成部分,它与我们将看到的其他复杂情况非常相似,因此这有助于我们构建正在运行的内核的图景。