Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1428089
  • 博文数量: 430
  • 博客积分: 9995
  • 博客等级: 中将
  • 技术积分: 4388
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-24 18:04
文章存档

2013年(1)

2008年(2)

2007年(14)

2006年(413)

分类:

2006-06-12 19:27:18

核主要需要两种类型的时间: 1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。 2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。 PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。

核主要需要两种类型的时间:
1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用
户通过时间syscall进行检索。
2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件
是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)
时间戳计数器(Time Stamp Counter,TSC)。

7.1 时钟硬件
7.1.1 实时时钟RTC
自从IBM PC AT起,所有的PC机就都包含了一个叫做实时时钟(RTC)的时钟芯片,以便在PC机断电后仍然能够继续保持时间。显
然,RTC是通过主板上的电池来供电的,而不是通过PC机电源来供电的,因此当PC机关掉电源后,RTC仍然会继续工作。通
常,CMOS RAM和RTC被集成到一块芯片上,因此RTC也称作“CMOS Timer”。最常见的RTC芯片是MC146818(Motorola)和
DS12887(maxim),DS12887完全兼容于MC146818,并有一定的扩展。本节内容主要基于MC146818这一标准的RTC芯片。具体内
容可以参考MC146818的Datasheet。

7.1.1.1 RTC寄存器
MC146818 RTC芯片一共有64个寄存器。它们的芯片内部地址编号为0x00~0x3F(不是I/O端口地址),这些寄存器一共可以分为
三组:
(1)时钟与日历寄存器组:共有10个(0x00~0x09),表示时间、日历的具体信息。在PC机中,这些寄存器中的值都是以BCD格式
来存储的(比如23dec=0x23BCD)。
(2)状态和控制寄存器组:共有4个(0x0A~0x0D),控制RTC芯片的工作方式,并表示当前的状态。
(3)CMOS配置数据:通用的CMOS RAM,它们与时间无关,因此我们不关心它。
时钟与日历寄存器组的详细解释如下:
Address Function
00 Current second for RTC
01 Alarm second
02 Current minute
03 Alarm minute
04 Current hour
05 Alarm hour
06 Current day of week(01=Sunday)
07 Current date of month
08 Current month
09 Current year(final two digits,eg:93)

状态寄存器A(地址0x0A)的格式如下:
其中:
(1)bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问
的(此时访问它们将得到一个无意义的渐变值)。
(2)bit[6:4]——这三位是“除法器控制位”(divider-control bits),用来定义RTC的操作频率。各种可能的值如下:
Divider bits Time-base frequency Divider Reset Operation Mode
DV2 DV1 DV0
0 0 0 4.194304 MHZ NO YES
0 0 1 1.048576 MHZ NO YES
0 1 0 32.769 KHZ NO YES
1 1 0/1 任何 YES NO
PC机通常将Divider bits设置成“010”。
(3)bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS bits 4.194304或1.048578 MHZ 32.768 KHZ
RS3 RS2 RS1 RS0 周期性中断 方波 周期性中断 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
PC机BIOS对其默认的设置值是“0110”。

状态寄存器B的格式如下所示:
各位的含义如下:
(1)bit[7]——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0
表示将允许更新过程继续。
(2)bit[6]——PIE标志,周期性中断使能标志。
(3)bit[5]——AIE标志,告警中断使能标志。
(4)bit[4]——UIE标志,更新结束中断使能标志。
(5)bit[3]——SQWE标志,方波信号使能标志。
(6)bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。BIOS总是将它设置为0。
(7)bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PC机BIOS总是将它设置为1。
(8)bit[0]——DSE标志。BIOS总是将它设置为0。

状态寄存器C的格式如下:
(1)bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求发生。
(2)bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
(3)bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
(4)bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。

状态寄存器D的格式如下:
(1)bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC已经掉电。
(2)bit[6:0]——总是为0,未定义。

7.1.1.2 通过I/O端口访问RTC
在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端
口。
读RTC芯片寄存器的步骤是:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
in al, 71h ;
写RTC寄存器的步骤如下:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
mov al, value
out 71h, al

7.1.2 可编程间隔定时器PIT
每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址
是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-
cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0
时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处于“Terminal
count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生
18.2次时钟中断。

7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
Port Description
40h Channel 0 counter(read/write)
41h Channel 1 counter(read/write)
42h Channel 2 counter(read/write)
43h PIT control word(write only)
其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读
写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控
制寄存器来决定。8254 PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。“00”表示选择Counter 0,“01”表示选择Counter
1,“10”表示选择Counter 2,“11”表示Read-Back Command(仅对于8254,对于8253无效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示锁存(Latch)当前计数器的值;“01”只读写计数器的高字节
(MSB);“10”只读写计数器的低字节(LSB);“11”表示先读写计数器的LSB,再读写MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”对应Mode 0;“001”对应Mode 1;“010”对应Mode 2;“011”
对应Mode 3;“100”对应Mode 4;“101”对应Mode 5。
(4)bit[0]——控制计数器的存储模式。0表示以二进制格式存储,1表示计数器中的值以BCD格式存储。

7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6种模式下:
1. Mode 0:当通道处于“Terminal count”状态时产生中断信号。
2. Mode 1:Hardware retriggerable one-shot。
3. Mode 2:Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平,
并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间,OUT管脚将变为低电平,直到计数器的值减到0。当计数
器的值被自动地重新加载后,OUT管脚又变成高电平,然后重复上述过程。通道0通常工作在这个模式下。
4. Mode 3:方波信号发生器。
5. Mode 4:Software triggered strobe。
6. Mode 5:Hardware triggered strobe。

7.1.2.3 锁存计数器(Latch Counter)
当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值,因为计
数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。

7.1.3 时间戳记数器TSC
从Pentium开始,所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增
加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是
400MHZ,那么TSC就将每2.5纳秒增加一次。

7.2 Linux内核对RTC的编程
MC146818 RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上产生周期性的中断,中断的频率在2HZ~8192HZ之间。与
MC146818 RTC对应的设备驱动程序实现在include/linux/rtc.h和drivers/char/rtc.c文件中,对应的设备文件是/dev/
rtc(major=10,minor=135,只读字符设备)。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活
IRQ8线,从而将RTC当作一个闹钟来用。
而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时,它从RTC中读取时间与
日期的基准值。然后再运行期间内核就完全抛开RTC,从而以软件的形式维护系统的当前时间与日期,并在需要时将时间回写到RTC
芯片中。
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义了mc146818 RTC芯片各寄
存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件中。

7.2.1 RTC芯片的I/O端口操作
Linux在include/asm-i386/mc146818rtc.h头文件中定义了RTC芯片的I/O端口操作。端口0x70被称为“RTC端口0”,端口0x71
被称为“RTC端口1”,如下所示:
#ifndef RTC_PORT
#define RTC_PORT(x) (0x70 + (x))
#define RTC_ALWAYS_BCD 1 /* RTC operates in binary mode */
#endif
显然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。
端口0x70被用作RTC芯片内部寄存器的地址索引端口,而端口0x71则被用作RTC芯片内部寄存器的数据端口。再读写一个RTC寄存器
之前,必须先把该寄存器在RTC芯片内部的地址索引值写到端口0x70中。根据这一点,读写一个RTC寄存器的宏定义CMOS_READ()和
CMOS_WRITE()如下:
#define CMOS_READ(addr) ({
outb_p((addr),RTC_PORT(0));
inb_p(RTC_PORT(1));
})
#define CMOS_WRITE(val, addr) ({
outb_p((addr),RTC_PORT(0));
outb_p((val),RTC_PORT(1));
})
#define RTC_IRQ 8
在上述宏定义中,参数addr是RTC寄存器在芯片内部的地址值,取值范围是0x00~0x3F,参数val是待写入寄存器的值。宏RTC_IRQ
是指RTC芯片所连接的中断请求输入线号,通常是8。

7.2.2 对RTC寄存器的定义
Linux在include/linux/mc146818rtc.h这个头文件中定义了RTC各寄存器的含义。

(1)寄存器内部地址索引的定义
Linux内核仅使用RTC芯片的时间与日期寄存器组和控制寄存器组,地址为0x00~0x09之间的10个时间与日期寄存器的定义如下:
#define RTC_SECONDS 0
#define RTC_SECONDS_ALARM 1
#define RTC_MINUTES 2
#define RTC_MINUTES_ALARM 3
#define RTC_HOURS 4
#define RTC_HOURS_ALARM 5
/* RTC_*_alarm is always true if 2 MSBs are set */
# define RTC_ALARM_DONT_CARE 0xC0

#define RTC_DAY_OF_WEEK 6
#define RTC_DAY_OF_MONTH 7
#define RTC_MONTH 8
#define RTC_YEAR 9

四个控制寄存器的地址定义如下:
#define RTC_REG_A 10
#define RTC_REG_B 11
#define RTC_REG_C 12
#define RTC_REG_D 13

(2)各控制寄存器的状态位的详细定义
控制寄存器A(0x0A)主要用于选择RTC芯片的工作频率,因此也称为RTC频率选择寄存器。因此Linux用一个宏别名
RTC_FREQ_SELECT来表示控制寄存器A,如下:
#define RTC_FREQ_SELECT RTC_REG_A
RTC频率寄存器中的位被分为三组:①bit[7]表示UIP标志;②bit[6:4]用于除法器的频率选择;③bit[3:0]用于速率选
择。它们的定义如下:
# define RTC_UIP 0x80
# define RTC_DIV_CTL 0x70
/* Periodic intr. / Square wave rate select. 0=none, 1=32.8kHz,... 15=2Hz */
# define RTC_RATE_SELECT 0x0F
正如7.1.1.1节所介绍的那样,bit[6:4]有5中可能的取值,分别为除法器选择不同的工作频率或用于重置除法器,各种可能的
取值如下定义所示:
/* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */
# define RTC_REF_CLCK_4MHZ 0x00
# define RTC_REF_CLCK_1MHZ 0x10
# define RTC_REF_CLCK_32KHZ 0x20
/* 2 values for divider stage reset, others for "testing purposes only" */
# define RTC_DIV_RESET1 0x60
# define RTC_DIV_RESET2 0x70

寄存器B中的各位用于使能/禁止RTC的各种特性,因此控制寄存器B(0x0B)也称为“控制寄存器”,Linux用宏别名RTC_CONTROL
来表示控制寄存器B,它与其中的各标志位的定义如下所示:
#define RTC_CONTROL RTC_REG_B
# define RTC_SET 0x80 /* disable updates for clock setting */
# define RTC_PIE 0x40 /* periodic interrupt enable */
# define RTC_AIE 0x20 /* alarm interrupt enable */
# define RTC_UIE 0x10 /* update-finished interrupt enable */
# define RTC_SQWE 0x08 /* enable square-wave output */
# define RTC_DM_BINARY 0x04 /* all time/date values are BCD if clear */
# define RTC_24H 0x02 /* 24 hour mode - else hours bit 7 means pm */
# define RTC_DST_EN 0x01 /* auto switch DST - works f. USA only */

寄存器C是RTC芯片的中断请求状态寄存器,Linux用宏别名RTC_INTR_FLAGS来表示寄存器C,它与其中的各标志位的定义如下所
示:
#define RTC_INTR_FLAGS RTC_REG_C
/* caution - cleared by read */
# define RTC_IRQF 0x80 /* any of the following 3 is active */
# define RTC_PF 0x40
# define RTC_AF 0x20
# define RTC_UF 0x10

寄存器D仅定义了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也称为RTC的有效寄存器。Linux用宏别名RTC_VALID
来表示寄存器D,如下:
#define RTC_VALID RTC_REG_D
# define RTC_VRT 0x80 /* valid RAM and time */

(3)二进制格式与BCD格式的相互转换
由于时间与日期寄存器中的值可能以BCD格式存储,也可能以二进制格式存储,因此需要定义二进制格式与BCD格式之间的相互转换
宏,以方便编程。如下:
#ifndef BCD_TO_BIN
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
#endif

#ifndef BIN_TO_BCD
#define BIN_TO_BCD(val) ((val)=(((val)/10)= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */
mon += 12; /* Puts Feb last since it has leap day */
year -= 1;
}

return (((
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +
year*365 - 719499
)*24 + hour /* now have hours */
)*60 + min /* now have minutes */
)*60 + sec; /* finally seconds */
}

(3)set_rtc_mmss()函数
该函数用来更新RTC中的时间,它仅有一个参数nowtime,是以秒数表示的当前时间,其源码如下:
static int set_rtc_mmss(unsigned long nowtime)
{
int retval = 0;
int real_seconds, real_minutes, cmos_minutes;
unsigned char save_control, save_freq_select;

/* gets recalled with irq locally disabled */
spin_lock(&rtc_lock);
save_control = CMOS_READ(RTC_CONTROL); /* tell the clock it's being set */
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL);

save_freq_select = CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT);

cmos_minutes = CMOS_READ(RTC_MINUTES);
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
BCD_TO_BIN(cmos_minutes);

/*
* since we're only adjusting minutes and seconds,
* don't interfere with hour overflow. This avoids
* messing with unknown time zones but requires your
* RTC not to be off by more than 15 minutes
*/
real_seconds = nowtime % 60;
real_minutes = nowtime / 60;
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1)
real_minutes += 30; /* correct for half hour time zone */
real_minutes %= 60;

if (abs(real_minutes - cmos_minutes) tv_usec>=
0。
Linux内核通过timeval结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
但是,全局变量xtime所维持的当前时间通常是供用户来检索和设置的,而其他内核模块通常很少使用它(其他内核模块用得最多的
是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部分(bottom half)中
来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似
于jiffies的全局变量wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时
侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
③全局变量sys_tz:它是一个timezone结构类型的全局变量,表示系统当前的时区信息。结构类型timezone定义在include/
linux/time.h头文件中,如下所示:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of dst correction */
};
基于上述结构,Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处的时区信息,如下所示:
struct timezone sys_tz;

7.3.3 Linux对TSC的编程实现
Linux用定义在arch/i386/kernel/time.c文件中的全局变量use_tsc来表示内核是否使用CPU的TSC寄存器,use_tsc=1表示
使用TSC,use_tsc=0表示不使用TSC。该变量的值是在time_init()初始化函数中被初始化的(详见下一节)。该变量的定义如
下:
static int use_tsc;
宏cpu_has_tsc可以确定当前系统的CPU是否配置有TSC寄存器。此外,宏CONFIG_X86_TSC也表示是否存在TSC寄存器。

7.3.3.1 读TSC寄存器的宏操作
x86 CPU的rdtsc指令将TSC寄存器的高32位值读到EDX寄存器中、低32位读到EAX寄存器中。Linux根据不同的需要,在rdtsc指
令的基础上封装几个高层宏操作,以读取TSC寄存器的值。它们均定义在include/asm-i386/msr.h头文件中,如下:
#define rdtsc(low,high)
__asm__ __volatile__("rdtsc" : "=a" (low), "=d" (high))

#define rdtscl(low)
__asm__ __volatile__ ("rdtsc" : "=a" (low) : : "edx")

#define rdtscll(val)
__asm__ __volatile__ ("rdtsc" : "=A" (val))
宏rdtsc()同时读取TSC的LSB与MSB,并分别保存到宏参数low和high中。宏rdtscl则只读取TSC寄存器的LSB,并保存到宏参数
low中。宏rdtscll读取TSC的当前64位值,并将其保存到宏参数val这个64位变量中。

7.3.3.2 校准TSC
与可编程定时器PIT相比,用TSC寄存器可以获得更精确的时间度量。但是在可以使用TSC之前,它必须精确地确定1个TSC计数值到
底代表多长的时间间隔,也即到底要过多长时间间隔TSC寄存器才会加1。Linux内核用全局变量fast_gettimeoffset_quotient
来表示这个值,其定义如下(arch/i386/kernel/time.c):
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
根据上述定义的注释我们可以看出,这个变量的值是通过下述公式来计算的:
fast_gettimeoffset_quotient = (2^32) / (每微秒内的时钟周期个数)
定义在arch/i386/kernel/time.c文件中的函数calibrate_tsc()就是根据上述公式来计算fast_gettimeoffset_quotient
的值的。显然这个计算过程必须在内核启动时完成,因此,函数calibrate_tsc()只被初始化函数time_init()所调用。

用TSC实现高精度的时间服务
在拥有TSC(TimeStamp Counter)的x86 CPU上,Linux内核可以实现微秒级的高精度定时服务,也即可以确定两次时钟中断之
间的某个时刻的微秒级时间值。如下图所示:
图7-7 TSC时间关系

从上图中可以看出,要确定时刻x的微秒级时间值,就必须确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值
(以微秒为单位)。为此,内核定义了以下两个变量:
(1)中断服务执行延迟delay_at_last_interrupt:由于从产生时钟中断的那个时刻到内核时钟中断服务函数
timer_interrupt真正在CPU上执行的那个时刻之间是有一段延迟间隔的,因此,Linux内核用变量delay_at_last_interrupt
来表示这一段时间延迟间隔,其定义如下(arch/i386/kernel/time.c):
/* Number of usecs that the last interrupt was delayed */
static int delay_at_last_interrupt;
关于delay_at_last_interrupt的计算步骤我们将在分析timer_interrupt()函数时讨论。
(2)全局变量last_tsc_low:它表示中断服务timer_interrupt真正在CPU上执行时刻的TSC寄存器值的低32位(LSB)。
显然,通过delay_at_last_interrupt、last_tsc_low和时刻x处的TSC寄存器值,我们就可以完全确定时刻x距上一次时钟中
断产生时刻的时间间隔偏移offset_usec的值。实现在arch/i386/kernel/time.c中的函数do_fast_gettimeoffset()就是这
样计算时间间隔偏移的,当然它仅在CPU配置有TSC寄存器时才被使用,后面我们会详细分析这个函数。

7.4 时钟中断的驱动
如前所述,8253/8254 PIT的通道0通常被用来在IRQ0上产生周期性的时钟中断。对时钟中断的驱动是绝大数操作系统内核实现
time-keeping的关键所在。不同的OS对时钟驱动的要求也不同,但是一般都包含下列要求内容:
1. 维护系统的当前时间与日期。
2. 防止进程运行时间超出其允许的时间。
3. 对CPU的使用情况进行记帐统计。
4. 处理用户进程发出的时间系统调用。
5. 对系统某些部分提供监视定时器。
其中,第一项功能是所有OS都必须实现的基础功能,它是OS内核的运行基础。通常有三种方法可用来维护系统的时间与日期:(1)
最简单的一种方法就是用一个64位的计数器来对时钟滴答进行计数。(2)第二种方法就是用一个32位计数器来对秒进行计数。用一
个32位的辅助计数器来对时钟滴答计数直至累计一秒为止。因为232超过136年,因此这种方法直至22世纪都可以工作得很
好。(3)第三种方法也是按滴答进行计数,但却是相对于系统启动以来的滴答次数,而不是相对于一个确定的外部时刻。当读后备
时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。
UNIX类的OS通常都采用第三种方法来维护系统的时间与日期。

7.4.1 Linux对时钟中断的初始化
Linux对时钟中断的初始化是分为几个步骤来进行的:(1)首先,由init_IRQ()函数通过调用init_ISA_IRQ()函数对中断向量
32~256所对应的中断向量描述符进行初始化设置。显然,这其中也就把IRQ0(也即中断向量32)的中断向量描述符初始化
了。(2)然后,init_IRQ()函数设置中断向量32~256相对应的中断门。(3)init_IRQ()函数对PIT进行初始化编
程;(4)sched_init()函数对计数器、时间中断的Bottom Half进行初始化。(5)最后,由time_init()函数对Linux内核的
时钟中断机制进行初始化。这三个初始化函数都是由init/main.c文件中的start_kernel()函数调用的,如下:
asmlinkage void __init start_kernel()
{

trap_init();
init_IRQ();
sched_init();
time_init();
softirq_init();

}

(1)init_IRQ()函数对8254 PIT的初始化编程
函数init_IRQ()函数在完成中断门的初始化后,就对8254 PIT进行初始化编程设置,设置的步骤如下:(1)设置8254 PIT的控
制寄存器(端口0x43)的值为“01100100”,也即选择通道0、先读写LSB再读写MSB、工作模式2、二进制存储格式。(2)将宏
LATCH的值写入通道0的计数器中(端口0x40),注意要先写LATCH的LSB,再写LATCH的高字节。其源码如下所示(arch/i386/
kernel/i8259.c):
void __init init_IRQ(void)
{
……
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
……
}

(2)sched_init()对定时器机制和时钟中断的Bottom Half的初始化
函数sched_init()中与时间相关的初始化过程主要有两步:(1)调用init_timervecs()函数初始化内核定时器机制;(2)调
用init_bh()函数将BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所对应的BH函数分别设置成timer_bh()、tqueue_bh()和
immediate_bh()函数。如下所示(kernel/sched.c):
void __init sched_init(void)
{
……
init_timervecs();

init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
……
}

(3)time_init()函数对内核时钟中断机制的初始化
前面两个函数所进行的初始化步骤都是为时间中断机制做好准备而已。在执行完init_IRQ()函数和sched_init()函数后,CPU已
经可以为IRQ0上的时钟中断进行服务了,因为IRQ0所对应的中断门已经被设置好指向中断服务函数IRQ0x20_interrupt()。但是
由于此时中断向量0x20的中断向量描述符irq_desc[0]还是处于初始状态(其status成员的值为IRQ_DISABLED),并未挂接任
何具体的中断服务描述符,因此这时CPU对IRQ0的中断服务并没有任何具体意义,而只是按照规定的流程空跑一趟。但是当CPU执行
完time_init()函数后,情形就大不一样了。
函数time_init()主要做三件事:(1)从RTC中获取内核启动时的时间与日期;(2)在CPU有TSC的情况下校准TSC,以便为后面
使用TSC做好准备;(3)在IRQ0的中断请求描述符中挂接具体的中断服务描述符。其源码如下所示(arch/i386/kernel/
time.c):
void __init time_init(void)
{
extern int x86_udelay_tsc;

xtime.tv_sec = get_cmos_time();
xtime.tv_usec = 0;

/*
* If we have APM enabled or the CPU clock speed is variable
* (CPU stops clock on HLT or slows clock to save power)
* then the TSC timestamps may diverge by up to 1 jiffy from
* 'real time' but nothing will break.
* The most frequent case is that the CPU is "woken" from a halt
* state by the timer interrupt itself, so we get 0 error. In the
* rare cases where a driver would "wake" the CPU and request a
* timestamp, the maximum error is handler函数指针所指向的
timer_interrupt()函数对时钟中断请求进行真正的服务,而不是向前面所说的那样只是让CPU“空跑”一趟。此时,Linux内核可
以说是真正的“跳动”起来了。
在本节一开始所述的对时钟中断驱动的5项要求中,通常只有第一项(即timekeeping)是最为迫切的,因此必须在时钟中断服务例
程中完成。而其余的几个要求可以稍缓,因此可以放在时钟中断的Bottom Half中去执行。这样,Linux内核就是
timer_interrupt()函数的执行时间尽可能的短,因为它是在CPU关中断的条件下执行的。
函数timer_interrupt()的源码如下(arch/i386/kernel/time.c):
/*
* This is the same as the above, except we _also_ save the current
* Time Stamp Counter value at the time of the timer interrupt, so that
* we later on can estimate the time of day more exactly.
*/
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int count;

/*
* Here we are in the timer irq handler. We just have irqs locally
* disabled but we don't know if the timer_bh is running on the other
* CPU. We need to avoid to SMP race with it. NOTE: we don' t need
* the irq version of write_lock because as just said we have irq
* locally disabled. -arca
*/
write_lock(&xtime_lock);

if (use_tsc)
{
/*
* It is important that these two operations happen almost at
* the same time. We do the RDTSC stuff first, since it's
* faster. To avoid any inconsistencies, we need interrupts
* disabled locally.
*/

/*
* Interrupts are just disabled locally since the timer irq
* has the SA_INTERRUPT flag set. -arca
*/

/* read Pentium cycle counter */

rdtscl(last_tsc_low);

spin_lock(&i8253_lock);
outb_p(0x00, 0x43); /* latch the count ASAP */

count = inb_p(0x40); /* read the latched count */
count |= inb(0x40) last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec eflags) || (3 & (regs)->xcs))
……
#endif
(3)调用mark_bh()函数激活时钟中断的Bottom Half向量TIMER_BH和TQUEUE_BH(注意,TQUEUE_BH仅在任务队列tq_timer
不为空的情况下才会被激活)。

至此,内核对时钟中断的服务流程宣告结束,下面我们详细分析一下update_process_times()函数的实现。

7.4.3 更新时间记帐信息——CPU分时的实现
函数update_process_times()被用来在发生时钟中断时更新当前进程以及内核中与时间相关的统计信息,并根据这些信息作出相
应的动作,比如:重新进行调度,向当前进程发出信号等。该函数仅有一个参数user_tick,取值为1或0,其含义在前面已经叙述
过。
该函数的源代码如下(kernel/timer.c):
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;

update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
(1)首先,用smp_processor_id()宏得到当前进程的CPU ID。
(2)然后,让局部变量system=user_tick^1,表示当发生时钟中断时CPU是否正处于核心态下。因此,如果user_tick=1,则
system=0;如果user_tick=0,则system=1。
(3)调用update_one_process()函数来更新当前进程的task_struct结构中的所有与时间相关的统计信息以及成员变量。该函
数还会视需要向当前进程发送相应的信号(signal)。
(4)如果当前进程的PID非0,则执行下列步骤来决定是否重新进行调度,并更新内核时间统计信息:
l 将当前进程的可运行时间片长度(由task_struct结构中的counter成员表示,其单位是时钟滴答次数)减1。如果减到0值,则
说明当前进程已经用完了系统分配给它的的运行时间片,因此必须重新进行调度。于是将当前进程的task_struct结构中的
need_resched成员变量设置为1,表示需要重新执行调度。
l 如果当前进程的task_struct结构中的nice成员值大于0,那么将内核全局统计信息变量kstat中的per_cpu_nice[cpu]值将
上user_tick。否则就将user_tick值加到内核全局统计信息变量kstat中的per_cpu_user[cpu]成员上。
l 将system变量值加到内核全局统计信息kstat.per_cpu_system[cpu]上。
(5)否则,就判断当前CPU在服务时钟中断前是否处于softirq软中断服务的执行中,或则正在服务一次低优先级别的硬件中断
中。如果是这样的话,则将system变量的值加到内核全局统计信息kstat.per_cpu.system[cpu]上。

l update_one_process()函数
实现在kernel/timer.c文件中的update_one_process()函数用来在时钟中断发生时更新一个进程的task_struc结构中的时间
统计信息。其源码如下(kernel/timer.c):

void update_one_process(struct task_struct *p, unsigned long user,
unsigned long system, int cpu)
{
p->per_cpu_utime[cpu] += user;
p->per_cpu_stime[cpu] += system;
do_process_times(p, user, system);
do_it_virt(p, user);
do_it_prof(p);
}
注释如下:
(1)由于在一个进程的整个生命期(Lifetime)中,它可能会在不同的CPU上执行,也即一个进程可能一开始在CPU1上执行,当
它用完在CPU1上的运行时间片后,它可能又会被调度到CPU2上去执行。另外,当进程在某个CPU上执行时,它可能又会在用户态和
内核态下分别各执行一段时间。所以为了统计这些事件信息,进程task_struct结构中的per_cpu_utime[NR_CPUS]数组就表示
该进程在各CPU的用户台下执行的累计时间长度,per_cpu_stime[NR_CPUS]数组就表示该进程在各CPU的核心态下执行的累计时
间长度;它们都以时钟滴答次数为单位。
所以,update_one_process()函数的第一个步骤就是更新进程在当前CPU上的用户态执行时间统计per_cpu_utime[cpu]和核
心态执行时间统计per_cpu_stime[cpu]。
(2)调用do_process_times()函数更新当前进程的总时间统计信息。
(3)调用do_it_virt()函数为当前进程的ITIMER_VIRTUAL软件定时器更新时间间隔。
(4)调用do_it_prof()函数为当前进程的ITIMER_PROF软件定时器更新时间间隔。

l do_process_times()函数
函数do_process_times()将更新指定进程的总时间统计信息。每个进程task_struct结构中都有一个成员times,它是一个tms
结构类型(include/linux/times.h):
struct tms {
clock_t tms_utime; /* 本进程在用户台下的执行时间总和 */
clock_t tms_stime; /* 本进程在核心态下的执行时间总和 */
clock_t tms_cutime; /* 所有子进程在用户态下的执行时间总和 */
clock_t tms_cstime; /* 所有子进程在核心态下的执行时间总和 */
};
上述结构的所有成员都以时钟滴答次数为单位。
函数do_process_times()的源码如下(kernel/timer.c):
static inline void do_process_times(struct task_struct *p,
unsigned long user, unsigned long system)
{
unsigned long psecs;

psecs = (p->times.tms_utime += user);
psecs += (p->times.tms_stime += system);
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_cur) {
/* Send SIGXCPU every second.. */
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
/* and SIGKILL when we go over max.. */
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_max)
send_sig(SIGKILL, p, 1);
}
}
注释如下:
(1)根据参数user更新指定进程task_struct结构中的times.tms_utime值。根据参数system更新指定进程task_struct结构
中的times.tms_stime值。
(2)将更新后的times.tms_utime值与times.tms_stime值的和保存到局部变量psecs中,因此psecs就表示了指定进程p到目
前为止已经运行的总时间长度(以时钟滴答次数计)。如果这一总运行时间长超过进程P的资源限额,那就每隔1秒给进程发送一个信
号SIGXCPU;如果运行时间长度超过了进程资源限额的最大值,那就发送一个SIGKILL信号杀死该进程。

l do_it_virt()函数
每个进程都有一个用户态执行时间的itimer软件定时器。进程任务结构task_struct中的it_virt_value成员是这个软件定时器
的时间计数器。当进程在用户态下执行时,每一次时钟滴答都使计数器it_virt_value减1,当减到0时内核向进程发送SIGVTALRM
信号,并重置初值。初值保存在进程的task_struct结构的it_virt_incr成员中。
函数do_it_virt()的源码如下(kernel/timer.c):
static inline void do_it_virt(struct task_struct * p, unsigned long ticks)
{
unsigned long it_virt = p->it_virt_value;

if (it_virt) {
it_virt -= ticks;
if (!it_virt) {
it_virt = p->it_virt_incr;
send_sig(SIGVTALRM, p, 1);
}
p->it_virt_value = it_virt;
}
}

l do_it_prof()函数
类似地,每个进程也都有一个itimer软件定时器ITIMER_PROF。进程task_struct中的it_prof_value成员就是这个定时器的时
间计数器。不管进程是在用户态下还是在内核态下运行,每个时钟滴答都使it_prof_value减1。当减到0时内核就向进程发送
SIGPROF信号,并重置初值。初值保存在进程task_struct结构中的it_prof_incr成员中。
函数do_it_prof()就是用来完成上述功能的,其源码如下(kernel/timer.c):
static inline void do_it_prof(struct task_struct *p)
{
unsigned long it_prof = p->it_prof_value;

if (it_prof) {
if (--it_prof == 0) {
it_prof = p->it_prof_incr;
send_sig(SIGPROF, p, 1);
}
p->it_prof_value = it_prof;
}
}
7.5 时钟中断的Bottom Half
与时钟中断相关的Bottom Half向两主要有两个:TIMER_BH和TQUEUE_BH。与TIMER_BH相对应的BH函数是timer_bh(),与
TQUEUE_BH对应的函数是tqueue_bh()。它们均实现在kernel/timer.c文件中。

7.5.1 TQUEUE_BH向量
TQUEUE_BH的作用是用来运行tq_timer这个任务队列中的任务。因此do_timer()函数仅仅在tq_timer任务队列不为空的情况才
激活TQUEUE_BH向量。函数tqueue_bh()的实现非常简单,它只是简单地调用run_task_queue()函数来运行任务队列
tq_timer。如下所示:
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}
任务对列tq_timer也是定义在kernel/timer.c文件中,如下所示:
DECLARE_TASK_QUEUE(tq_timer);

7.5.2 TIMER_BH向量
TIMER_BH这个Bottom Half向量是Linux内核时钟中断驱动的一个重要辅助部分。内核在每一次对时钟中断的服务快要结束时,都
会无条件地激活一个TIMER_BH向量,以使得内核在稍后一段延迟后执行相应的BH函数——timer_bh()。该任务的源码如下:
void timer_bh(void)
{
update_times();
run_timer_list();
}
从上述源码可以看出,内核在时钟中断驱动的底半部分主要有两个任务:(1)调用update_times()函数来更新系统全局时间
xtime;(2)调用run_timer_list()函数来执行定时器。关于定时器我们将在下一节讨论。本节我们主要讨论TIMER_BH的第一
个任务——对内核时间xtime的更新。
我们都知道,内核局部时间xtime是用来供用户程序通过时间syscall来检索或设置当前系统时间的,而内核代码在大多数情况下都
引用jiffies变量,而很少使用xtime(偶尔也会有引用xtime的情况,比如更新inode的时间标记)。因此,对于时钟中断服务程
序timer_interrupt()而言,jiffies变量的更新是最紧迫的,而xtime的更新则可以延迟到中断服务的底半部分来进行。
由于Bottom Half机制在执行时间具有某些不确定性,因此在timer_bh()函数得到真正执行之前,期间可能又会有几次时钟中断发
生。这样就会造成时钟滴答的丢失现象。为了处理这种情况,Linux内核使用了一个辅助全局变量wall_jiffies,来表示上一次更
新xtime时的jiffies值。其定义如下(kernel/timer.c):
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
而timer_bh()函数真正执行时的jiffies值与wall_jiffies的差就是在timer_bh()真正执行之前所发生的时钟中断次数。
函数update_times()的源码如下(kernel/timer.c):
static inline void update_times(void)
{
unsigned long ticks;

/*
* update_times() is run from the raw timer_bh handler so we
* just know that the irqs are locally enabled and so we don't
* need to save/restore the flags of the local CPU here. -arca
*/
write_lock_irq(&xtime_lock);

ticks = jiffies - wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
update_wall_time(ticks);
}
write_unlock_irq(&xtime_lock);
calc_load(ticks);
}
(1)首先,根据jiffies和wall_jiffies的差值计算在此之前一共发生了几次时钟滴答,并将这个值保存到局部变量ticks中。
并在ticks值大于0的情况下(ticks大于等于1,一般情况下为1):①更新wall_jiffies为jiffies变量的当前值
(wall_jiffies+=ticks等价于wall_jiffies=jiffies)。②以参数ticks调用update_wall_time()函数去真正地更新全
局时间xtime。
(2)调用calc_load()函数去计算系统负载情况。这里我们不去深究它。

函数update_wall_time()函数根据参数ticks所指定的时钟滴答次数相应地更新内核全局时间变量xtime。其源码如下
(kernel/timer.c):
/*
* Using a loop looks inefficient, but "ticks" is
* usually just one (we shouldn't be losing ticks,
* we're doing this this way mainly for interrupt
* latency reasons, not because we think we'll
* have lots of lost timer ticks
*/
static void update_wall_time(unsigned long ticks)
{
do {
ticks--;
update_wall_time_one_tick();
} while (ticks);

if (xtime.tv_usec >= 1000000) {
xtime.tv_usec -= 1000000;
xtime.tv_sec++;
second_overflow();
}
}
对该函数的注释如下:
(1)首先,用一个do{}循环来根据参数ticks的值一次一次调用update_wall_time_one_tick()函数来为一次时钟滴答更新
xtime中的tv_usec成员。
(2)根据需要调整xtime中的秒数成员tv_usec和微秒数成员tv_usec。如果微秒数成员tv_usec的值超过106,则说明已经过了
一秒钟。因此将tv_usec的值减去1000000,并将秒数成员tv_sec的值加1,然后调用second_overflow()函数来处理微秒数成
员溢出的情况。

函数update_wall_time_one_tick()用来更新一次时钟滴答对系统全局时间xtime的影响。由于tick全局变量表示了一次时钟
滴答的时间间隔长度(以us为单位),因此该函数的实现中最核心的代码就是将xtime的tv_usec成员增加tick微秒。这里我们不
去关心函数实现中与NTP(Network Time Protocol)和系统调用adjtimex()的相关部分。其源码如下(kernel/
timer.c):
/* in the NTP reference this is called "hardclock()" */
static void update_wall_time_one_tick(void)
{
if ( (time_adjust_step = time_adjust) != 0 ) {
/* We are doing an adjtime thing.
*
* Prepare time_adjust_step to be within bounds.
* Note that a positive time_adjust means we want the clock
* to run faster.
*
* Limit the amount of the step to be in the range
* -tickadj .. +tickadj
*/
if (time_adjust > tickadj)
time_adjust_step = tickadj;
else if (time_adjust > SHIFT_SCALE;
time_phase += ltemp = FINEUSEC) {
long ltemp = time_phase >> SHIFT_SCALE;
time_phase -= ltemp list.next = timer->list.prev = NULL;
}
由于定时器通常被连接在一个双向循环队列中等待执行(此时我们说定时器处于pending状态)。因此函数time_pending()就可以
用list成员是否为空来判断一个定时器是否处于pending状态。如下所示(include/linux/timer.h):
static inline int timer_pending (const struct timer_list * timer)
{
return timer->list.next != NULL;
}

l 时间比较操作
在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操
作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏
(include/linux/timer.h):
#define time_after(a,b) ((long)(b) - (long)(a) = 0)
#define time_before_eq(a,b) time_after_eq(b,a)

7.6.2 动态内核定时器机制的原理
Linux是怎样为其内核定时器机制提供动态扩展能力的呢?其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双
向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每
一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。
显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最
大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指
针,这显然是不现实的。
另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),
也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为
计数单位的)。
基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下
列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时
器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256
个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是
将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时
器的expires值可以互不相同的一个定时器队列。
具体的组织方案可以分为两大部分:
(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组
织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如
下述代码段所示:
/*
* Event timer code
*/
#define TVN_BITS 6
#define TVR_BITS 8
#define TVN_SIZE (1 >8)具有相同值的定时器都将被组
织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要26=64个松散定时器向
量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构
timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。
对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个
松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个松散定时器向量。类似
地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。
对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放
在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要26=64个松散定时器
向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定
时器向量。
对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将
被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要26=64个松散
定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个
松散定时器向量。

最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。
整个内核定时器机制的总体结构如下图7-8所示:

7.6.3 内核动态定时器机制的实现
在内核动态定时器机制的实现中,有三个操作时非常重要的:(1)将一个定时器插入到它应该所处的定时器向量中。(2)定时器的
迁移,也即将一个定时器从它原来所处的定时器向量迁移到另一个定时器向量中。(3)扫描并执行当前已经到期的定时器。

7.6.3.1 动态定时器机制的初始化
函数init_timervecs()实现对动态定时器机制的初始化。该函数仅被sched_init()初始化例程所调用。动态定时器机制初始化
过程的主要任务就是将tv1、tv2、…、tv5这5个结构变量中的定时器向量指针数组vec[]初始化为NULL。如下所示(kernel/
timer.c):
void init_timervecs (void)
{
int i;

for (i = 0; i expires;
unsigned long idx = expires - timer_jiffies;
struct list_head * vec;

if (idx > TVR_BITS) & TVN_MASK;
vec = tv2.vec + i;
} else if (idx > (TVR_BITS + TVN_BITS)) & TVN_MASK;
vec = tv3.vec + i;
} else if (idx > (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
vec = tv4.vec + i;
} else if ((signed long) idx > (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
vec = tv5.vec + i;
} else {
/* Can only get here on architectures with 64-bit jiffies */
INIT_LIST_HEAD(&timer->list);
return;
}
/*
* Timers are FIFO!
*/
list_add(&timer->list, vec->prev);
}
对该函数的注释如下:
(1)首先,计算定时器的expires值与timer_jiffies的插值(注意!这里应该使用动态定时器自己的时间基准),这个差值就表
示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量idx保存这个差值。
(2)根据idx的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在7.6.2节已经说过了,这里不再详述。
最后,定时器向量的头部指针vec表示这个定时器应该所处的定时器向量链表头部。
(3)最后,调用list_add()函数将定时器插入到vec指针所指向的定时器队列的尾部。

7.6.3.5 修改一个定时器的expires值
当一个定时器已经被插入到内核动态定时器链表中后,我们还可以修改该定时器的expires值。函数mod_timer()实现这一点。如
下所示(kernel/timer.c):
int mod_timer(struct timer_list *timer, unsigned long expires)
{
int ret;
unsigned long flags;

spin_lock_irqsave(&timerlist_lock, flags);
timer->expires = expires;
ret = detach_timer(timer);
internal_add_timer(timer);
spin_unlock_irqrestore(&timerlist_lock, flags);
return ret;
}
该函数首先根据参数expires值更新定时器的expires成员。然后调用detach_timer()函数将该定时器从它原来所属的链表中删
除。最后调用internal_add_timer()函数将该定时器根据它新的expires值重新插入到相应的链表中。

函数detach_timer()首先调用timer_pending()来判断指定的定时器是否已经处于某个链表中,如果定时器原来就不处于任何链
表中,则detach_timer()函数什么也不做,直接返回0值,表示失败。否则,就调用list_del()函数将定时器从它原来所处的链
表中摘除。如下所示(kernel/timer.c):
static inline int detach_timer (struct timer_list *timer)
{
if (!timer_pending(timer))
return 0;
list_del(&timer->list);
return 1;
}

7.6.3.6 删除一个定时器
函数del_timer()用来将一个定时器从相应的内核定时器队列中删除。该函数实际上是对detach_timer()函数的高层封装。如下
所示(kernel/timer.c):
int del_timer(struct timer_list * timer)
{
int ret;
unsigned long flags;

spin_lock_irqsave(&timerlist_lock, flags);
ret = detach_timer(timer);
timer->list.next = timer->list.prev = NULL;
spin_unlock_irqrestore(&timerlist_lock, flags);
return ret;
}

7.6.3.7 定时器迁移操作
由于一个定时器的interval值会随着时间的不断流逝(即jiffies值的不断增大)而不断变小,因此那些原本到期紧迫程度较低的
定时器会随着jiffies值的不断增大而成为既将马上到期的定时器。比如定时器向量tv2.vec[0]中的定时器在经过256个时钟滴答
后会成为未来256个时钟滴答内会到期的定时器。因此,定时器在内核动态定时器链表中的位置也应相应地随着改变。改变的规则
是:当tv1.index重新变为0时(意味着tv1中的256个定时器向量都已被内核扫描一遍了,从而使tv1中的256个定时器向量变为
空),则用tv2.vec[index]定时器向量中的定时器去填充tv1,同时使tv2.index加1(它以64为模)。当tv2.index重新变为
0(意味着tv2中的64个定时器向量都已经被全部填充到tv1中去了,从而使得tv2变为空),则用tv3.vec[index]定时器向量中
的定时器去填充tv2。如此一直类推下去,直到tv5。
函数cascade_timers()完成这种定时器迁移操作,该函数只有一个timer_vec结构类型指针的参数tv。这个函数将把定时器向量
tv->vec[tv->index]中的所有定时器重新填充到上一层定时器向量中去。如下所示(kernel/timer.c):
static inline void cascade_timers(struct timer_vec *tv)
{
/* cascade all the timers from tv up one level */
struct list_head *head, *curr, *next;

head = tv->vec + tv->index;
curr = head->next;
/*
* We are removing _all_ timers from the list, so we don't have to
* detach them individually, just clear the list afterwards.
*/
while (curr != head) {
struct timer_list *tmp;

tmp = list_entry(curr, struct timer_list, list);
next = curr->next;
list_del(curr); // not needed
internal_add_timer(tmp);
curr = next;
}
INIT_LIST_HEAD(head);
tv->index = (tv->index + 1) & TVN_MASK;
}
对该函数的注释如下:
(1)首先,用指针head指向定时器头部向量头部的list_head结构。指针curr指向定时器向量中的第一个定时器。
(2)然后,用一个while{}循环来遍历定时器向量tv->vec[tv->index]。由于定时器向量是一个双向循环队列,因此循环的终
止条件是curr=head。对于每一个被扫描的定时器,循环体都先调用list_del()函数将当前定时器从链表中摘除,然后调用
internal_add_timer()函数重新确定该定时器应该被放到哪个定时器向量中去。
(3)当从while{}循环退出后,定时器向量tv->vec[tv->index]中所有的定时器都已被迁移到其它地方(到它们该呆的地方:
-),因此它本身就成为一个空队列。这里我们显示地调用INIT_LIST_HEAD()宏来将定时器向量的表头结构初始化为空。
(4)最后,将tv->index值加1,当然它是以64为模。

7.6.4.8 扫描并执行当前已经到期的定时器
函数run_timer_list()完成这个功能。如前所述,该函数是被timer_bh()函数所调用的,因此内核定时器是在时钟中断的
Bottom Half中被执行的。记住这一点非常重要。全局变量timer_jiffies表示了内核上一次执行run_timer_list()函数的时
间,因此jiffies与timer_jiffies的差值就表示了自从上一次处理定时器以来,期间一共发生了多少次时钟中断,显然
run_timer_list()函数必须为期间所发生的每一次时钟中断补上定时器服务。该函数的源码如下(kernel/timer.c):
static inline void run_timer_list(void)
{
spin_lock_irq(&timerlist_lock);
while ((long)(jiffies - timer_jiffies) >= 0) {
struct list_head *head, *curr;
if (!tv1.index) {
int n = 1;
do {
cascade_timers(tvecs[n]);
} while (tvecs[n]->index == 1 && ++n next;
if (curr != head) {
struct timer_list *timer;
void (*fn)(unsigned long);
unsigned long data;

timer = list_entry(curr, struct timer_list, list);
fn = timer->function;
data= timer->data;

detach_timer(timer);
timer->list.next = timer->list.prev = NULL;
timer_enter(timer);
spin_unlock_irq(&timerlist_lock);
fn(data);
spin_lock_irq(&timerlist_lock);
timer_exit();
goto repeat;
}
++timer_jiffies;
tv1.index = (tv1.index + 1) & TVR_MASK;
}
spin_unlock_irq(&timerlist_lock);
}
函数run_timer_list()的执行过程主要就是用一个大while{}循环来为时钟中断执行定时器服务,每一次循环服务一次时钟中
断。因此一共要执行(jiffies-timer_jiffies+1)次循环。循环体所执行的服务步骤如下:
(1)首先,判断tv1.index是否为0,如果为0则需要从tv2中补充定时器到tv1中来。但tv2也可能为空而需要从tv3中补充定时
器,因此用一个do{}while循环来调用cascade_timer()函数来依次视需要从tv2中补充tv1,从tv3中补充tv2、…、从tv5中补
充tv4。显然如果tvi.index=0(2≤i≤5),则对于tvi执行cascade_timers()函数后,tvi.index肯定为1。反过来讲,如果对
tvi执行过cascade_timers()函数后tvi.index不等于1,那么可以肯定在未对tvi执行cascade_timers()函数之
前,tvi.index值肯定不为0,因此这时tvi不需要从tv(i+1)中补充定时器,这时就可以终止do{}while循环。
(2)接下来,就要执行定时器向量tv1.vec[tv1.index]中的所有到期定时器。因此这里用一个goto repeat循环从头到尾依
次扫描整个定时器对列。由于在执行定时器的关联函数时并不需要关CPU中断,所以在用detach_timer()函数将当前定时器从对列
中摘除后,就可以调用spin_unlock_irq()函数进行解锁和开中断,然后在执行完当前定时器的关联函数后重新用
spin_lock_irq()函数加锁和关中断。
(3)当执行完定时器向量tv1.vec[tv1.index]中的所有到期定时器后,tv1.vec[tv1.index]应该是个空队列。至此这一次
定时器服务也就宣告结束。
(4)最后,将timer_jiffies值加1,将tv1.index值加1,当然它的模是256。然后,回到while循环开始下一次定时器服务。
7.7 进程间隔定时器itimer
所谓“间隔定时器(Interval Timer,简称itimer)就是指定时器采用“间隔”值(interval)来作为计时方式,当定时器启动
后,间隔值interval将不断减小。当interval值减到0时,我们就说该间隔定时器到期。与上一节所说的内核动态定时器相比,二
者最大的区别在于定时器的计时方式不同。内核定时器是通过它的到期时刻expires值来计时的,当全局变量jiffies值大于或等于
内核动态定时器的expires值时,我们说内核内核定时器到期。而间隔定时器则实际上是通过一个不断减小的计数器来计时的。虽然
这两种定时器并不相同,但却也是相互联系的。假如我们每个时钟节拍都使间隔定时器的间隔计数器减1,那么在这种情形下间隔定
时器实际上就是内核动态定时器(下面我们会看到进程的真实间隔定时器就是这样通过内核定时器来实现的)。
间隔定时器主要被应用在用户进程上。每个Linux进程都有三个相互关联的间隔定时器。其各自的间隔计数器都定义在进程的
task_struct结构中,如下所示(include/linux/sched.h):
struct task_struct{
……
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
……
}
(1)真实间隔定时器(ITIMER_REAL):这种间隔定时器在启动后,不管进程是否运行,每个时钟滴答都将其间隔计数器减1。当
减到0值时,内核向进程发送SIGALRM信号。结构类型task_struct中的成员it_real_incr则表示真实间隔定时器的间隔计数器的
初始值,而成员it_real_value则表示真实间隔定时器的间隔计数器的当前值。由于这种间隔定时器本质上与上一节的内核定时器
时一样的,因此Linux实际上是通过real_timer这个内嵌在task_struct结构中的内核动态定时器来实现真实间隔定时器
ITIMER_REAL的。
(2)虚拟间隔定时器ITIMER_VIRT:也称为进程的用户态间隔定时器。结构类型task_struct中成员it_virt_incr和
it_virt_value分别表示虚拟间隔定时器的间隔计数器的初始值和当前值,二者均以时钟滴答次数位计数单位。当虚拟间隔定时器
启动后,只有当进程在用户态下运行时,一次时钟滴答才能使间隔计数器当前值it_virt_value减1。当减到0值时,内核向进程发
送SIGVTALRM信号(虚拟闹钟信号),并将it_virt_value重置为初值it_virt_incr。具体请见7.4.3节中的do_it_virt()函
数的实现。
(3)PROF间隔定时器ITIMER_PROF:进程的task_struct结构中的it_prof_value和it_prof_incr成员分别表示PROF间隔定
时器的间隔计数器的当前值和初始值(均以时钟滴答为单位)。当一个进程的PROF间隔定时器启动后,则只要该进程处于运行中,
而不管是在用户态或核心态下执行,每个时钟滴答都使间隔计数器it_prof_value值减1。当减到0值时,内核向进程发送SIGPROF
信号,并将it_prof_value重置为初值it_prof_incr。具体请见7.4.3节的do_it_prof()函数。
Linux在include/linux/time.h头文件中为上述三种进程间隔定时器定义了索引标识,如下所示:
#define ITIMER_REAL 0
#define ITIMER_VIRTUAL 1
#define ITIMER_PROF 2

7.7.1 数据结构itimerval
虽然,在内核中间隔定时器的间隔计数器是以时钟滴答次数为单位,但是让用户以时钟滴答为单位来指定间隔定时器的间隔计数器的
初值显然是不太方便的,因为用户习惯的时间单位是秒、毫秒或微秒等。所以Linux定义了数据结构itimerval来让用户以秒或微秒
为单位指定间隔定时器的时间间隔值。其定义如下(include/linux/time.h):
struct itimerval {
struct timeval it_interval; /* timer interval */
struct timeval it_value; /* current value */
};
其中,it_interval成员表示间隔计数器的初始值,而it_value成员表示间隔计数器的当前值。这两个成员都是timeval结构类型
的变量,因此其精度可以达到微秒级。

l timeval与jiffies之间的相互转换
由于间隔定时器的间隔计数器的内部表示方式与外部表现方式互不相同,因此有必要实现以微秒为单位的timeval结构和为时钟滴答
次数单位的jiffies之间的相互转换。为此,Linux在kernel/itimer.c中实现了两个函数实现二者的互相转换——
tvtojiffies()函数和jiffiestotv()函数。它们的源码如下:
static unsigned long tvtojiffies(struct timeval *value)
{
unsigned long sec = (unsigned) value->tv_sec;
unsigned long usec = (unsigned) value->tv_usec;

if (sec > (ULONG_MAX / HZ))
return ULONG_MAX;
usec += 1000000 / HZ - 1;
usec /= 1000000 / HZ;
return HZ*sec+usec;
}

static void jiffiestotv(unsigned long jiffies, struct timeval *value)
{
value->tv_usec = (jiffies % HZ) * (1000000 / HZ);
value->tv_sec = jiffies / HZ;
}

7.7.2 真实间隔定时器ITIMER_REAL的底层运行机制
间隔定时器ITIMER_VIRT和ITIMER_PROF的底层运行机制是分别通过函数do_it_virt()函数和do_it_prof()函数来实现
的,这里就不再重述(可以参见7.4.3节)。
由于间隔定时器ITIMER_REAL本质上与内核动态定时器并无区别。因此内核实际上是通过内核动态定时器来实现进程的
ITIMER_REAL间隔定时器的。为此,task_struct结构中专门设立一个timer_list结构类型的成员变量real_timer。动态定时
器real_timer的函数指针function总是被task_struct结构的初始化宏INIT_TASK设置为指向函数it_real_fn()。如下所示
(include/linux/sched.h):
#define INIT_TASK(tsk)
……
real_timer: {
function: it_real_fn
}
……
}
而real_timer链表元素list和data成员总是被进程创建时分别初始化为空和进程task_struct结构的地址,如下所示(kernel/
fork.c):
int do_fork(……)
{
……
p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
init_timer(&p->real_timer);
p->real_timer.data = (unsigned long)p;
……
}
当用户通过setitimer()系统调用来设置进程的ITIMER_REAL间隔定时器时,it_real_incr被设置成非零值,于是该系统调用相
应地设置好real_timer.expires值,然后进程的real_timer定时器就被加入到内核动态定时器链表中,这样该进程的
ITIMER_REAL间隔定时器就被启动了。当real_timer定时器到期时,它的关联函数it_real_fn()将被执行。注意!所有进程的
real_timer定时器的function函数指针都指向it_real_fn()这同一个函数,因此it_real_fn()函数必须通过其参数来识别是
哪一个进程,为此它将unsigned long类型的参数p解释为进程task_struct结构的地址。该函数的源码如下(kernel/
itimer.c):
void it_real_fn(unsigned long __data)
{
struct task_struct * p = (struct task_struct *) __data;
unsigned long interval;

send_sig(SIGALRM, p, 1);
interval = p->it_real_incr;
if (interval) {
if (interval > (unsigned long) LONG_MAX)
interval = LONG_MAX;
p->real_timer.expires = jiffies + interval;
add_timer(&p->real_timer);
}
}
函数it_real_fn()的执行过程大致如下:
(1)首先将参数p通过强制类型转换解释为进程的task_struct结构类型的指针。
(2)向进程发送SIGALRM信号。
(3)在进程的it_real_incr非0的情况下继续启动real_timer定时器。首先,计算real_timer定时器的expires值为
(jiffies+it_real_incr)。然后,调用add_timer()函数将real_timer加入到内核动态定时器链表中。

7.7.3 itimer定时器的系统调用
与itimer定时器相关的syscall有两个:getitimer()和setitimer()。其中,getitimer()用于查询调用进程的三个间隔定时
器的信息,而setitimer()则用来设置调用进程的三个间隔定时器。这两个syscall都是现在kernel/itimer.c文件中。

7.7.3.1 getitimer()系统调用的实现
函数sys_getitimer()有两个参数:(1)which,指定查询调用进程的哪一个间隔定时器,其取值可以是
ITIMER_REAL、ITIMER_VIRT和ITIMER_PROF三者之一。(2)value指针,指向用户空间中的一个itimerval结构,用于接收查
询结果。该函数的源码如下:
/* SMP: Only we modify our itimer values. */
asmlinkage long sys_getitimer(int which, struct itimerval *value)
{
int error = -EFAULT;
struct itimerval get_buffer;

if (value) {
error = do_getitimer(which, &get_buffer);
if (!error &&
copy_to_user(value, &get_buffer, sizeof(get_buffer)))
error = -EFAULT;
}
return error;
}
显然,sys_getitimer()函数主要通过do_getitimer()函数来查询当前进程的间隔定时器信息,并将查询结果保存在内核空间的
结构变量get_buffer中。然后,调用copy_to_usr()宏将get_buffer中结果拷贝到用户空间缓冲区中。
函数do_getitimer()的源码如下(kernel/itimer.c):
int do_getitimer(int which, struct itimerval *value)
{
register unsigned long val, interval;

switch (which) {
case ITIMER_REAL:
interval = current->it_real_incr;
val = 0;
/*
* FIXME! This needs to be atomic, in case the kernel timer happens!
*/
if (timer_pending(¤t->real_timer)) {
val = current->real_timer.expires - jiffies;

/* look out for negative/zero itimer.. */
if ((long) val it_virt_value;
interval = current->it_virt_incr;
break;
case ITIMER_PROF:
val = current->it_prof_value;
interval = current->it_prof_incr;
break;
default:
return(-EINVAL);
}
jiffiestotv(val, &value->it_value);
jiffiestotv(interval, &value->it_interval);
return 0;
}
查询的过程如下:
(1)首先,用局部变量val和interval分别表示待查询间隔定时器的间隔计数器的当前值和初始值。
(2)如果which=ITIMER_REAL,则查询当前进程的ITIMER_REAL间隔定时器。于是从current->it_real_incr中得到
ITIMER_REAL间隔定时器的间隔计数器的初始值,并将其保存到interval局部变量中。而对于间隔计数器的当前值,由于
ITITMER_REAL间隔定时器是通过real_timer这个内核动态定时器来实现的,因此不能通过current->it_real_value来获得
ITIMER_REAL间隔定时器的间隔计数器的当前值,而必须通过real_timer来得到这个值。为此先用timer_pending()函数来判断
current->real_timer是否已被起动。如果未启动,则说明ITIMER_REAL间隔定时器也未启动,因此其间隔计数器的当前值肯定
是0。因此将val变量简单地置0就可以了。如果已经启动,则间隔计数器的当前值应该等于(timer_real.expires-
jiffies)。
(3)如果which=ITIMER_VIRT,则查询当前进程的ITIMER_VIRT间隔定时器。于是简单地将计数器初值it_virt_incr和当前
值it_virt_value分别保存到局部变量interval和val中。
(4)如果which=ITIMER_PROF,则查询当前进程的ITIMER_PROF间隔定时器。于是简单地将计数器初值it_prof_incr和当前
值it_prof_value分别保存到局部变量interval和val中。
(5)最后,通过转换函数jiffiestotv()将val和interval转换成timeval格式的时间值,并保存到value->it_value和
value->it_interval中,作为查询结果返回。

7.7.3.2 setitimer()系统调用的实现
函数sys_setitimer()不仅设置调用进程的指定间隔定时器,而且还返回该间隔定时器的原有信息。它有三个参数:(1)which,
含义与sys_getitimer()中的参数相同。(2)输入参数value,指向用户空间中的一个itimerval结构,含有待设置的新
值。(3)输出参数ovalue,指向用户空间中的一个itimerval结构,用于接收间隔定时器的原有信息。
该函数的源码如下(kernel/itimer.c):
/* SMP: Again, only we play with our itimers, and signals are SMP safe
* now so that is not an issue at all anymore.
*/
asmlinkage long sys_setitimer(int which, struct itimerval *value,
struct itimerval *ovalue)
{
struct itimerval set_buffer, get_buffer;
int error

阅读(1553) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~