设计一个高效的定时任务系统

【51CTO.com原创稿件】今天想跟大家一起探讨一个听起来很简单的话题:定时任务机制 。

设计一个高效的定时任务系统

文章插图
图片来自 Pexels
无非就是一个计时器,到了指定时间就开始跑呗 。too young,要是这么简单我还说啥呢,干不就完了 。
那如果是几千上万个定时任务,你的计时器该如何设计呢?如果是 A 任务执行完后再执行 B 任务你会怎么调度呢?
如果是几十台机器同时要处理一些任务,你又该如何设计呢?带着这些看似不简单的问题我们开始时间之旅 。
操作系统的时间系统
应用程序部署在操作系统上,定时任务依赖操作系统的时钟 。鉴于大部分的服务器都部署在 linux 上,我们就只讨论 Linux 的时间系统,windows 服务器别打我 。
大部分 PC 机中有两个时钟源,他们分别叫做 RTC(Real Time Clock,实时时钟) 和 OS(操作系统)时钟 。
RTC
RTC(Real Time Clock,实时时钟)也叫做 CMOS 时钟,它是 PC 主机板上的一块芯片(或者叫做时钟电路),它靠电池供电,即使系统断电也可以维持日期和时间 。
由于独立于操作系统所以也被称为硬件时钟,它为整个计算机提供一个计时标准,是最原始最底层的时钟数据 。
OS 时钟
OS 时钟产生于 PC 主板上的定时/计数芯片(8253/8254),由操作系统控制这个芯片的工作,OS 时钟的基本单位就是该芯片的计数周期 。
在开机时操作系统取得 RTC 中的时间数据来初始化 OS 时钟,然后通过计数芯片的向下计数形成了 OS 时钟,所以 OS 时钟并不是本质意义上的时钟,它更应该被称为一个计数器 。
OS 时钟只在开机时才有效,而且完全由操作系统控制,所以也被称为软时钟或系统时钟 。
时钟中断
Linux 的 OS 时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入 CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断 。
Linux 中用全局变量 jiffies 表示系统自启动以来的时钟滴答数目 。每个时钟滴答,时钟中断得到执行 。
时钟中断执行的频率很高:100 次/秒(Linux 设计者将一个时钟滴答(tick)定义为 10ms),时钟中断的主要工作是处理和时间有关的所有信息、决定是否执行调度程序 。
和时间有关的所有信息包括系统时间、进程的时间片、延时、使用 CPU 的时间、各种定时器,进程更新后的时间片为进程调度提供依据,然后在时钟中断返回时决定是否要执行调度程序 。
在单处理器系统中,每个 tick 只发生一次时钟中断 。在对应的中断处理程序中完成更新系统时间、统计、定时器、等全部功能 。
而在多处理器系统下,时钟中断实际上是分成两个部分:
  • 全局时钟中断,系统中每个 tick 只发生一次 。对应的中断处理程序用于更新系统时间和统计系统负载 。
  • 本地时钟中断,系统中每个 tick 在每个 CPU 上发生一次 。对应的中断处理程序用于统计对应 CPU 和运行于该CPU上的进程的时间,以及触发对应 CPU 上的定时器 。
于是,在多处理器系统下,每个 tick,每个 CPU 要处理一次本地时钟中断;另外,其中一个 CPU 还要处理一次全局时钟中断 。
时钟中断的应用
更新系统时间:在 Linux 内核中,全局变量 jiffies_64 用于记录系统启动以来所经历的 tick 数 。
每次进入时钟中断处理程序(多处理器系统下对应的是全局时钟中断)都会更新 jiffies_64 的值,正常情况下,每次总是给 jiffies_64 加 1 。
而时钟中断存在丢失的可能 。内核中的某些临界区是不能被中断的,所以进入临界区前需要屏蔽中断 。
当中断屏蔽取消的时候,硬件只能告诉内核是否曾经发生了时钟中断、却不知道已经发生过多少次 。
于是,在极端情况下,中断屏蔽时间可能超过 1 个 tick,从而导致时钟中断丢失 。
如果计算机上的时钟振荡器有很高的精度,Linux 内核可以读振荡器中的计数器,通过比较上一次读的值与当前值,以确定中断是否丢失;如果发现中断丢失,则本次中断处理程序会给 jiffies_64 增加相应的计数 。
但是如果振荡器硬件不允许(不提供计数器、或者计数器不允许读、或者精度不够),内核也没法知道时钟中断是否丢失了 。
内核中的全局变量 xtime 用于记录当前时间(自 1970-01-01 起经历的秒数、本秒中经历的纳秒数) 。xtime 的初始值就是内核启动时从 RTC 读出的 。
在时钟中断处理程序更新 jiffies_64 的值后,便更新 xtime 的值 。通过比较 jiffies_64 的当前值与上一次的值(上面说到,差值可能大于 1),决定 xtime 应该更新多少 。


推荐阅读