第6章 时间度量
时间度量在一个操作系统中占据着重要的地位,操作系统中的很多活动都离不开时间度量。例如,判断当前进程的时间片用完、并进行调度,向用户进程提供当前的年月日,在一段时间(如3秒钟)后执行一个任务,在将来的某一时刻执行一个任务等。
一般来说,时间度量子系统需要使用和维护两种类型的时间:墙上时间xtims,相对时间jiffies。这两种不同类型的时间有不同的用途,计算机系统采用不同的机制对这两种时间进行度量和维护。
本章随后围绕这两个主体进行阐述,从硬件支持到软件架构;从时间度量模块的初始化到如何使用时间度量的工作机制;从软件定时器的使用到软件定时器的工作原理。内容简介如下。
6.1、6.2节介绍时间度量所需的硬件和软件架构。
6.3节讲解系统时钟度量子系统的初始化过程,介绍了内定时期、系统时钟中断源、时钟中断处理函数的初始化过程。
6.4节详细讨论了时钟中断的处理过程,介绍了时钟中断的处理过程和时钟中断下半部完成的工作。
6.5节对内核定时进行专门的论述,详细介绍了内核定时器的工作原理。
6.6节讨论了Linux内核中微秒级延迟的实现方法。
6.1 硬件支持
本节从硬件角度介绍了PC架构下的时钟源,为系统所需的两种不同时间度量提供所需的硬件支持。
6.1.1 实时钟RTC
从第一台IBM个人计算机起,所有的个人计算机都包含一个实时钟(Real Time Clock,简称为RTC)的时钟芯片。该芯片内部拥有256字节的CMOS RAM,用于记录系统当前时刻距UTC时间1970/01/01/00:00:00的秒数和BIOS配置参数。此芯片靠主板上的电池供电,因此当计算机关掉后,时钟芯片仍能正常工作。早期的RTC芯片是Motorola的MC146818或其他兼容的芯片。随着计算机工业的发展,实时钟芯片已经集成到系统的南桥芯片中了,如在Intel 2005年5月发布的945G的芯片组中的南桥芯片82801GR ICH7R中,就集成了一个MC146818兼容的实时钟功能模块。
实时钟芯片主要用于向系统提供墙上时间(wall time)。在系统初始化的过程中,内核会通过函数get_cmos_time()读取实时钟芯片内所保存的时间来初始化系统的墙上时间。此外,实时钟芯片还可用来产生周期信号,这样该设备就能被当作一个定时器使用,产生的定时信号通过8号中断向量提交给系统。实时钟芯片也可用于在指定的时间唤醒计算机。
对操作系统而言,RTC仅作为一个外部计时设备而存在,Linux内核仅在系统初始化过程中根据RTC的值来设置系统的绝对时间,系统启动以后的计时与该时钟源不再有联系。读者可以与下一节中的系统时钟进行比较。
6.1.2 系统时钟
操作系统应该具备在将来某个时刻调度某个任务的能力,所以需要一种能保证任务准时调度运行的机制。该机制的核心就是系统时钟。与实时钟RTC不同,系统时钟是定时器硬件和系统软件的结合。
1.系统时钟中断源
系统时钟硬件在通过编程配置后可以产生一定频率的中断。在个人计算机中,与该中断相关的中断向量号是0。系统软件通过累计从开机到现在产生该中断的次数来维护系统时间,形成系统时钟。本小节主要介绍在个人计算机中常见的、可以用于系统时钟的硬件定时器。
(1)8254可编程定时器。
当前使用最普遍的定时器硬件芯片是Intel 8254可编程定时器芯片(Programable Interval Timer,简称为PIT),该芯片由一个1 193 181Hz的振荡器驱动,含有3个独立的通道;每个通道包含一个16位的计数器。对于每一个到达的时钟脉冲,通道中计数器中的值减1,当计数器减到0时,相应的通道就会产生一次输出。其中通道0的输出连接到了中断控制器,其对应的中断向量号为0,用于产生系统时钟所需要的滴答;通道1的输出在早期的计算机中用于DRAM的刷新,新近的计算机系统中有专门的硬件负责DRAM的刷新,通道1的功能已经不存在了;通道2的输出连接到了位于主板上的蜂鸣器(PC Speaker),控制蜂鸣器发出一定频率的声音。
这里介绍一下驱动8254工作时钟频率的来历。最初的个人电脑设计时出于成本上的考虑,主板上采用了当时广泛用于电视机且价格最便宜的一个14.318 18MHz振荡器,该振荡器的频率远远高于系统其他器件所要求的工作频率。设计师采用了3分频后得到4.77MHz驱动中央处理器8088;采用4分频后得到3.58MHz信号用于驱动彩色图形适配器;最后将系统各种频率的基频1.193 181 6MHz(各种频率的最大公约数,即12分频)信号用做系统可编程定时器芯片的输入时钟。为了保持兼容性,可编程定时器8254一直采用这个频率的时钟作为输入。
(2)高精度事件定时器。
高精度事件定时器(High Precision Event Timers)被设计用于取代8254可编程定时器的全部功能和实时钟RTC芯片的周期性中断功能,和8254可编程定时器相比,该定时器能产生更高精度的周期性中断;和实时钟RTC芯片的周期性中断相比,该定时器能提供更高精度、更宽范围的中断频率。
该硬件定时器遵循Intel和Microsoft联合制定的高精度事件定时器规范。该规范中规定一个高精度事件定时器最多拥有32个定时器。通过配置后,timer 0用于取代8254可编程定时器所产生的时钟中断;timer 1作为硬件定时器取代实时钟RTC芯片的周期性中断功能;其余的timer作为硬件定时器供内核或用户进程直接使用。
(3)处理器本地时钟。
在多处理器系统中,处理器本地时钟(CPU Local Timer)用于向本地处理器发送时钟中断请求,更新本地处理器上的相对时间jiffies。
2.其他辅助时钟源
这类辅助时钟源不具备向系统发出中断请求的功能,但有比能产生系统时钟中断的定时器硬件更高的计时精度。在系统时钟中断处理过程中,处理程序可以利用这些时钟的值来完成高精度时间度量,如"6.6微秒级延迟"中的udelay、ndelay就使用了这类的时钟源(如时间戳计数器)完成高精度的延迟。下面介绍常见的这类辅助时钟源。
(1)时间戳计数器。
从Pentium开始,所有的Intel处理器都包含一个64位的寄存器,该寄存器被称为时间戳记数器(Time Stamp Counter,简称为TSC)。TSC在CPU的每个时钟信号到来一次时加1,实际上该寄存器是一个不断增加的计数器,如果处理器的主频为1GHz,那么TSC寄存器的每1ns增加1。汇编指令rdtsc可用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。
(2)电源管理时钟。
内核中,除了使用上面的时间戳记数器作为系统时钟的辅助时钟源外,电源管理时钟(ACPI Power Management Timer)也可作为系统的辅助时钟源。这里对这些时钟源不做详细介绍。
3.与系统时钟相关的宏定义
(1)宏定义HZ。
宏定义Hz记录了不同体系结构下,系统时钟所要求的可编程定时器产生中断的频率。在IA32体系结构下该宏定义在文件src/include/asm-i386/param.h中的第6行定义如下:
#define HZ CONFIG_HZ /* Internal kernel timer frequency */
其中的CONFIG_HZ是内核配置选项,该内核配置选项有3个频率候选值依次是100Hz、1 000Hz、250Hz,分别用于要求高系统吞吐量的服务器系统、要求快速响应的个人桌面计算机系统以及兼有两种类型应用的计算机系统中。
(2)宏定义CLOCK_TICK_RATE。
宏定义CLOCK_TICK_RATE记录了不同体系结构下,驱动可编程定时器工作的输入时钟频率。在IA32体系结构下该值在文件src/include/asm-i386/timex.h中的第15行定义如下:
#define CLOCK_TICK_RATE 1193182 /* Underlying HZ */
其中,数值1 193 182是8254可编程定时器的输入时钟频率。详情请参见本小节中对8254可编程定时器的分析。
(3)宏定义LATCH。
宏定义LATCH记录了上述两个宏定义的比值,用于在内核初始化过程中设置可编程定时器中计数器寄存器counter的初始值。在IA32体系结构下,该宏定义在文件src/include/ Linux/jiffies.h中第46行定义如下:
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
6.2 软件架构
本节介绍Linux内核中和时间度量密切相关的两个重要变量jiffies(jiffies_64)、xtime。这两个变量分别用来记录从系统启动直到当前时刻的系统时钟产生的滴答数和系统的墙上时间。
6.2.1 相对时间(1)
相对时间由系统时钟中断进行维护,存储于系统核心变量jiffies中。该变量记录了系统启动到当前时刻为止系统时钟产生中断的次数,用于提示内核或用户进程一段指定的时间已经过去了,与墙上时间没有直接和必然的联系。
1.jiffies与jiffies_64
在Linux内核中,变量jiffies用于记录系统自启动到当前时刻为止系统时钟产生的滴答数;在系统响应时钟中断、进行中断处理过程中,时钟中断处理程序timer_interrupt()将该变量的值加1。因为一秒钟内产生系统时钟中断次数等于宏定义HZ的值,所以变量jiffies的值在一秒内增加HZ。由此可知,系统自启动到当前时刻为止运行了jiffies/HZ秒。在Linux内核2.4.21中,变量jiffies在文件src/kernel/timer.c中的第68行定义如下。
unsigned long volatile jiffies;
可知该变量jiffies在32位的系统中为32位无符号整型,在64位系统中为64位无符号整型。
为了提高时间度量的准确度,Linux内核2.6中提高了系统时钟中断的频率,从2.4中的100Hz提高到2.6中最高1 000Hz(内核2.6中,时钟中断频率在内核配置阶段可以选100、250、1 000三个频率值之一)。使得原来工作497天才发生溢出的32位的jiffies计数器现在只需49.7天就发生溢出了。溢出发生时会给内核时间度量带来混乱和其他潜在的未知问题,所以内核2.6中引入了一个64位无符号整型变量jiffies_64,使得系统能在系统时钟频率为1 000Hz的情况下运行584 942 417年而不发生溢出,从而有效预防了溢出导致的潜在问题(因为任何人的计算机都不可能连续运行这么长的时间)。
为了保证兼容性以及访问的效率,内核中保留了jiffies这个变量,因为大量的驱动程序使用该变量来进行一些和时间相关的操作;而系统核心使用64位的jiffies_64变量来记录自系统启动到当前时刻为止的时钟滴答次数。关于jiffies变量,在文件src/include/Linux/jiffies.h中的第85行有以下声明:
extern unsigned long volatile __jiffy_data jiffies;
可知该变量在32位的系统中为3