linux内核必须完成两种主要的定时测量:
- 保存当前的时间和日期,以便能通过time, ftime和gettimeofday系统调用把他们返回给用户程序
- 维持定时器,告诉内核或用户程序,某一时间间隔已经过去了。
定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的。
时钟和定时器电路
实时时钟(RTC, Real Time Clock)
它是独立于CPU和所有其他芯片的。
即使切断电源,RTC还继续工作,靠一个小电池或蓄电池供电。CMOS RAM和RTC被集成在一个芯片上。
RTC能在IRQ8上发周期性的中断。linux只用RTC来获取时间和日期。内核通过0x70和0x71 I/O端口访问RTC。
时间戳计数器(TSC, Time Stamp Counter)
所有的80x86微处理器都包含一条CLK输入引线,接受外部振荡器的时钟信号。包含一个计数器,该计数器利用64位的TSC寄存器来实现,可以通过汇编指令rdtsc读这个寄存器。linux在初始化阶段必须确定时钟信号的频率,编译内核时并不声明这个频率,所以内核映像可以运行在不同时钟频率的CPU上。
初始化完成之后,通过calibrate_tsc函数算一个大约在5ms的时间间隔内产生的时钟信号的个数来算出CPU实际频率。
unsigned long __init calibrate_tsc(void)
{
mach_prepare_counter();
{
unsigned long startlow, starthigh;
unsigned long endlow, endhigh;
unsigned long count;
rdtsc(startlow,starthigh);
mach_countup(&count);
rdtsc(endlow,endhigh);
/* Error: ECTCNEVERSET */
if (count <= 1)
goto bad_ctc;
/* 64-bit subtract - gcc just messes up with long longs */
__asm__("subl %2,%0\n\t"
"sbbl %3,%1"
:"=a" (endlow), "=d" (endhigh)
:"g" (startlow), "g" (starthigh),
"0" (endlow), "1" (endhigh));
/* Error: ECPUTOOFAST */
if (endhigh)
goto bad_ctc;
/* Error: ECPUTOOSLOW */
if (endlow <= CALIBRATE_TIME)
goto bad_ctc;
__asm__("divl %2"
:"=a" (endlow), "=d" (endhigh)
:"r" (endlow), "0" (0), "1" (CALIBRATE_TIME));
return endlow;
}
/*
* The CTC wasn't reliable: we got a hit on the very first read,
* or the CPU was so fast/slow that the quotient wouldn't fit in
* 32 bits..
*/
bad_ctc:
return 0;
}
可编程间隔定时器(PIT, Programmable Internal Timer)
IBM兼容PC还包含了第三种时间测量设备,就是PIT。这个设备通过发出一个特殊的中断,叫做时钟中断来通知内核又一个时间间隔过去了。PIT通常是使用0x40 ~ 0x43 I/O端口的一个8254 CMOS芯片。
时钟中断的频率取决于硬件体系结构。
linux中,有几个宏产生决定时钟中断频率的常量:
- HZ产生每秒时钟中断的近似个数,也就是时钟中断的频率。在IBM PC上,这个值为1000
- CLOCK_TICK_RATE产生的值为1193182,是8254芯片的内部振荡器频率。
- LATCH产生CLOCK_TICK_RATE和HZ的比值再四舍五入的整数值。这个值用来对PIT编程。
void setup_pit_timer(void)
{
extern spinlock_t i8253_lock;
unsigned long flags;
spin_lock_irqsave(&i8253_lock, flags);
outb_p(0x34,PIT_MODE); /* binary, mode 2, LSB/MSB, ch 0 */
udelay(10);
outb_p(LATCH & 0xff , PIT_CH0); /* LSB */
udelay(10);
outb(LATCH >> 8 , PIT_CH0); /* MSB */
spin_unlock_irqrestore(&i8253_lock, flags);
}
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)
#define PIT_MODE 0x43
#define PIT_CH0 0x40
#define PIT_CH2 0x42
CPU本地定时器
在最近80x86的本地APIC中,还提供了CPU本地定时器,这个是一种能够提供单步中断和周期性中断的设备。它与可编程间隔定时器不同的是:
- APIC计数器时32位,而PIC计数器时16位;因此,可以对本地定时器编程来产生很低频率的中断。
- 本地APIC定时器把中断只发送给自己的处理器,而PIT产生一个全局性中断,系统中的任一CPU都可以对其处理。
- APIC定时器是基于总线时钟信号的,PIT有其自己的内部时钟振荡器,可以灵活编程。
高精度事件定时器(HPET)
HPET是由Intel和Microsoft联合开发的一种新型定时器芯片。
ACPI电源管理定时器
它的时钟信号拥有大约3.58MHz的固定频率。为了读取计数器的当前值,内核需要访问某个I/O端口,这个I/O端口的地址由BIOS在初始化阶段确定。
linux计时体系结构
基于80x86多处理器机器所具有的计时体系结构和单处理器机器所具有的稍有不同:
- 在单处理器上,所有的计时活动都是由全局定时器产生的中断触发的。
- 在多处理器,所有普通的活动都是由全局定时器产生的中断触发的,具体CPU的活动都是由本地APIC定时器产生的中断触发的。
内核使用两种基本的计时函数:一个保持当前最新的时间,另一个计算在当前秒内走过的纳秒数。
计时体系结构的数据结构
定时器对象
它是timer_opts类型的一个描述符。
struct timer_opts {
char* name; //标识定时器源的一个字符串
void (*mark_offset)(void); //记录上一个节拍的准确时间,由时钟中断处理程序调用
unsigned long (*get_offset)(void); //返回自上一个节拍开始所经过的纳秒数
unsigned long long (*monotonic_clock)(void);//返回自内核初始化开始所经过的纳秒数
void (*delay)(unsigned long); //等待指定数目的“循环”
};
其中最重要的时mark_offset和get_offset两个字段。由于这两种方法,linux计时体系结构能够达到子节拍的分辨度。内核能以比节拍周期更高的精度来测定当前的时间,这种操作叫做“定时插补(time interpolation)”。cur_timer存放了某个定时器的地址,该定时器时系统可利用的定时器资源中“最好的”。最初cur_timer指向timer_zone,这是一个虚拟的定时器资源对象。内核初始化期间,select_timer函数设置cur_timer指向适当定时器对象的地址。
struct timer_opts *cur_timer = &timer_none;
struct timer_opts* __init select_timer(void)
{
int i = 0;
//优先选择HPET;否则,将选择ACPI电源管理定时器;再次之使TSC;最后方案选择总是存在PIT。
/* find most preferred working timer */
while (timers[i]) {
if (timers[i]->init)
if (timers[i]->init(clock_override) == 0)
return timers[i]->opts;
++i;
}
panic("select_timer: Cannot find a suitable timer\n");
return NULL;
}
void __init time_init(void)
{
#ifdef CONFIG_HPET_TIMER
if (is_hpet_capable()) {
/*
* HPET initialization needs to do memory-mapped io. So, let
* us do a late initialization after mem_init().
*/
late_time_init = hpet_time_init;
return;
}
#endif
xtime.tv_sec = get_cmos_time();
xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);
set_normalized_timespec(&wall_to_monotonic,
-xtime.tv_sec, -xtime.tv_nsec);
cur_timer = select_timer();
printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);
time_init_hook();
}
在time_init中通过select_timer返回值设置cur_timer。
static struct init_timer_opts* __initdata timers[] = {
#ifdef CONFIG_X86_CYCLONE_TIMER
&timer_cyclone_init,
#endif
#ifdef CONFIG_HPET_TIMER
&timer_hpet_init,
#endif
#ifdef CONFIG_X86_PM_TIMER
&timer_pmtmr_init,
#endif
&timer_tsc_init,
&timer_pit_init,
NULL,
};
struct init_timer_opts {
int (*init)(char *override);
struct timer_opts *opts;
};
本地APIC定时器没有对应的定时器对象,因为本地APIC定时器仅用来产生周期性中断而从不用来获得子节拍的分辨度。
jiffies变量
这是一个计数器,用来记录系统启动以来产生的节拍总数。每次时钟中断发生时,它便加1。80x86体系结构中,jiffies是一个32位的变量,每隔大约50天它的值会回绕到0,。使用了time_after, time_after_eq, time_before和time_before_eq四个宏,内核处理了jiffies变量的溢出。
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
jiffies被初始化为fffb6c20,它是32位有符号值,等于-300000。所以,计数器将会在系统启动后的5分钟内处于溢出状态。这样做,使得那些不对jiffies作溢出检测的内核代码在开发阶段被及时发现,从而不再出现在稳定版本中。
linux需要自系统启动以来产生的系统节拍的真实数目。所以,jiffies变量通过连接器被换算成一个64位计数器的低32位,被称作为jiffies_64。