操作系统行为的基本原理是,在任何一个特定时刻,在一个 CPU (核心)上有且只有一个任务是活动的。除了当前正在占用的程序,99.9999%的程序都处于中断或休眠状态。对于几乎所有人来说,几乎100%的CPU时间都是空闲的。那么,如果 CPU 无事可做的时候,又会是什么样的呢?

不难想到,既然CPU闲着,那不如让他找点事情做呗!于是就有了第一种想法:

让CPU持续很长时间去遍历内核,检查是否有一个活动任务需要它,有的话就去做。

这是很多人第一想到的思路,CPU没事情的话就让他自己找点事情做。可惜这并不是一个明智的选择,这样带来的是CPU时间的高消耗,连内存/硬盘都难逃一劫。既然说“给操作系统写内核的人都是天才”,那么他们贡献了沿用至今的一种很巧妙的方法:Cpu Idle Loop。

Cpu Idle Loop

有没有注意到任务管理器中总有一个叫做System Idle Process的进程使用着几乎全部的CPU资源?

为了保证CPU能在我们需要它的时候被唤醒,又要让他闲置的时候有事情做,不能彻底关闭掉;再加上又要让CPU的功耗在闲置时降低...于是,操作系统开发者创建了一个空闲任务,当没有其它任务可做时就调度它去运行。在 Linux 中,这个空闲任务就是进程 0,它是由计算机打开电源时运行的第一个指令直接派生出来的。对于Windows来说,它就是System Idle Process

通过一段简化过的Linux内核代码来了解一下这个Loop到底是如何运行的。

while (1) {
while(!need_resched()) {   //当不需要重新安排给CPU安排任务的时候。
cpuidle_idle_call();            //执行CPU空闲进程,通俗的就是让CPU去休息。
}

/*
//将CPU重新分配给其他需要它的进程
*/
schedule_preempt_disabled();
}

对于Intel处理器来说,空闲状态意味着运行下面的指令。

static inline void native_halt(void)
{
asm volatile("hlt": : :"memory");
}

现在,我们已经告诉 CPU 去 halt(睡眠)了,我们还需要以某种方式让它醒来以便执行其他任务,不要就这样睡死过去。天才们创建了很多唤醒它的巧妙方法——中断。

中断操作

返回去看到上面的源代码:while(!need_resched()) 。当不需要重新为CPU安排时间的时候做的事情。

“中断”会让你的CPU知道它现在需要分配到其他任务了(need_resched返回true的时候)什么是中断?比如当你的鼠标点击了一下,因为它产生了一个新的输入鼠标的驱动程序就会做出响应,告诉CPU进程就可运行了。在那个时刻, need_resched() 返回 true,然后空闲任务(cpuidle_idle_call();)因你的这次点击而被踢出而终止运行。

计时器中断

那如果我就是不点击,不输入,那我的电脑为什么也没有睡死过去呢?在这个示例中,由内核计划的定时器中断会每 4 毫秒发生一次,也就是说不管什么情况,每隔4ms打断一次CPU的工作,重新给他安排一次工作。这就是滴答tick周期。

如图所示,也就是说每秒钟将有 250 个滴答,因此,这个滴答速率(频率)是 250 Hz。这是运行在 Intel 处理器上的 Linux 的典型值,而其它操作系统喜欢使用 100 Hz。

对于一个空闲 CPU 来说,它看起来似乎是个无意义的工作。如果外部世界没有新的输入,在你的笔记本电脑的电池耗尽之前,CPU 将始终处于这种每秒钟被唤醒 250 次的地狱般折磨的小憩中。但是这让CPU降低到了非常低的工作频率,同时也降低到了非常低的功耗,而且让CPU在任何需要它的时候都不会宕机。

最新的空闲策略

假如你有一个进程要进行超长的操作,然后你的内核每4ms打断一次CPU,再让他继续去做原来的工作,那这也太糟糕了。最新的解决方法是动态滴答自适应滴答


午夜零点的帷幕彼方,难消之恨,愿为消之。