用户态的定时器设计
记得某段时间的工作中,经常会用到定时器。发现有些同学为了图方便,会这样实现定时器:
while(1) {
sleep_awhile();
while((timer = get_expired_timer()))
do_timer_handler(timer);
}
用一个线程,周期性地睡眠一段时间,然后起来看看有没有需要触发的定时任务。
这种定时器写起来确实很简单,但是也让人感觉很拙。一方面,周期性的睡眠与唤醒,占用了一定的调度开销,并且定时线程被唤醒之后,经常是无事可做的。另一方面,定时器的精度非常之低(依赖于定时线程的睡眠时间)。若想提高精度,只能缩短睡眠时间,从而造成更大的调度开销。
当然,在一些特定的场景下,这样的定时器也未必不能很好地满足需求。但是多数情况下,这还是不够理想的。
换一种思路感觉会更好一些,从timer本身出发,按需睡眠(timer需要等待多久就睡眠多久,而不是周期性地睡眠)。
这样的定时器实现起来会稍麻烦一些,需要将所有的timer按其expire_time增序排序。定时线程每次需要sleep的时间就是当前时间到第一个timer的expire_time的间隔时间。当新增一个排在第一位的timer、或者删除排在第一位的timer时,都需要调整定时线程的睡眠时间。
于是,定时线程每一次从睡眠中被唤醒,总是伴随着一个(或多个)timer需要被处理。并且定时的精度很高(依赖于操作系统提供的定时精度)。
内核态的定时器设计
linux内核里面的定时器似乎也面临着这样的问题,从《linux时钟浅析》一文中可以看到,内核管理的时钟和定时器实际上采用的是“周期性睡眠”的这种方案。时钟源周期性地发出中断(一般以1ms为周期),时钟中断处理程序每一次都会更新时间、更新一些统计信息、然后检查并触发定时器。
显然,时钟中断会周期性地打断CPU上正在执行的任务。同时,定时器的精度又因为依赖于时钟中断的周期,而受到限制。而减小时钟中断的周期又会带来更频繁的中断,造成更大的开销。
从第一次了解到linux的时钟机制开始,我就在想,是否这种机制也能改造成“按需睡眠”(按需触发时钟中断)的方案呢?从而减小时钟中断的开销、增加定时器的精度。
但是注意到,时钟中断处理程序需要做的事情并不限于处理定时器(比这个要复杂得多)。“按需睡眠”的方案可能会非常复杂,甚至可能会增加时钟与其他子系统之间的耦合度。
比如说,调度程序依赖周期性的时钟中断来更新当前进程的执行时间,从而不断更新进程的动态优先级,从而决定当前进程是否应该被替换掉(见《linux进程调度浅析》)。如果改用“按需睡眠”,则调度程序必须在当前进程被调度运行时,就预料到该进程将在运行多久时间之后被取代,从而设置相应的定时器。并且,当当前进程状态改变、或者某些中断使某些进程状态改变、等情况发生时,这个定时器可能需要更新。
再比如,由于“按需睡眠”,系统时间就不能保持周期性的更新了。应用程序读系统时间时,可能就要有两种做法:如果需要高精度的时间,只能通过读硬件时钟源设备来解决;对于低精度的(比如只需要精确到秒),则可以通过设置较长的周期性时钟中断来解决(比如设置每秒一次的时钟中断,这样的开销几乎是可以忽略的)。
再比如,当应用程序需要去读各种统计信息时,可能就只能实时去计算了,而不能直接得到已经由周期性时钟中断更新好的统计结果。
此外,受影响的地方应该还有很多……
可能是由于太过复杂,以至于可能得不偿失(各种复杂情况综合下来,“按需睡眠”可能并不比“周期性睡眠”开销小),“按需睡眠”的方案似乎没有被人们怎么考虑过。
然而,高精度的定时器却是很有需求的,于是linux内核中就有了hrtimer(High-resolution Timer)。
hrtimer就是按“按需睡眠”的方案去实现的,通过红黑树去维护一组按expire_time排好序的timer。然后将时钟源设备设置为非周期性触发中断的模式,并总是设置下一次中断触发的时间就是第一个timer的expire_time。(当然,这些都需要硬件的支持。)当需要使用到高精度的定时器时,就可以向这棵红黑树添加timer。
那么,原来的那一套“周期性睡眠”的机制怎么办呢?hrtimer工作在原先那套机制的底层,内核会向hrtimer注册周期性的timer,从而获得周期性的时钟中断,原来的“周期性睡眠”这一套东西完全不用变。
(关于hrtimer,详见《Linux
时钟管理》。)
hrtimer通过将时钟的底层从“周期性睡眠”改为“按需睡眠”,实现了高精度的定时器(精度依赖于硬件)。而将来“按需睡眠”的层次是否会得到提升(使得“周期性睡眠”的模式被修改),从而减少时钟中断的次数,以降低开销呢?
另外,如果你读了上面那篇《Linux
时钟管理》,可以发现,linux内核已经有了nohz这种模式。开启nohz模式后,就不存在周期性的时钟中断了。
但是这种模式出现的原因并不是为了实现我们这里说的“按需睡眠”,而是为了让CPU在长期没有任务的情况下能够逐渐进入休眠,以达到节能的目的(周期性的时钟中断会不断给CPU制造任务,使其不得安宁)。于是,内核总是试图在CPU进入空闲时(进入空闲,代表已经没有任务需要执行了),开启nohz模式;在退出空闲时,停止nohz模式。
不过既然周期性的时钟中断没有了,很多需要依赖它的部分也需要做修改。就比如之前举例说明的那些,只是仅为支持nohz还不需要改得那么彻底。那么,今后这些修改又会否更加彻底,从而实现“按需睡眠”呢?