核主要需要两种类型的时间:
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
是待写入寄存器的值:闞
TC_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()
将更新指定进程的总时间统计信息』
ask_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
个定时器向量』
56≤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
导醯
?
时,我们就说该间隔定时器到期。与上一节所说的内核动态定时器相比,二
者最大的区别在于定时器的计时方式不同。内核定时器是通过它的到期时刻
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
就可以了
imer_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'
还有问题请来论坛寻求帮助:
http://www.xxlinux.com/bbs/
posted on 2006-09-22 12:24
yuhen 阅读(3824)
评论(0) 编辑 收藏 引用 所属分类:
软件 、
硬件