LINUX内核时钟中断机制

更新时间:2023-05-21 02:14:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

UNIX概述7

Linux内核的时钟中断机制 opyright © 2003 by 詹荣开

E-mail:zhanrk@

Linux-2.4.0

Version 1.0.0,2003-2-14

摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。 关键词:Linux、时钟、定时器

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给: The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。

前言

时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。一般说来,内核主要需要两种类型的时间:

(1)、在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。

(2)、维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。

PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡

UNIX概述7

器来提供时钟方波信号输入。这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);

(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。

1、 时钟硬件

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。

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)

UNIX概述7

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

UNIX概述7

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,未定义。

1.1.2 通过I/O端口访问RTC

在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端口。

UNIX概述7

读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

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”状态。

(65536)=18.2HZ,通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/

也就是说,此时一秒钟之内将产生18.2次时钟中断。

1.2.1 PIT的I/O端口

在i386平台上,8254芯片的各寄存器的I/O端口地址如下:

Port Description

40h Channel 0 counter(read/write)

UNIX概述7

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格式存储。

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。

1.2.3 锁存计数器(Latch Counter)

当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O

UNIX概述7

端口可以读到一个稳定的计数器值,因为计数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。

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纳秒增加一次。

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头文件中。

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。

UNIX概述7

端口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。

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

UNIX概述7

#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

正如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,它与其中的各标志

UNIX概述7

位的定义如下所示:

#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)<<4) + (val)%10)

UNIX概述7

#endif

2.3 内核对RTC的操作

如前所述,Linux内核与RTC进行互操作的时机只有两个:(1)内核在启动时从RTC中读取启动时的时间与日期;(2)内核在需要时将时间与日期回写到RTC中。为此,Linux内核在arch/i386/kernel/time.c文件中实现了函数get_cmos_time()来进行对RTC的第一种操作。显然,get_cmos_time()函数仅仅在内核启动时被调用一次。而对于第二种操作,Linux则同样在arch/i386/kernel/time.c文件中实现了函数set_rtc_mmss(),以支持向RTC中回写当前时间与日期。下面我们将来分析这二个函数的实现。

在分析get_cmos_time()函数之前,我们先来看看RTC芯片对其时间与日期寄存器组的更新原理。

(1)Update In Progress

当控制寄存器B中的SET标志位为0时,MC146818芯片每秒都会在芯片内部执行一个“更新周期”(Update Cycle),其作用是增加秒寄存器的值,并检查秒寄存器是否溢出。如果溢出,则增加分钟寄存器的值,如此一致下去直到年寄存器。在“更新周期”期间,时间与日期寄存器组(0x00~0x09)是不可用的,此时如果读取它们的值将得到未定义的值,因为MC146818在整个更新周期期间会把时间与日期寄存器组从CPU总线上脱离,从而防止软件程序读到一个渐变的数据。

在MC146818的输入时钟频率(也即晶体增荡器的频率)为4.194304MHZ或1.048576MHZ的情况下,“更新周期”需要花费248us,而对于输入时钟频率为32.768KHZ的情况,“更新周期”需要花费1984us=1.984ms。控制寄存器A中的UIP标志位用来表示MC146818是否正处于更新周期中,当UIP从0变为1的那个时刻,就表示MC146818将在稍后马上就开更新周期。在UIP从0变到1的那个时刻与MC146818真正开始Update Cycle的那个时刻之间时有一段时间间隔的,通常是244us。也就是说,在UIP从0变到1的244us之后,时间与日期寄存器组中的值才会真正开始改变,而在这之间的244us间隔内,它们的值并不会真正改变。

(2)get_cmos_time()函数

该函数只被内核的初始化例程time_init()和内核的APM模块所调用。其源码如下: /* not static: needed by APM */

unsigned long get_cmos_time(void)

{

UNIX概述7

unsigned int year, mon, day, hour, min, sec;

int i;

/* The Linux interpretation of the CMOS clock register contents:

* When the Update-In-Progress (UIP) flag goes from 1 to 0, the

* RTC registers show the second which has precisely just started.

* Let's hope other operating systems interpret the RTC the same way.

*/

/* read RTC exactly on falling edge of update flag */

for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */

if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)

break;

for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */

if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))

break;

do { /* Isn't this overkill ? UIP above should guarantee consistency */

sec = CMOS_READ(RTC_SECONDS);

min = CMOS_READ(RTC_MINUTES);

hour = CMOS_READ(RTC_HOURS);

day = CMOS_READ(RTC_DAY_OF_MONTH);

mon = CMOS_READ(RTC_MONTH);

year = CMOS_READ(RTC_YEAR);

} while (sec != CMOS_READ(RTC_SECONDS));

if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD) {

BCD_TO_BIN(sec);

BCD_TO_BIN(min);

BCD_TO_BIN(hour);

BCD_TO_BIN(day);

BCD_TO_BIN(mon);

BCD_TO_BIN(year);

UNIX概述7

}

if ((year += 1900) < 1970)

year += 100;

return mktime(year, mon, day, hour, min, sec);

}

对该函数的注释如下:

(1)在从RTC中读取时间时,由于RTC存在Update Cycle,因此软件发出读操作的时机是很重要的。对此,get_cmos_time()函数通过UIP标志位来解决这个问题:第一个for循环不停地读取RTC频率选择寄存器中的UIP标志位,并且只要读到UIP的值为1就马上退出这个for循环。第二个for循环同样不停地读取UIP标志位,但他只要一读到UIP的值为0就马上退出这个for循环。这两个for循环的目的就是要在软件逻辑上同步RTC的Update Cycle,显然第二个for循环最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)

(2)从第二个for循环退出后,RTC的Update Cycle已经结束。此时我们就已经把当前时间逻辑定准在RTC的当前一秒时间间隔内。也就是说,这是我们就可以开始从RTC寄存器中读取当前时间值。但是要注意,读操作应该保证在244us内完成(准确地说,读操作要在RTC的下一个更新周期开始之前完成,244us的限制是过分偏执的:-)。所以,get_cmos_time()函数接下来通过CMOS_READ()宏从RTC中依次读取秒、分钟、小时、日期、月份和年分。这里的do{}while(sec!=CMOS_READ(RTC_SECOND))循环就是用来确保上述6个读操作必须在下一个Update Cycle开始之前完成。

(3)接下来判定时间的数据格式,PC机中一般总是使用BCD格式的时间,因此需要通过BCD_TO_BIN()宏把BCD格式转换为二进制格式。

(4)接下来对年分进行修正,以将年份转换为“19XX”的格式,如果是1970以前的年份,则将其加上100。

(5)最后调用mktime()函数将当前时间与日期转换为相对于1970-01-01 00:00:00的秒数值,并将其作为函数返回值返回。

函数mktime()定义在include/linux/time.h头文件中,它用来根据Gauss算法将以year/mon/day/hour/min/sec(如1980-12-31 23:59:59)格式表示的时间转换为相对于1970-01-01 00:00:00这个UNIX时间基准以来的相对秒数。其源码如下:

static inline unsigned long

mktime (unsigned int year, unsigned int mon,

UNIX概述7

unsigned int day, unsigned int hour,

unsigned int min, unsigned int sec)

{

if (0 >= (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);

UNIX概述7

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) < 30) {

if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) {

BIN_TO_BCD(real_seconds);

BIN_TO_BCD(real_minutes);

}

CMOS_WRITE(real_seconds,RTC_SECONDS);

CMOS_WRITE(real_minutes,RTC_MINUTES);

} else {

printk(KERN_WARNING

"set_rtc_mmss: can't update from %d to %d\n",

cmos_minutes, real_minutes);

retval = -1;

}

/* The following flags have to be released exactly in this order,

* otherwise the DS12887 (popular MC146818A clone with integrated

* battery and quartz) will not reset the oscillator and will not

* update precisely 500 ms later. You won't find this mentioned in

UNIX概述7

* the Dallas Semiconductor data sheets, but who believes data

* sheets anyway ... -- Markus Kuhn

*/

CMOS_WRITE(save_control, RTC_CONTROL);

CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT);

spin_unlock(&rtc_lock);

return retval;

}

对该函数的注释如下:

(1)首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。

(2)接下来,在RTC控制寄存器中设置SET标志位,以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL寄存器的当前值读到变量save_control中,然后再把值(save_control | RTC_SET)回写到寄存器RTC_CONTROL中。

(3)然后,通过RTC_FREQ_SELECT寄存器中bit[6:4]重启RTC芯片内部的除法器。为此,类似地先把RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中,然后再把值(save_freq_select | RTC_DIV_RESET2)回写到RTC_FREQ_SELECT寄存器中。

(4)接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中,并根据需要将它从BCD格式转化为二进制格式。

(5)从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意,这里对于半小时区的情况要修正分钟数real_minutes的值。

(6)然后,在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下,将real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然,在回写之前要记得把二进制转换为BCD格式。

(7)最后,恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是:先恢复RTC_CONTROL寄存器,再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。

最后,需要说明的一点是,set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置(也即500ms处)左右被调用。此外,Linux内核对每一次成功的更新RTC时间都留下时间轨迹,它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间(单

UNIX概述7

位是秒数)。该变量定义在arch/i386/kernel/time.c文件中:

/* last time the cmos clock got updated */

static long last_rtc_update;

每一次成功地调用set_rtc_mmss()函数后,内核都会马上将last_rtc_update更新为当前时间(具体请见4.3节)。

3、Linux对时间的表示

通常,操作系统可以使用三种方法来表示系统的当前时间与日期:①最简单的一种方法就是直接用一个64位的计数器来对时钟滴答进行计数。②第二种方法就是用一个32位计数器来对秒进行计数,同时还用一个32位的辅助计数器对时钟滴答计数,之子累积到一秒为止。因为232超过136年,因此这种方法直至22世纪都可以让系统工作得很好。③第三种方法也是按时钟滴答进行计数,但是是相对于系统启动以来的滴答次数,而不是相对于相对于某个确定的外部时刻;当读外部后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。

UNIX类操作系统通常都采用第三种方法来维护系统的时间与日期。

3.1 基本概念

首先,有必要明确一些Linux内核时钟驱动中的基本概念。

(1)时钟周期(clock cycle)的频率:8253/8254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中:

#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */

(2)时钟滴答(clock tick):我们知道,当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。

(3)时钟滴答的频率(HZ):也即1秒时间内PIT所产生的时钟滴答次数。类似地,这个值也是由PIT通道0的计数器初值决定的(反过来说,确定了时钟滴答的频率值后也就可以确定8254 PIT通道0的计数器初值)。Linux内核用宏HZ来表示时钟滴答的频率,而且在不同的平台上HZ有不同的定义值。对于ALPHA和IA62平台HZ的值是1024,对于SPARC、MIPS、ARM和i386等平台HZ的值都是100。该宏在i386平台上的定义如下

UNIX概述7

(include/asm-i386/param.h):

#ifndef HZ

#define HZ 100

#endif

根据HZ的值,我们也可以知道一次时钟滴答的具体时间间隔应该是(1000ms/HZ)=10ms。

(4)时钟滴答的时间间隔:Linux用全局变量tick来表示时钟滴答的时间间隔长度,该变量定义在kernel/timer.c文件中,如下:

long tick = (1000000 + HZ/2) / HZ; /* timer interrupt period */

tick变量的单位是微妙(μs),由于在不同平台上宏HZ的值会有所不同,因此方程式tick=1000000÷HZ的结果可能会是个小数,因此将其进行四舍五入成一个整数,所以Linux将tick定义成(1000000+HZ/2)/HZ,其中被除数表达式中的HZ/2的作用就是用来将tick值向上圆整成一个整型数。

另外,Linux还用宏TICK_SIZE来作为tick变量的引用别名(alias),其定义如下(arch/i386/kernel/time.c):

#define TICK_SIZE tick

(5)宏LATCH:Linux用宏LATCH来定义要写到PIT通道0的计数器中的值,它表示PIT将没隔多少个时钟周期产生一次时钟中断。显然LATCH应该由下列公式计算:

LATCH=(1秒之内的时钟周期个数)÷(1秒之内的时钟中断次数)=(CLOCK_TICK_RATE)÷(HZ)

类似地,上述公式的结果可能会是个小数,应该对其进行四舍五入。所以,Linux将LATCH定义为(include/linux/timex.h):

/* LATCH is used in the interval timer and ftape setup. */

#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */

类似地,被除数表达式中的HZ/2也是用来将LATCH向上圆整成一个整数。

3.2 表示系统当前时间的内核数据结构

作为一种UNIX类操作系统,Linux内核显然采用本节一开始所述的第三种方法来表示系统的当前时间。Linux内核在表示系统当前时间时用到了三个重要的数据结构:

①全局变量jiffies:这是一个32位的无符号整数,用来表示自内核上一次启动以来的时钟滴答次数。每发生一次时钟滴答,内核的时钟中断处理函数timer_interrupt()都要将该全局变量jiffies加1。该变量定义在kernel/timer.c源文件中,如下所示:

UNIX概述7

unsigned long volatile jiffies;

C语言限定符volatile表示jiffies是一个易该变的变量,因此编译器将使对该变量的访问从不通过CPU内部cache来进行。

②全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示: struct timeval {

time_t tv_sec; /* seconds */

suseconds_t tv_usec; /* microseconds */

};

其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>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 */

UNIX概述7

int tz_dsttime; /* type of dst correction */

};

基于上述结构,Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处的时区信息,如下所示:

struct timezone sys_tz;

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寄存器。

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))

宏rdtscl宏rdtsc()同时读取TSC的LSB与MSB,并分别保存到宏参数low和high中。

则只读取TSC寄存器的LSB,并保存到宏参数low中。宏rdtscll读取TSC的当前64位值,并将其保存到宏参数val这个64位变量中。

3.3.2 校准TSC

与可编程定时器PIT相比,用TSC寄存器可以获得更精确的时间度量。但是在可以使用TSC之前,它必须精确地确定1个TSC计数值到底代表多长的时间间隔,也即到底要过多长时间

UNIX概述7

间隔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内核可以实现微秒级的高精度定时服务,也即可以确定两次时钟中断之间的某个时刻的微秒级时间值。

要确定时刻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()就是这样计算时间间隔偏移的,当然它

本文来源:https://www.bwwdw.com/article/x014.html

Top